Skip to content

Commit cfee18a

Browse files
Fix S2583/S2589 FP: Tuple deconstruction assignment (#8461)
1 parent 30fc307 commit cfee18a

17 files changed

+304
-40
lines changed

analyzers/src/SonarAnalyzer.Common/SymbolicExecution/Roslyn/Extensions/IOperationExtensions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ OperationKindEx.PropertyReference when operation.ToPropertyReference() is { Prop
5151
internal static IConversionOperationWrapper? AsConversion(this IOperation operation) =>
5252
operation.As(OperationKindEx.Conversion, IConversionOperationWrapper.FromOperation);
5353

54+
internal static IDeclarationExpressionOperationWrapper? AsDeclarationExpression(this IOperation operation) =>
55+
operation.As(OperationKindEx.DeclarationExpression, IDeclarationExpressionOperationWrapper.FromOperation);
56+
5457
internal static IDeclarationPatternOperationWrapper? AsDeclarationPattern(this IOperation operation) =>
5558
operation.As(OperationKindEx.DeclarationPattern, IDeclarationPatternOperationWrapper.FromOperation);
5659

@@ -69,6 +72,9 @@ OperationKindEx.PropertyReference when operation.ToPropertyReference() is { Prop
6972
internal static IPropertyReferenceOperationWrapper? AsPropertyReference(this IOperation operation) =>
7073
operation.As(OperationKindEx.PropertyReference, IPropertyReferenceOperationWrapper.FromOperation);
7174

75+
internal static ITupleOperationWrapper? AsTuple(this IOperation operation) =>
76+
operation.As(OperationKindEx.Tuple, ITupleOperationWrapper.FromOperation);
77+
7278
internal static IAwaitOperationWrapper ToAwait(this IOperation operation) =>
7379
IAwaitOperationWrapper.FromOperation(operation);
7480

@@ -123,6 +129,9 @@ internal static IParameterReferenceOperationWrapper ToParameterReference(this IO
123129
internal static IEventReferenceOperationWrapper ToEventReference(this IOperation operation) =>
124130
IEventReferenceOperationWrapper.FromOperation(operation);
125131

132+
internal static ITupleOperationWrapper ToTuple(this IOperation operation) =>
133+
ITupleOperationWrapper.FromOperation(operation);
134+
126135
internal static IUnaryOperationWrapper ToUnary(this IOperation operation) =>
127136
IUnaryOperationWrapper.FromOperation(operation);
128137

analyzers/src/SonarAnalyzer.Common/SymbolicExecution/Roslyn/OperationDispatcher.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ internal static class OperationDispatcher
4545
{ OperationKindEx.CompoundAssignment, new CompoundAssignment() },
4646
{ OperationKindEx.Conversion, new Conversion() },
4747
{ OperationKindEx.DeclarationPattern, new DeclarationPattern() },
48+
{ OperationKindEx.DeconstructionAssignment, new DeconstructionAssignment() },
4849
{ OperationKindEx.Decrement, new IncrementOrDecrement() },
4950
{ OperationKindEx.DefaultValue, new DefaultValue() },
5051
{ OperationKindEx.DelegateCreation, new NotNullOperation() },
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* SonarAnalyzer for .NET
3+
* Copyright (C) 2015-2023 SonarSource SA
4+
* mailto: contact AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
21+
namespace SonarAnalyzer.SymbolicExecution.Roslyn.OperationProcessors;
22+
23+
internal sealed class DeconstructionAssignment : SimpleProcessor<IDeconstructionAssignmentOperationWrapper>
24+
{
25+
protected override IDeconstructionAssignmentOperationWrapper Convert(IOperation operation) =>
26+
IDeconstructionAssignmentOperationWrapper.FromOperation(operation);
27+
28+
protected override ProgramState Process(SymbolicContext context, IDeconstructionAssignmentOperationWrapper assignment)
29+
{
30+
var operationValues = TupleElementValues(context.State, assignment.Target, assignment.Value);
31+
var newState = context.State;
32+
foreach (var (tupleMember, value) in operationValues)
33+
{
34+
newState = newState.SetOperationAndSymbolValue(tupleMember, value);
35+
}
36+
return newState;
37+
}
38+
39+
private static IEnumerable<OperationValue> TupleElementValues(ProgramState state, IOperation target, IOperation value)
40+
{
41+
var operationValues = new List<OperationValue>();
42+
var leftTupleElements = TupleElements(Unwrap(target, state).ToTuple(), state);
43+
// If the right side is a tuple, then every symbol/constraint is copied to the left side.
44+
if (Unwrap(value, state).AsTuple() is { } rightSideTuple)
45+
{
46+
var rightTupleElements = TupleElements(rightSideTuple, state);
47+
for (var i = 0; i < leftTupleElements.Length; i++)
48+
{
49+
var leftTupleMember = leftTupleElements[i];
50+
var rightTupleMember = rightTupleElements[i];
51+
if (leftTupleMember.Kind == OperationKindEx.Discard)
52+
{
53+
continue;
54+
}
55+
else if (leftTupleMember.AsTuple() is { } nestedTuple)
56+
{
57+
operationValues.AddRange(TupleElementValues(state, nestedTuple.WrappedOperation, rightTupleMember));
58+
}
59+
else
60+
{
61+
operationValues.Add(new(leftTupleMember, state[rightTupleMember]));
62+
}
63+
}
64+
}
65+
// If the right side is not a tuple, then every member of the left side tuple is set to empty.
66+
else
67+
{
68+
operationValues.AddRange(leftTupleElements
69+
.Where(x => x.Kind != OperationKindEx.Discard)
70+
.Select(x => new OperationValue(x, SymbolicValue.Empty)));
71+
}
72+
return operationValues;
73+
}
74+
75+
private static IOperation[] TupleElements(ITupleOperationWrapper operation, ProgramState state) =>
76+
operation.Elements.Select(x => Unwrap(x, state)).ToArray();
77+
78+
private static IOperation Unwrap(IOperation operation, ProgramState state)
79+
{
80+
var unwrapped = state.ResolveCaptureAndUnwrapConversion(operation);
81+
return unwrapped.AsDeclarationExpression()?.Expression ?? unwrapped;
82+
}
83+
84+
private sealed record OperationValue(IOperation Operation, SymbolicValue Value);
85+
}

analyzers/src/SonarAnalyzer.Common/SymbolicExecution/Roslyn/ProgramState.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,16 @@ protected ProgramState(ProgramState original) // Custom record override constr
6363
Exceptions = original.Exceptions;
6464
}
6565

66+
public ProgramState SetOperationAndSymbolValue(IOperation operation, SymbolicValue value)
67+
{
68+
var newState = SetOperationValue(operation, value);
69+
if (operation.TrackedSymbol(this) is { } symbol)
70+
{
71+
newState = newState.SetSymbolValue(symbol, value);
72+
}
73+
return newState;
74+
}
75+
6676
public ProgramState SetOperationValue(IOperationWrapper operation, SymbolicValue value) =>
6777
operation is null
6878
? throw new ArgumentNullException(nameof(operation))
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* SonarAnalyzer for .NET
3+
* Copyright (C) 2015-2023 SonarSource SA
4+
* mailto: contact AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
21+
using SonarAnalyzer.SymbolicExecution.Roslyn;
22+
using SonarAnalyzer.UnitTest.TestFramework.SymbolicExecution;
23+
24+
namespace SonarAnalyzer.UnitTest.SymbolicExecution.Roslyn;
25+
26+
public partial class ProgramStateTest
27+
{
28+
[TestMethod]
29+
public void SetOperationAndSymbolValue_TrackedSymbol()
30+
{
31+
var operations = TestHelper.CompileCfgBodyCS("bool b = true;").Blocks[1].Operations;
32+
var localReference = operations[0].ChildOperations.First().ToLocalReference();
33+
var symbol = localReference.Local;
34+
var sut = ProgramState.Empty;
35+
36+
sut[localReference].Should().BeNull();
37+
sut[symbol].Should().BeNull();
38+
39+
sut = sut.SetOperationAndSymbolValue(localReference.WrappedOperation, SymbolicValue.True);
40+
41+
sut[localReference].Should().Be(SymbolicValue.True);
42+
sut[symbol].Should().Be(SymbolicValue.True);
43+
}
44+
45+
[TestMethod]
46+
public void SetOperationAndSymbolValue_NotTrackedSymbol()
47+
{
48+
var operations = TestHelper.CompileCfgBodyCS("""var texts = new string[] { }; texts[42] = string.Empty;""").Blocks[1].Operations;
49+
var arrayElementReference = operations[1].ChildOperations.First().ChildOperations.First().ToArrayElementReference();
50+
var sut = ProgramState.Empty;
51+
52+
sut[arrayElementReference].Should().BeNull();
53+
54+
sut = sut.SetOperationAndSymbolValue(arrayElementReference.WrappedOperation, SymbolicValue.NotNull);
55+
56+
sut[arrayElementReference].Should().Be(SymbolicValue.NotNull);
57+
}
58+
}

analyzers/tests/SonarAnalyzer.UnitTest/TestCases/SymbolicExecution/Roslyn/ConditionEvaluatesToConstant.CSharp10.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,35 @@ public static void DoSomething()
2626
else if (myStruct.y) { } // FN
2727
}
2828
}
29+
30+
// https://github.com/SonarSource/sonar-dotnet/issues/7057
31+
public class Repro_7057
32+
{
33+
private (string, int) SomeTuple() => ("hello", 1);
34+
private string SomeString() => "hello";
35+
36+
public void WithTuple()
37+
{
38+
string text1 = null;
39+
(text1, var (text2, _)) = (SomeString(), SomeTuple());
40+
if (text1 == null) // Compliant
41+
{
42+
Console.WriteLine();
43+
}
44+
if (text2 == null) // Compliant
45+
{
46+
Console.WriteLine();
47+
}
48+
49+
string text3 = null;
50+
((text3, _), var (text4, _)) = ((null, 42), ("hello", 42));
51+
if (text3 == null) // Noncompliant
52+
{
53+
Console.WriteLine();
54+
}
55+
if (text4 == null) // Noncompliant
56+
{
57+
Console.WriteLine(); // Secondary
58+
}
59+
}
60+
}

analyzers/tests/SonarAnalyzer.UnitTest/TestCases/SymbolicExecution/Roslyn/ConditionEvaluatesToConstant.CSharp7.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ public void GoGoGo()
294294
{
295295
var tmp = 0;
296296
var flag = true;
297-
while (flag) // Noncompliant
297+
while (flag) // Compliant
298298
{
299299
(flag, tmp) = (false, 5);
300300
}
@@ -329,7 +329,7 @@ public void MutedNull()
329329
{
330330
var tmp = 0;
331331
var flag = "x";
332-
while (flag != null) // Noncompliant
332+
while (flag != null) // Compliant
333333
{
334334
(flag, tmp) = (null, 5);
335335
}

analyzers/tests/SonarAnalyzer.UnitTest/TestCases/SymbolicExecution/Roslyn/ConditionEvaluatesToConstant.CSharp8.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,8 @@ public void ParenthesizedVariableDesignation_Nested(object arg)
133133
public void NestedDeconstructionAssignment()
134134
{
135135
var (a, (b, _)) = (true, (true, true));
136-
if (a) { } // FN
137-
if (b) { } // FN
136+
if (a) { } // Noncompliant
137+
if (b) { } // Noncompliant
138138
}
139139

140140
int UsingDeclaration_Null()
@@ -330,7 +330,7 @@ public void AssignmentTarget(bool arg)
330330
{
331331
}
332332

333-
if (b) // FN
333+
if (b) // Noncompliant
334334
{
335335
}
336336
}

analyzers/tests/SonarAnalyzer.UnitTest/TestCases/SymbolicExecution/Roslyn/ConditionEvaluatesToConstant.CSharp9.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,11 @@ public object InitWithTupleAssignment
121121
{
122122
var tmp = 0;
123123
var flag = true;
124-
while (flag) // Noncompliant
124+
while (flag) // Compliant
125125
{
126126
(flag, tmp) = (false, 5);
127127
}
128-
o = value; // Secondary
128+
o = value;
129129
}
130130
}
131131
}

0 commit comments

Comments
 (0)