From 32bac5d59b060a19747835ab642ddd84be742e58 Mon Sep 17 00:00:00 2001 From: Alexander Kireev Date: Thu, 25 Jun 2026 21:30:04 +0700 Subject: [PATCH] Fix findCycles missing self-loops created with named edges findCycles checked for a single-node component's self-loop via graph.hasEdge(v, v), which only matches the default (unnamed) edge. A self-loop added as a named edge in a multigraph (setEdge(v, v, label, name)) was therefore missed, so findCycles returned no cycle for such a node. Use graph.outEdges(v, v), which returns every edge between v and itself regardless of edge name, so any self-loop is detected. Fixes #111 --- lib/alg/find-cycles.ts | 7 ++++++- test/alg/find-cycles-test.ts | 7 +++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/alg/find-cycles.ts b/lib/alg/find-cycles.ts index a8de55c2..43891d8b 100644 --- a/lib/alg/find-cycles.ts +++ b/lib/alg/find-cycles.ts @@ -1,4 +1,5 @@ import {Graph} from '../graph'; +import type {Edge} from '../types'; import {tarjan} from './tarjan'; /** @@ -14,6 +15,10 @@ import {tarjan} from './tarjan'; */ export function findCycles(graph: Graph): string[][] { return tarjan(graph).filter(function (cmpt) { - return cmpt.length > 1 || (cmpt.length === 1 && graph.hasEdge(cmpt[0]!, cmpt[0]!)); + // A single-node component is a cycle iff the node has a self-loop. We check via outEdges + // rather than hasEdge(v, v) because the latter only matches the default (unnamed) edge and + // would miss a named self-loop edge in a multigraph. + return cmpt.length > 1 + || (cmpt.length === 1 && (graph.outEdges(cmpt[0]!, cmpt[0]!) as Edge[]).length > 0); }); } diff --git a/test/alg/find-cycles-test.ts b/test/alg/find-cycles-test.ts index b97aa9fe..865a5dc3 100644 --- a/test/alg/find-cycles-test.ts +++ b/test/alg/find-cycles-test.ts @@ -30,6 +30,13 @@ describe("alg.findCycles", () => { expect(sort(findCycles(g))).toEqual([["a", "b", "c"]]); }); + it("detects a self-loop created with a named edge in a multigraph", () => { + const g = new Graph({multigraph: true}); + g.setNode("a"); + g.setEdge("a", "a", "label", "name"); + expect(sort(findCycles(g))).toEqual([["a"]]); + }); + it("returns multiple entries for multiple cycles", () => { const g = new Graph(); g.setPath(["a", "b", "a"]);