Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
<ItemGroup>
<PackageVersion Include="coverlet.collector" Version="8.0.0"/>
<PackageVersion Include="FluentAssertions" Version="8.8.0"/>
<PackageVersion Include="Microsoft.Build" Version="18.3.3"/>
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0"/>
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.103"/>
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="5.0.0"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.3"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2"/>
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3"/>
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.3"/>
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.3"/>
<PackageVersion Include="Microsoft.Build" Version="18.4.0"/>
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0"/>
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.200"/>
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="5.3.0"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.4"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.4"/>
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.4"/>
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.4"/>
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.4"/>
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.3.0"/>
<PackageVersion Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15"/>
<PackageVersion Include="NSubstitute" Version="5.3.0"/>
Expand Down
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "10.0.103",
"version": "10.0.200",
"rollForward": "latestMajor",
"allowPrerelease": true
}
Expand Down
110 changes: 69 additions & 41 deletions src/ProjGraph.Lib.Core/Domain/Algorithms/TarjanSccAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,55 +109,16 @@ private static void StrongConnect(
{
var frame = dfsStack.Pop();
var node = frame.Node;
var neighborIdx = frame.NeighborIndex;

var neighbors = adjacencyList.TryGetValue(node, out var list) ? list : [];

var pushed = false;
for (var i = neighborIdx; i < neighbors.Count; i++)
{
var w = neighbors[i];
if (!context.Indices.TryGetValue(w, out var wIndex))
{
// Save current frame (will resume at neighbor i+1 after w completes)
dfsStack.Push(new StackFrame(node, i + 1));

// Initialize w and push it
context.Indices[w] = context.Index;
context.Lowlink[w] = context.Index;
context.Index++;
context.Stack.Push(w);
context.OnStack.Add(w);

dfsStack.Push(new StackFrame(w, 0));
pushed = true;
break;
}

if (context.OnStack.Contains(w))
{
context.Lowlink[node] = Math.Min(context.Lowlink[node], wIndex);
}
}

if (pushed)
if (TryPushNeighbor(node, frame.NeighborIndex, adjacencyList, dfsStack, context))
{
continue;
}

// All neighbors processed — check for SCC root
if (context.Lowlink[node] == context.Indices[node])
{
var component = new List<Guid>();
Guid w;
do
{
w = context.Stack.Pop();
context.OnStack.Remove(w);
component.Add(w);
} while (w != node);

context.Sccs.Add(component);
CollectSccComponent(node, context);
}

// Update parent's lowlink
Expand All @@ -168,4 +129,71 @@ private static void StrongConnect(
}
}
}

/// <summary>
/// Iterates over the neighbors of <paramref name="node"/> starting at <paramref name="neighborIdx"/>,
/// and pushes the first unvisited neighbor onto the DFS stack.
/// </summary>
/// <param name="node">The node whose neighbors to iterate.</param>
/// <param name="neighborIdx">The index into the neighbor list to resume iteration from.</param>
/// <param name="adjacencyList">The adjacency list representing the graph.</param>
/// <param name="dfsStack">The explicit DFS stack used by the iterative algorithm.</param>
/// <param name="context">The context of the Tarjan's algorithm execution.</param>
/// <returns><see langword="true"/> if an unvisited neighbor was pushed; otherwise <see langword="false"/>.</returns>
private static bool TryPushNeighbor(
Guid node,
int neighborIdx,
Dictionary<Guid, List<Guid>> adjacencyList,
Stack<StackFrame> dfsStack,
TarjanContext context)
{
var neighbors = adjacencyList.TryGetValue(node, out var list) ? list : [];

for (var i = neighborIdx; i < neighbors.Count; i++)
{
var w = neighbors[i];
if (!context.Indices.TryGetValue(w, out var wIndex))
{
// Save current frame (will resume at neighbor i+1 after w completes)
dfsStack.Push(new StackFrame(node, i + 1));

// Initialize w and push it
context.Indices[w] = context.Index;
context.Lowlink[w] = context.Index;
context.Index++;
context.Stack.Push(w);
context.OnStack.Add(w);

dfsStack.Push(new StackFrame(w, 0));
return true;
}

if (context.OnStack.Contains(w))
{
context.Lowlink[node] = Math.Min(context.Lowlink[node], wIndex);
}
}

return false;
}

/// <summary>
/// Pops nodes from the Tarjan stack until <paramref name="node"/> is reached,
/// forming a strongly connected component and adding it to the results.
/// </summary>
/// <param name="node">The root node of the strongly connected component.</param>
/// <param name="context">The context of the Tarjan's algorithm execution.</param>
private static void CollectSccComponent(Guid node, TarjanContext context)
{
var component = new List<Guid>();
Guid w;
do
{
w = context.Stack.Pop();
context.OnStack.Remove(w);
component.Add(w);
} while (w != node);

context.Sccs.Add(component);
}
}
2 changes: 1 addition & 1 deletion tests/ProjGraph.Tests.Unit.Core/NullOutputConsoleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public async Task PromptSelectionAsync_ShouldReturnFirstChoice()
[Fact]
public async Task PromptSelectionAsync_EmptyChoices_ShouldThrowInvalidOperationException()
{
var act = () => _sut.PromptSelectionAsync("Pick one", Array.Empty<string>());
var act = () => _sut.PromptSelectionAsync("Pick one", []);

await act.Should().ThrowAsync<InvalidOperationException>()
.WithMessage("*No choices available*");
Expand Down
Loading