diff --git a/analyzers/src/SonarAnalyzer.CFG/CfgSerializer/CfgSerializer.RoslynCfgWalker.cs b/analyzers/src/SonarAnalyzer.CFG/CfgSerializer/CfgSerializer.RoslynCfgWalker.cs index 11e77296e8c..1773699cf2b 100644 --- a/analyzers/src/SonarAnalyzer.CFG/CfgSerializer/CfgSerializer.RoslynCfgWalker.cs +++ b/analyzers/src/SonarAnalyzer.CFG/CfgSerializer/CfgSerializer.RoslynCfgWalker.cs @@ -27,8 +27,8 @@ public static partial class CfgSerializer { private class RoslynCfgWalker { - private readonly DotWriter writer; - private readonly HashSet visited = new(); + protected readonly DotWriter writer; + private readonly HashSet visited = []; private readonly RoslynCfgIdProvider cfgIdProvider; private readonly int cfgId; @@ -46,6 +46,27 @@ public void Visit(ControlFlowGraph cfg, string title) writer.WriteGraphEnd(); } + protected virtual void WriteEdges(BasicBlock block) + { + foreach (var predecessor in block.Predecessors) + { + var condition = string.Empty; + if (predecessor.Source.ConditionKind != ControlFlowConditionKind.None) + { + condition = predecessor == predecessor.Source.ConditionalSuccessor ? predecessor.Source.ConditionKind.ToString() : "Else"; + } + var semantics = predecessor.Semantics == ControlFlowBranchSemantics.Regular ? null : predecessor.Semantics.ToString(); + writer.WriteEdge(BlockId(predecessor.Source), BlockId(block), $"{semantics} {condition}".Trim()); + } + if (block.FallThroughSuccessor is { Destination: null }) + { + writer.WriteEdge(BlockId(block), "NoDestination_" + BlockId(block), block.FallThroughSuccessor.Semantics.ToString()); + } + } + + protected string BlockId(BasicBlock block) => + $"cfg{cfgId}_block{block.Ordinal}"; + private void VisitSubGraph(ControlFlowGraph cfg, string title) { writer.WriteSubGraphStart(cfgIdProvider.Next(), title); @@ -141,27 +162,6 @@ private static string SerializeRegion(ControlFlowRegion region) return sb.ToString(); } - private void WriteEdges(BasicBlock block) - { - foreach (var predecessor in block.Predecessors) - { - var condition = string.Empty; - if (predecessor.Source.ConditionKind != ControlFlowConditionKind.None) - { - condition = predecessor == predecessor.Source.ConditionalSuccessor ? predecessor.Source.ConditionKind.ToString() : "Else"; - } - var semantics = predecessor.Semantics == ControlFlowBranchSemantics.Regular ? null : predecessor.Semantics.ToString(); - writer.WriteEdge(BlockId(predecessor.Source), BlockId(block), $"{semantics} {condition}".Trim()); - } - if (block.FallThroughSuccessor is { Destination: null }) - { - writer.WriteEdge(BlockId(block), "NoDestination_" + BlockId(block), block.FallThroughSuccessor.Semantics.ToString()); - } - } - - private string BlockId(BasicBlock block) => - $"cfg{cfgId}_block{block.Ordinal}"; - private static IEnumerable AnonymousFunctions(ControlFlowGraph cfg) => cfg.Blocks .SelectMany(x => x.Operations) diff --git a/analyzers/src/SonarAnalyzer.CFG/CfgSerializer/CfgSerializer.RoslynLvaWalker.cs b/analyzers/src/SonarAnalyzer.CFG/CfgSerializer/CfgSerializer.RoslynLvaWalker.cs new file mode 100644 index 00000000000..6c28bdf562d --- /dev/null +++ b/analyzers/src/SonarAnalyzer.CFG/CfgSerializer/CfgSerializer.RoslynLvaWalker.cs @@ -0,0 +1,49 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2024 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarAnalyzer.CFG.LiveVariableAnalysis; +using SonarAnalyzer.CFG.Roslyn; + +namespace SonarAnalyzer.CFG; + +public static partial class CfgSerializer +{ + private sealed class RoslynLvaWalker : RoslynCfgWalker + { + private readonly RoslynLiveVariableAnalysis lva; + + public RoslynLvaWalker(RoslynLiveVariableAnalysis lva, DotWriter writer, RoslynCfgIdProvider cfgIdProvider) : base(writer, cfgIdProvider) + { + this.lva = lva; + } + + protected override void WriteEdges(BasicBlock block) + { + foreach (var predecessor in lva.BlockPredecessors[block.Ordinal]) + { + writer.WriteEdge(BlockId(predecessor), BlockId(block), string.Empty); + } + if (block.FallThroughSuccessor is { Destination: null }) + { + writer.WriteEdge(BlockId(block), "NoDestination_" + BlockId(block), block.FallThroughSuccessor.Semantics.ToString()); + } + } + } +} diff --git a/analyzers/src/SonarAnalyzer.CFG/CfgSerializer/CfgSerializer.cs b/analyzers/src/SonarAnalyzer.CFG/CfgSerializer/CfgSerializer.cs index c53401b5fde..ec99d817ef4 100644 --- a/analyzers/src/SonarAnalyzer.CFG/CfgSerializer/CfgSerializer.cs +++ b/analyzers/src/SonarAnalyzer.CFG/CfgSerializer/CfgSerializer.cs @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using SonarAnalyzer.CFG.LiveVariableAnalysis; using SonarAnalyzer.CFG.Roslyn; using SonarAnalyzer.CFG.Sonar; @@ -38,4 +39,11 @@ public static string Serialize(ControlFlowGraph cfg, string title = "RoslynCfg") new RoslynCfgWalker(writer, new RoslynCfgIdProvider()).Visit(cfg, title); return writer.ToString(); } + + public static string Serialize(RoslynLiveVariableAnalysis lva, string title = "RoslynCfg") + { + var writer = new DotWriter(); + new RoslynLvaWalker(lva, writer, new RoslynCfgIdProvider()).Visit(lva.Cfg, title); + return writer.ToString(); + } } diff --git a/analyzers/src/SonarAnalyzer.CFG/LiveVariableAnalysis/RoslynLiveVariableAnalysis.cs b/analyzers/src/SonarAnalyzer.CFG/LiveVariableAnalysis/RoslynLiveVariableAnalysis.cs index 9eeaa5e3789..9ac3b2f4623 100644 --- a/analyzers/src/SonarAnalyzer.CFG/LiveVariableAnalysis/RoslynLiveVariableAnalysis.cs +++ b/analyzers/src/SonarAnalyzer.CFG/LiveVariableAnalysis/RoslynLiveVariableAnalysis.cs @@ -28,6 +28,8 @@ public sealed class RoslynLiveVariableAnalysis : LiveVariableAnalysisBase> blockPredecessors = []; private readonly Dictionary> blockSuccessors = []; + internal ImmutableDictionary> BlockPredecessors => blockPredecessors.ToImmutableDictionary(); + protected override BasicBlock ExitBlock => Cfg.ExitBlock; public RoslynLiveVariableAnalysis(ControlFlowGraph cfg, CancellationToken cancel) diff --git a/analyzers/tests/SonarAnalyzer.Test/CFG/Roslyn/RoslynLvaSerializerTest.cs b/analyzers/tests/SonarAnalyzer.Test/CFG/Roslyn/RoslynLvaSerializerTest.cs new file mode 100644 index 00000000000..dce240422a4 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.Test/CFG/Roslyn/RoslynLvaSerializerTest.cs @@ -0,0 +1,176 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2024 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarAnalyzer.CFG; +using SonarAnalyzer.CFG.LiveVariableAnalysis; + +namespace SonarAnalyzer.Test.CFG.Roslyn; + +[TestClass] +public class RoslynLvaSerializerTest +{ + [TestMethod] + public void Serialize_TryCatchFinally() + { + const string code = """ + class Sample + { + void Method() + { + var value = 0; + try + { + Use(0); + value = 42; + } + catch + { + Use(value); + value = 1; + } + finally + { + Use(value); + } + } + + void Use(int v) {} + } + """; + var dot = CfgSerializer.Serialize(CreateLva(code)); + dot.Should().BeIgnoringLineEndingsAndEmptyLines(""" + digraph "RoslynCfg" { + subgraph "cluster_1" { + label = "LocalLifetime region, Locals: value" + subgraph "cluster_2" { + label = "TryAndFinally region" + subgraph "cluster_3" { + label = "Try region" + subgraph "cluster_4" { + label = "TryAndCatch region" + subgraph "cluster_5" { + label = "Try region" + cfg0_block2 [shape=record label="{BLOCK #2|0#: ExpressionStatementOperation: Use(0);|1#: 0#.Operation: InvocationOperation: Use: Use(0)|2#: 1#.Instance: InstanceReferenceOperation: Use|2#: ArgumentOperation: 0|3#: 2#.Value: LiteralOperation: 0|##########|0#: ExpressionStatementOperation: value = 42;|1#: 0#.Operation: SimpleAssignmentOperation: value = 42|2#: 1#.Target: LocalReferenceOperation: value|2#: 1#.Value: LiteralOperation: 42|##########}"] + } + subgraph "cluster_6" { + label = "Catch region: object" + cfg0_block3 [shape=record label="{BLOCK #3|0#: ExpressionStatementOperation: Use(value);|1#: 0#.Operation: InvocationOperation: Use: Use(value)|2#: 1#.Instance: InstanceReferenceOperation: Use|2#: ArgumentOperation: value|3#: 2#.Value: LocalReferenceOperation: value|##########|0#: ExpressionStatementOperation: value = 1;|1#: 0#.Operation: SimpleAssignmentOperation: value = 1|2#: 1#.Target: LocalReferenceOperation: value|2#: 1#.Value: LiteralOperation: 1|##########}"] + } + } + } + subgraph "cluster_7" { + label = "Finally region" + cfg0_block4 [shape=record label="{BLOCK #4|0#: ExpressionStatementOperation: Use(value);|1#: 0#.Operation: InvocationOperation: Use: Use(value)|2#: 1#.Instance: InstanceReferenceOperation: Use|2#: ArgumentOperation: value|3#: 2#.Value: LocalReferenceOperation: value|##########}"] + } + } + cfg0_block1 [shape=record label="{BLOCK #1|0#: SimpleAssignmentOperation: value = 0|1#: 0#.Target: LocalReferenceOperation: value = 0|1#: 0#.Value: LiteralOperation: 0|##########}"] + } + cfg0_block0 [shape=record label="{ENTRY #0}"] + cfg0_block5 [shape=record label="{EXIT #5}"] + cfg0_block1 -> cfg0_block2 + cfg0_block2 -> cfg0_block3 + cfg0_block1 -> cfg0_block3 + cfg0_block2 -> cfg0_block4 + cfg0_block3 -> cfg0_block4 + cfg0_block4 -> NoDestination_cfg0_block4 [label="StructuredExceptionHandling"] + cfg0_block0 -> cfg0_block1 + cfg0_block4 -> cfg0_block5 + cfg0_block4 -> cfg0_block5 + } + """); + } + + [TestMethod] + public void Serialize_TryCatchFinallyRethrow() + { + const string code = """ + class Sample + { + void Method() + { + var value = 0; + try + { + Use(0); + value = 42; + } + catch + { + Use(value); + value = 1; + throw; + } + finally + { + Use(value); + } + } + + void Use(int v) {} + } + """; + var dot = CfgSerializer.Serialize(CreateLva(code)); + dot.Should().BeIgnoringLineEndingsAndEmptyLines(""" + digraph "RoslynCfg" { + subgraph "cluster_1" { + label = "LocalLifetime region, Locals: value" + subgraph "cluster_2" { + label = "TryAndFinally region" + subgraph "cluster_3" { + label = "Try region" + subgraph "cluster_4" { + label = "TryAndCatch region" + subgraph "cluster_5" { + label = "Try region" + cfg0_block2 [shape=record label="{BLOCK #2|0#: ExpressionStatementOperation: Use(0);|1#: 0#.Operation: InvocationOperation: Use: Use(0)|2#: 1#.Instance: InstanceReferenceOperation: Use|2#: ArgumentOperation: 0|3#: 2#.Value: LiteralOperation: 0|##########|0#: ExpressionStatementOperation: value = 42;|1#: 0#.Operation: SimpleAssignmentOperation: value = 42|2#: 1#.Target: LocalReferenceOperation: value|2#: 1#.Value: LiteralOperation: 42|##########}"] + } + subgraph "cluster_6" { + label = "Catch region: object" + cfg0_block3 [shape=record label="{BLOCK #3|0#: ExpressionStatementOperation: Use(value);|1#: 0#.Operation: InvocationOperation: Use: Use(value)|2#: 1#.Instance: InstanceReferenceOperation: Use|2#: ArgumentOperation: value|3#: 2#.Value: LocalReferenceOperation: value|##########|0#: ExpressionStatementOperation: value = 1;|1#: 0#.Operation: SimpleAssignmentOperation: value = 1|2#: 1#.Target: LocalReferenceOperation: value|2#: 1#.Value: LiteralOperation: 1|##########}"] + } + } + } + subgraph "cluster_7" { + label = "Finally region" + cfg0_block4 [shape=record label="{BLOCK #4|0#: ExpressionStatementOperation: Use(value);|1#: 0#.Operation: InvocationOperation: Use: Use(value)|2#: 1#.Instance: InstanceReferenceOperation: Use|2#: ArgumentOperation: value|3#: 2#.Value: LocalReferenceOperation: value|##########}"] + } + } + cfg0_block1 [shape=record label="{BLOCK #1|0#: SimpleAssignmentOperation: value = 0|1#: 0#.Target: LocalReferenceOperation: value = 0|1#: 0#.Value: LiteralOperation: 0|##########}"] + } + cfg0_block0 [shape=record label="{ENTRY #0}"] + cfg0_block5 [shape=record label="{EXIT #5}"] + cfg0_block1 -> cfg0_block2 + cfg0_block2 -> cfg0_block3 + cfg0_block1 -> cfg0_block3 + cfg0_block3 -> NoDestination_cfg0_block3 [label="Rethrow"] + cfg0_block2 -> cfg0_block4 + cfg0_block4 -> NoDestination_cfg0_block4 [label="StructuredExceptionHandling"] + cfg0_block0 -> cfg0_block1 + cfg0_block4 -> cfg0_block5 + } + """); + } + + private static RoslynLiveVariableAnalysis CreateLva(string code) + { + var cfg = TestHelper.CompileCfgCS(code); + return new RoslynLiveVariableAnalysis(cfg, CancellationToken.None); + } +} diff --git a/analyzers/tests/SonarAnalyzer.Test/LiveVariableAnalysis/RoslynLiveVariableAnalysisTest.cs b/analyzers/tests/SonarAnalyzer.Test/LiveVariableAnalysis/RoslynLiveVariableAnalysisTest.cs index d0b2b22c4f6..275f44bcc60 100644 --- a/analyzers/tests/SonarAnalyzer.Test/LiveVariableAnalysis/RoslynLiveVariableAnalysisTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/LiveVariableAnalysis/RoslynLiveVariableAnalysisTest.cs @@ -21,6 +21,7 @@ extern alias csharp; using csharp::SonarAnalyzer.Extensions; using Microsoft.CodeAnalysis.CSharp; +using SonarAnalyzer.CFG; using SonarAnalyzer.CFG.LiveVariableAnalysis; using SonarAnalyzer.CFG.Roslyn; @@ -837,7 +838,7 @@ public void NestedImplicitFinally_Lock_ForEach_LiveIn() var context = CreateContextCS(code, null, "string[] args"); context.Validate("Method(0);", LiveIn("args", null), LiveOut("args", "value", null)); // The null-named symbol is implicit `bool LockTaken` from the lock(args) statement context.Validate("Method(1);", LiveIn("value", null), LiveOut("value", null)); - context.Validate("Method(value);", LiveIn(null, "value"), LiveOut(new string[] { null })); + context.Validate("Method(value);", LiveIn(null, "value"), LiveOut([null])); context.Validate("Method(2);"); context.ValidateExit(); } @@ -1080,6 +1081,10 @@ public Context(string code, AnalyzerLanguage language, string localFunctionName { Cfg = TestHelper.CompileCfg(code, language, code.Contains("// Error CS"), localFunctionName); Lva = new RoslynLiveVariableAnalysis(Cfg, default); + const string Separator = "----------"; + Console.WriteLine(Separator); + Console.WriteLine(CfgSerializer.Serialize(Lva)); + Console.WriteLine(Separator); } public Context(string code, SyntaxKind syntaxKind)