logDependencyCycles method
void
logDependencyCycles(})
Detects strongly connected components in the dependency subgraph
restricted to enabledPluginIds (plus additionalEnabledPluginIds
for cross-scope dep resolution) and emits one severe log per cycle
naming the participants.
Detection is informational: cycles where every member is enabled satisfy each other's dependencies and the plugins remain attached. A cycle is still a structural smell (order ambiguity, fragile startup, harder debugging), so silence is the worst outcome. Logging preserves backward-compat behavior while surfacing the issue.
Uses Tarjan's algorithm to find SCCs in one pass. Components of size
= 2 are cycles; components of size 1 with a self-loop edge are self-cycles.
Implementation
void logDependencyCycles(
Set<PluginId> enabledPluginIds,
List<Plugin> pluginSubset, {
Set<PluginId> additionalEnabledPluginIds = const {},
}) {
if (enabledPluginIds.isEmpty) return;
final byId = <PluginId, Plugin>{
for (final p in pluginSubset)
if (enabledPluginIds.contains(p.pluginId)) p.pluginId: p,
};
final reachable = <PluginId>{
...enabledPluginIds,
...additionalEnabledPluginIds,
};
int index = 0;
final indexMap = <PluginId, int>{};
final lowlink = <PluginId, int>{};
final onStack = <PluginId>{};
final stack = <PluginId>[];
final sccs = <List<PluginId>>[];
void strongConnect(PluginId v) {
indexMap[v] = index;
lowlink[v] = index;
index++;
stack.add(v);
onStack.add(v);
final plugin = byId[v];
if (plugin != null) {
for (final w in plugin.dependencies) {
if (!reachable.contains(w)) continue;
if (!indexMap.containsKey(w)) {
strongConnect(w);
final lw = lowlink[w]!;
if (lw < lowlink[v]!) lowlink[v] = lw;
} else if (onStack.contains(w)) {
final iw = indexMap[w]!;
if (iw < lowlink[v]!) lowlink[v] = iw;
}
}
}
if (lowlink[v] == indexMap[v]) {
final scc = <PluginId>[];
PluginId w;
do {
w = stack.removeLast();
onStack.remove(w);
scc.add(w);
} while (w != v);
sccs.add(scc);
}
}
for (final v in enabledPluginIds) {
if (!indexMap.containsKey(v)) strongConnect(v);
}
for (final scc in sccs) {
final isCycle =
scc.length > 1 ||
(scc.length == 1 &&
(byId[scc.first]?.dependencies.contains(scc.first) ?? false));
if (!isCycle) continue;
final names = scc.map((p) => p).join(' -> ');
_runtimeLog.severe(
'Dependency cycle detected among enabled plugins: $names. '
'Cycles are functional when every member is enabled but indicate '
'tight coupling that should usually be one plugin or merged via '
'a shared service.',
);
}
}