Skip to content

Commit

Permalink
Add CfgSerializer.RoslynLvaWalker
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastien-marichal committed Jul 17, 2024
1 parent 1d18a67 commit 27a7634
Show file tree
Hide file tree
Showing 6 changed files with 264 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ public static partial class CfgSerializer
{
private class RoslynCfgWalker
{
private readonly DotWriter writer;
private readonly HashSet<BasicBlock> visited = new();
protected readonly DotWriter writer;
private readonly HashSet<BasicBlock> visited = [];
private readonly RoslynCfgIdProvider cfgIdProvider;
private readonly int cfgId;

Expand All @@ -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);
Expand Down Expand Up @@ -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<IFlowAnonymousFunctionOperationWrapper> AnonymousFunctions(ControlFlowGraph cfg) =>
cfg.Blocks
.SelectMany(x => x.Operations)
Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ public sealed class RoslynLiveVariableAnalysis : LiveVariableAnalysisBase<Contro
private readonly Dictionary<int, List<BasicBlock>> blockPredecessors = [];
private readonly Dictionary<int, List<BasicBlock>> blockSuccessors = [];

public ImmutableDictionary<int, List<BasicBlock>> BlockPredecessors => blockPredecessors.ToImmutableDictionary();

protected override BasicBlock ExitBlock => Cfg.ExitBlock;

public RoslynLiveVariableAnalysis(ControlFlowGraph cfg, CancellationToken cancel)
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 27a7634

Please sign in to comment.