diff --git a/src/Compilers/CSharp/Portable/FlowAnalysis/VariablesDeclaredWalker.cs b/src/Compilers/CSharp/Portable/FlowAnalysis/VariablesDeclaredWalker.cs index a6b1d3fda990e..0bae8577b2c6d 100644 --- a/src/Compilers/CSharp/Portable/FlowAnalysis/VariablesDeclaredWalker.cs +++ b/src/Compilers/CSharp/Portable/FlowAnalysis/VariablesDeclaredWalker.cs @@ -2,14 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Microsoft.CodeAnalysis.Collections; using Microsoft.CodeAnalysis.CSharp.Symbols; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; @@ -45,13 +44,13 @@ internal VariablesDeclaredWalker(CSharpCompilation compilation, Symbol member, B protected override void Free() { base.Free(); - _variablesDeclared = null; + _variablesDeclared = null!; } public override void VisitPattern(BoundPattern pattern) { - base.VisitPattern(pattern); NoteDeclaredPatternVariables(pattern); + base.VisitPattern(pattern); } protected override void VisitSwitchSection(BoundSwitchSection node, bool isLastSection) @@ -69,26 +68,109 @@ protected override void VisitSwitchSection(BoundSwitchSection node, bool isLastS /// private void NoteDeclaredPatternVariables(BoundPattern pattern) { - if (IsInside) + switch (pattern) { - switch (pattern) - { - case BoundObjectPattern p: + case BoundDeclarationPattern declarationPattern: + noteOneVariable(declarationPattern.Variable); + break; + + case BoundRecursivePattern recursivePattern: + foreach (var subpattern in recursivePattern.Deconstruction.NullToEmpty()) + NoteDeclaredPatternVariables(subpattern.Pattern); + + foreach (var subpattern in recursivePattern.Properties.NullToEmpty()) + NoteDeclaredPatternVariables(subpattern.Pattern); + + noteOneVariable(recursivePattern.Variable); + break; + + case BoundITuplePattern ituplePattern: + foreach (var subpattern in ituplePattern.Subpatterns) + NoteDeclaredPatternVariables(subpattern.Pattern); + + break; + + case BoundListPattern listPattern: + foreach (var elementPattern in listPattern.Subpatterns) + NoteDeclaredPatternVariables(elementPattern); + + noteOneVariable(listPattern.Variable); + break; + + case BoundConstantPattern constantPattern: + // It is possible for the region to be the expression within a pattern. + VisitRvalue(constantPattern.Value); + break; + + case BoundRelationalPattern relationalPattern: + // It is possible for the region to be the expression within a pattern. + VisitRvalue(relationalPattern.Value); + break; + + case BoundNegatedPattern negatedPattern: + NoteDeclaredPatternVariables(negatedPattern.Negated); + break; + + case BoundSlicePattern slicePattern: + if (slicePattern.Pattern != null) + NoteDeclaredPatternVariables(slicePattern.Pattern); + + break; + + case BoundDiscardPattern or BoundTypePattern: + // Does not contain variables or expressions. Nothing to visit. + break; + + case BoundBinaryPattern: + { + var binaryPattern = (BoundBinaryPattern)pattern; + if (binaryPattern.Left is not BoundBinaryPattern) { - // The variable may be null if it is a discard designation `_`. - if (p.Variable?.Kind == SymbolKind.Local) - { - // Because this API only returns local symbols and parameters, - // we exclude pattern variables that have become fields in scripts. - _variablesDeclared.Add(p.Variable); - } + NoteDeclaredPatternVariables(binaryPattern.Left); + NoteDeclaredPatternVariables(binaryPattern.Right); + break; } + + // Use an explicit stack to avoid crashing on deeply nested binary patterns. + // Binary patterns are left-associative, so, a nested pattern like: 'A or B or C or D or E' + // is parsed/bound like: '((((A or B) or C) or D) or E)' + // 1) Push the binary patterns onto stack from outermost to innermost. + // 2) Pop the innermost binary off the stack, and visit its left and right (corresponding to A and B in above example). + // 3) Continue popping binaries off the stack, visiting each right operand (corresponding to C, D, E, ...). + var stack = ArrayBuilder.GetInstance(); + do + { + stack.Push(binaryPattern); + binaryPattern = binaryPattern.Left as BoundBinaryPattern; + } while (binaryPattern is not null); + + binaryPattern = stack.Pop(); + NoteDeclaredPatternVariables(binaryPattern.Left); + + do + { + NoteDeclaredPatternVariables(binaryPattern.Right); + } while (stack.TryPop(out binaryPattern)); + + stack.Free(); break; + } + default: + throw ExceptionUtilities.UnexpectedValue(pattern.Kind); + } + + void noteOneVariable(Symbol? symbol) + { + if (IsInside && symbol?.Kind == SymbolKind.Local) + { + // Because this API only returns local symbols and parameters, + // we exclude pattern variables that have become fields in scripts. + _variablesDeclared.Add(symbol); } } } - public override BoundNode VisitLocalDeclaration(BoundLocalDeclaration node) + public override BoundNode? VisitLocalDeclaration(BoundLocalDeclaration node) { if (IsInside) { @@ -98,7 +180,7 @@ public override BoundNode VisitLocalDeclaration(BoundLocalDeclaration node) return base.VisitLocalDeclaration(node); } - public override BoundNode VisitLambda(BoundLambda node) + public override BoundNode? VisitLambda(BoundLambda node) { if (IsInside && !node.WasCompilerGenerated) { @@ -111,7 +193,7 @@ public override BoundNode VisitLambda(BoundLambda node) return base.VisitLambda(node); } - public override BoundNode VisitLocalFunctionStatement(BoundLocalFunctionStatement node) + public override BoundNode? VisitLocalFunctionStatement(BoundLocalFunctionStatement node) { if (IsInside && !node.WasCompilerGenerated) { @@ -142,7 +224,7 @@ public override void VisitForEachIterationVariables(BoundForEachStatement node) } } - public override BoundNode VisitCatchBlock(BoundCatchBlock catchBlock) + public override BoundNode? VisitCatchBlock(BoundCatchBlock catchBlock) { if (IsInside) { @@ -159,11 +241,11 @@ public override BoundNode VisitCatchBlock(BoundCatchBlock catchBlock) return null; } - public override BoundNode VisitQueryClause(BoundQueryClause node) + public override BoundNode? VisitQueryClause(BoundQueryClause node) { if (IsInside) { - if ((object)node.DefinedSymbol != null) + if ((object?)node.DefinedSymbol != null) { _variablesDeclared.Add(node.DefinedSymbol); } @@ -177,7 +259,7 @@ protected override void VisitLvalue(BoundLocal node) VisitLocal(node); } - public override BoundNode VisitLocal(BoundLocal node) + public override BoundNode? VisitLocal(BoundLocal node) { if (IsInside && node.DeclarationKind != BoundLocalDeclarationKind.None) { @@ -187,4 +269,4 @@ public override BoundNode VisitLocal(BoundLocal node) return null; } } -} +} \ No newline at end of file diff --git a/src/Compilers/CSharp/Test/Emit3/FlowAnalysis/PatternsVsRegions.cs b/src/Compilers/CSharp/Test/Emit3/FlowAnalysis/PatternsVsRegions.cs index e120d76d86137..3ec053ff35eb2 100644 --- a/src/Compilers/CSharp/Test/Emit3/FlowAnalysis/PatternsVsRegions.cs +++ b/src/Compilers/CSharp/Test/Emit3/FlowAnalysis/PatternsVsRegions.cs @@ -148,5 +148,563 @@ public static void Main(string[] args) Assert.Null(GetSymbolNamesJoined(dataFlowAnalysisResults.CapturedOutside)); Assert.Null(GetSymbolNamesJoined(dataFlowAnalysisResults.UnsafeAddressTaken)); } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/79489")] + public void RegionInIsPattern06() + { + var dataFlowAnalysisResults = CompileAndAnalyzeDataFlowExpression(""" + C.Use(doLogic: true); + + class C + { + public static (bool, string? errorMessage) M() => (false, "Something went wrong"); + + public static void Use(bool doLogic) + { + if (doLogic) + { + if (/**/M() is (false, var errorMessage)/**/) + { + Console.Error.WriteLine(errorMessage); + } + } + } + } + """); + VerifyDataFlowAnalysis(""" + VariablesDeclared: errorMessage + AlwaysAssigned: + Captured: + CapturedInside: + CapturedOutside: + DataFlowsIn: + DataFlowsOut: errorMessage + DefinitelyAssignedOnEntry: doLogic + DefinitelyAssignedOnExit: doLogic + ReadInside: + ReadOutside: doLogic, errorMessage + WrittenInside: errorMessage + WrittenOutside: doLogic + """, dataFlowAnalysisResults); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/79489")] + public void RegionInSwitch01() + { + var comp = CreateCompilation(""" + static void PatternMatchingUsage() + { + var t = (1, 2); + // selection start + switch (t) + { + case (var x, 2): + Console.WriteLine(x); + break; + } + // selection end + } + """); + + var tree = comp.SyntaxTrees[0]; + var model = comp.GetSemanticModel(tree); + + var @switch = tree.GetRoot().DescendantNodes().OfType().Single(); + var dataFlowAnalysisResults = model.AnalyzeDataFlow(@switch); + VerifyDataFlowAnalysis(""" + VariablesDeclared: x + AlwaysAssigned: + Captured: + CapturedInside: + CapturedOutside: + DataFlowsIn: t + DataFlowsOut: + DefinitelyAssignedOnEntry: t, args + DefinitelyAssignedOnExit: t, args + ReadInside: t, x + ReadOutside: + WrittenInside: x + WrittenOutside: t, args + """, dataFlowAnalysisResults); + } + + [Fact] + public void RegionInIsPattern07() + { + var dataFlowAnalysisResults = CompileAndAnalyzeDataFlowExpression(""" + C.Use(doLogic: true); + + class C + { + public static (bool, (string result, string? errorMessage)) M() => (false, "Something went wrong"); + + public static void Use(bool doLogic) + { + if (doLogic) + { + if (/**/M() is (false, (var inner, var errorMessage))/**/) + { + Console.Error.WriteLine(errorMessage); + } + } + } + } + + """); + VerifyDataFlowAnalysis(""" + VariablesDeclared: inner, errorMessage + AlwaysAssigned: + Captured: + CapturedInside: + CapturedOutside: + DataFlowsIn: + DataFlowsOut: errorMessage + DefinitelyAssignedOnEntry: doLogic + DefinitelyAssignedOnExit: doLogic + ReadInside: + ReadOutside: doLogic, errorMessage + WrittenInside: inner, errorMessage + WrittenOutside: doLogic + """, dataFlowAnalysisResults); + } + + [Fact] + public void RegionInIsPattern08() + { + var dataFlowAnalysisResults = CompileAndAnalyzeDataFlowExpression(""" + C.Use(doLogic: true); + + class C + { + public static (string result, (bool, string? errorMessage)) M() => (false, "Something went wrong"); + + public static void Use(bool doLogic) + { + if (doLogic) + { + if (/**/M() is (var outer, (true, var errorMessage) tuple)/**/) + { + Console.Error.WriteLine(errorMessage); + } + } + } + } + + """); + VerifyDataFlowAnalysis(""" + VariablesDeclared: outer, errorMessage, tuple + AlwaysAssigned: + Captured: + CapturedInside: + CapturedOutside: + DataFlowsIn: + DataFlowsOut: errorMessage + DefinitelyAssignedOnEntry: doLogic + DefinitelyAssignedOnExit: doLogic + ReadInside: + ReadOutside: doLogic, errorMessage + WrittenInside: outer, errorMessage, tuple + WrittenOutside: doLogic + """, dataFlowAnalysisResults); + } + + [Fact] + public void RegionInIsPattern09() + { + var dataFlowAnalysisResults = CompileAndAnalyzeDataFlowExpression(""" + C.Use(doLogic: true); + + class C + { + public static (string result, (bool, string? errorMessage)) M() => (false, "Something went wrong"); + + public static void Use(bool doLogic) + { + if (doLogic) + { + if (/**/M() is { result: var outer, Item2: { errorMessage: var errorMessage } tuple })/**/) + { + Console.Error.WriteLine(errorMessage); + } + } + } + } + + """); + VerifyDataFlowAnalysis(""" + VariablesDeclared: outer, errorMessage, tuple + AlwaysAssigned: outer, errorMessage, tuple + Captured: + CapturedInside: + CapturedOutside: + DataFlowsIn: + DataFlowsOut: errorMessage + DefinitelyAssignedOnEntry: doLogic + DefinitelyAssignedOnExit: doLogic, outer, errorMessage, tuple + ReadInside: + ReadOutside: doLogic, errorMessage + WrittenInside: outer, errorMessage, tuple + WrittenOutside: doLogic + """, dataFlowAnalysisResults); + } + + [Fact] + public void RegionWithinSubpattern01() + { + var comp = CreateCompilation(""" + int x = 1; + if (x is var y and 2) { } + """); + + comp.VerifyDiagnostics(); + + var tree = comp.SyntaxTrees[0]; + var model = comp.GetSemanticModel(tree); + var constantPattern = tree.GetRoot().DescendantNodes().OfType().Single(); + Assert.Equal("2", constantPattern.ToString()); + + Assert.Throws(() => model.AnalyzeDataFlow(constantPattern)); + var dataFlow = model.AnalyzeDataFlow(constantPattern.Expression); + VerifyDataFlowAnalysis(""" + VariablesDeclared: + AlwaysAssigned: + Captured: + CapturedInside: + CapturedOutside: + DataFlowsIn: + DataFlowsOut: + DefinitelyAssignedOnEntry: x, y, args + DefinitelyAssignedOnExit: x, y, args + ReadInside: + ReadOutside: x + WrittenInside: + WrittenOutside: x, y, args + """, + dataFlow); + } + + [Fact] + public void RegionWithinSubpattern02() + { + var comp = CreateCompilation(""" + static class Program + { + static void Main() + { + int x = 1; + if (x is var y and x.P and > 2) { } + } + + extension (ref int i) + { + public int P => i; + } + } + """); + + comp.VerifyDiagnostics( + // (6,28): error CS9135: A constant value of type 'int' is expected + // if (x is var y and x.P and > 2) { } + Diagnostic(ErrorCode.ERR_ConstantValueOfTypeExpected, "x.P").WithArguments("int").WithLocation(6, 28)); + + var tree = comp.SyntaxTrees[0]; + var model = comp.GetSemanticModel(tree); + var constantPattern = tree.GetRoot().DescendantNodes().OfType().Single(); + Assert.Equal("x.P", constantPattern.ToString()); + + Assert.Throws(() => model.AnalyzeDataFlow(constantPattern)); + var dataFlow = model.AnalyzeDataFlow(constantPattern.Expression); + VerifyDataFlowAnalysis(""" + VariablesDeclared: + AlwaysAssigned: + Captured: + CapturedInside: + CapturedOutside: + DataFlowsIn: x + DataFlowsOut: + DefinitelyAssignedOnEntry: x, y + DefinitelyAssignedOnExit: x, y + ReadInside: x + ReadOutside: x + WrittenInside: + WrittenOutside: x, y + """, + dataFlow); + } + + [Fact] + public void RegionWithinSubpattern03() + { + var comp = CreateCompilation(""" + bool x = false; + if (x is var y and (x is var z)) { } + """); + + comp.VerifyDiagnostics( + // (2,21): error CS9135: A constant value of type 'bool' is expected + // if (x is var y and (x is var z)) { } + Diagnostic(ErrorCode.ERR_ConstantValueOfTypeExpected, "x is var z").WithArguments("bool").WithLocation(2, 21)); + + var tree = comp.SyntaxTrees[0]; + var model = comp.GetSemanticModel(tree); + var constantPattern = tree.GetRoot().DescendantNodes().OfType().Single(); + Assert.Equal("(x is var z)", constantPattern.ToString()); + + Assert.Throws(() => model.AnalyzeDataFlow(constantPattern)); + var dataFlow = model.AnalyzeDataFlow(constantPattern.Expression); + VerifyDataFlowAnalysis(""" + VariablesDeclared: z + AlwaysAssigned: z + Captured: + CapturedInside: + CapturedOutside: + DataFlowsIn: x + DataFlowsOut: + DefinitelyAssignedOnEntry: x, y, args + DefinitelyAssignedOnExit: x, y, z, args + ReadInside: x + ReadOutside: x + WrittenInside: z + WrittenOutside: x, y, args + """, + dataFlow); + } + + [Fact] + public void RegionITuplePattern() + { + var source = """ + class Program + { + static bool M(object obj) + { + return obj is (object x, string y, int z); + } + } + """; + var compilation = CreateCompilation(source, targetFramework: TargetFramework.NetCoreApp); + compilation.VerifyEmitDiagnostics(); + + var tree = compilation.SyntaxTrees[0]; + var model = compilation.GetSemanticModel(tree); + var node = tree.GetRoot().DescendantNodes().OfType().Single(); + Assert.Equal("obj is (object x, string y, int z)", node.ToString()); + VerifyDataFlowAnalysis(""" + VariablesDeclared: x, y, z + AlwaysAssigned: + Captured: + CapturedInside: + CapturedOutside: + DataFlowsIn: obj + DataFlowsOut: + DefinitelyAssignedOnEntry: obj + DefinitelyAssignedOnExit: obj + ReadInside: obj + ReadOutside: + WrittenInside: x, y, z + WrittenOutside: obj + """, + model.AnalyzeDataFlow(node)); + } + + [Fact] + public void RegionListPattern() + { + var source = """ + class Program + { + static bool M(object[] arr) + { + return arr is [object x, string y, int z] arr1; + } + } + """; + var compilation = CreateCompilation(source, targetFramework: TargetFramework.NetCoreApp); + compilation.VerifyEmitDiagnostics(); + + var tree = compilation.SyntaxTrees[0]; + var model = compilation.GetSemanticModel(tree); + var node = tree.GetRoot().DescendantNodes().OfType().Single(); + Assert.Equal("arr is [object x, string y, int z] arr1", node.ToString()); + VerifyDataFlowAnalysis(""" + VariablesDeclared: x, y, z, arr1 + AlwaysAssigned: + Captured: + CapturedInside: + CapturedOutside: + DataFlowsIn: arr + DataFlowsOut: + DefinitelyAssignedOnEntry: arr + DefinitelyAssignedOnExit: arr + ReadInside: arr + ReadOutside: + WrittenInside: x, y, z, arr1 + WrittenOutside: arr + """, + model.AnalyzeDataFlow(node)); + } + + [Fact] + public void RegionSlicePattern() + { + var source = """ + class Program + { + static bool M(object[] arr) + { + return arr is [object x, ..object[] y]; + } + } + """; + var compilation = CreateCompilation(source, targetFramework: TargetFramework.NetCoreApp); + compilation.VerifyEmitDiagnostics(); + + var tree = compilation.SyntaxTrees[0]; + var model = compilation.GetSemanticModel(tree); + var node = tree.GetRoot().DescendantNodes().OfType().Single(); + Assert.Equal("arr is [object x, ..object[] y]", node.ToString()); + VerifyDataFlowAnalysis(""" + VariablesDeclared: x, y + AlwaysAssigned: + Captured: + CapturedInside: + CapturedOutside: + DataFlowsIn: arr + DataFlowsOut: + DefinitelyAssignedOnEntry: arr + DefinitelyAssignedOnExit: arr + ReadInside: arr + ReadOutside: + WrittenInside: x, y + WrittenOutside: arr + """, + model.AnalyzeDataFlow(node)); + } + + [Fact] + public void RegionNotPattern() + { + var source = """ + class Program + { + static void M(object obj) + { + if (obj is not string s) + { + } + } + } + """; + var compilation = CreateCompilation(source, targetFramework: TargetFramework.NetCoreApp); + compilation.VerifyEmitDiagnostics(); + + var tree = compilation.SyntaxTrees[0]; + var model = compilation.GetSemanticModel(tree); + var node = tree.GetRoot().DescendantNodes().OfType().Single(); + Assert.Equal("obj is not string s", node.ToString()); + VerifyDataFlowAnalysis(""" + VariablesDeclared: s + AlwaysAssigned: + Captured: + CapturedInside: + CapturedOutside: + DataFlowsIn: obj + DataFlowsOut: + DefinitelyAssignedOnEntry: obj + DefinitelyAssignedOnExit: obj + ReadInside: obj + ReadOutside: + WrittenInside: s + WrittenOutside: obj + """, + model.AnalyzeDataFlow(node)); + } + + [Fact] + public void RegionOrPattern() + { + var source = """ + class Program + { + static void M(object obj) + { + if (obj is string s or int i) + { + } + } + } + """; + var compilation = CreateCompilation(source, targetFramework: TargetFramework.NetCoreApp); + compilation.VerifyEmitDiagnostics( + // (5,27): error CS8780: A variable may not be declared within a 'not' or 'or' pattern. + // if (obj is string s or int i) + Diagnostic(ErrorCode.ERR_DesignatorBeneathPatternCombinator, "s").WithLocation(5, 27), + // (5,36): error CS8780: A variable may not be declared within a 'not' or 'or' pattern. + // if (obj is string s or int i) + Diagnostic(ErrorCode.ERR_DesignatorBeneathPatternCombinator, "i").WithLocation(5, 36)); + + var tree = compilation.SyntaxTrees[0]; + var model = compilation.GetSemanticModel(tree); + var node = tree.GetRoot().DescendantNodes().OfType().Single(); + Assert.Equal("obj is string s or int i", node.ToString()); + VerifyDataFlowAnalysis(""" + VariablesDeclared: s, i + AlwaysAssigned: + Captured: + CapturedInside: + CapturedOutside: + DataFlowsIn: obj + DataFlowsOut: + DefinitelyAssignedOnEntry: obj + DefinitelyAssignedOnExit: obj + ReadInside: obj + ReadOutside: + WrittenInside: + WrittenOutside: obj + """, + model.AnalyzeDataFlow(node)); + } + + [Fact] + public void RegionSplitState() + { + var source = """ + class Program + { + static void M(object obj) + { + if (obj is var s) + { + throw null!; + } + + return; + } + } + """; + var compilation = CreateCompilation(source, targetFramework: TargetFramework.NetCoreApp); + compilation.VerifyEmitDiagnostics(); + + var tree = compilation.SyntaxTrees[0]; + var model = compilation.GetSemanticModel(tree); + var node = tree.GetRoot().DescendantNodes().OfType().Single(); + Assert.Equal("obj is var s", node.ToString()); + VerifyDataFlowAnalysis(""" + VariablesDeclared: s + AlwaysAssigned: s + Captured: + CapturedInside: + CapturedOutside: + DataFlowsIn: obj + DataFlowsOut: + DefinitelyAssignedOnEntry: obj + DefinitelyAssignedOnExit: obj, s + ReadInside: obj + ReadOutside: + WrittenInside: s + WrittenOutside: obj + """, + model.AnalyzeDataFlow(node)); + } } } diff --git a/src/EditorFeatures/CSharpTest/ExtractMethod/ExtractMethodTests.cs b/src/EditorFeatures/CSharpTest/ExtractMethod/ExtractMethodTests.cs index 28d8ed1c1a6aa..36ac6fce88baa 100644 --- a/src/EditorFeatures/CSharpTest/ExtractMethod/ExtractMethodTests.cs +++ b/src/EditorFeatures/CSharpTest/ExtractMethod/ExtractMethodTests.cs @@ -11369,4 +11369,84 @@ private static void NewMethod(Spec.ListDictionary value) } } """"); + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/70024")] + public Task TestRecursivePatternVariable1() + => TestExtractMethodAsync("""" + using System; + + C.Use(doLogic: true); + + class C + { + public static (bool, string? errorMessage) M() => (false, "Something went wrong"); + + public static void Use(bool doLogic) + { + if (doLogic) + { + [|if (M() is (false, var errorMessage)) + { + Console.Error.WriteLine(errorMessage); + }|] + } + } + } + """", """" + using System; + + C.Use(doLogic: true); + + class C + { + public static (bool, string? errorMessage) M() => (false, "Something went wrong"); + + public static void Use(bool doLogic) + { + if (doLogic) + { + NewMethod(); + } + } + + private static void NewMethod() + { + if (M() is (false, var errorMessage)) + { + Console.Error.WriteLine(errorMessage); + } + } + } + """"); + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/70024")] + public Task TestRecursivePatternVariable2() + => TestExtractMethodAsync("""" + static void PatternMatchingUsage() + { + var t = (1, 2); + [|switch (t) + { + case (var x, 2): + Console.WriteLine(x); + break; + }|] + } + """", """" + static void PatternMatchingUsage() + { + var t = (1, 2); + NewMethod(t); + } + + static void NewMethod((int, int) t) + { + switch (t) + { + case (var x, 2): + Console.WriteLine(x); + break; + } + } + """"); }