codeanalyzer-python 0.3.0__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codeanalyzer/__main__.py +77 -4
- codeanalyzer/core.py +174 -72
- codeanalyzer/dataflow/__init__.py +35 -0
- codeanalyzer/dataflow/access_paths.py +563 -0
- codeanalyzer/dataflow/alias.py +93 -0
- codeanalyzer/dataflow/builder.py +688 -0
- codeanalyzer/dataflow/cfg.py +605 -0
- codeanalyzer/dataflow/defuse.py +113 -0
- codeanalyzer/dataflow/dominance.py +140 -0
- codeanalyzer/dataflow/identity.py +91 -0
- codeanalyzer/dataflow/pdg.py +100 -0
- codeanalyzer/dataflow/scalpel_oracle.py +269 -0
- codeanalyzer/dataflow/scc.py +91 -0
- codeanalyzer/dataflow/sdg.py +424 -0
- codeanalyzer/dataflow/slicing.py +93 -0
- codeanalyzer/dataflow/summaries.py +217 -0
- codeanalyzer/dataflow/syntactic.py +26 -0
- codeanalyzer/neo4j/__init__.py +1 -1
- codeanalyzer/neo4j/bolt.py +19 -4
- codeanalyzer/neo4j/cypher.py +9 -3
- codeanalyzer/neo4j/emit.py +10 -5
- codeanalyzer/neo4j/project.py +307 -60
- codeanalyzer/neo4j/rows.py +18 -15
- codeanalyzer/neo4j/schema.py +297 -15
- codeanalyzer/options/options.py +4 -0
- codeanalyzer/provenance.py +61 -0
- codeanalyzer/schema/__init__.py +19 -0
- codeanalyzer/schema/assign_ids.py +37 -0
- codeanalyzer/schema/call_graph_ids.py +12 -0
- codeanalyzer/schema/ids.py +23 -0
- codeanalyzer/schema/l1_body.py +29 -0
- codeanalyzer/schema/l2_callees.py +36 -0
- codeanalyzer/schema/py_schema.py +175 -26
- codeanalyzer/semantic_analysis/call_graph.py +24 -27
- codeanalyzer/semantic_analysis/pycg/pycg_analysis.py +10 -10
- codeanalyzer/semantic_analysis/pycg/shard_planner.py +7 -7
- codeanalyzer/syntactic_analysis/import_resolver.py +67 -0
- codeanalyzer/syntactic_analysis/symbol_table_builder.py +103 -20
- {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/METADATA +233 -43
- codeanalyzer_python-1.0.0.dist-info/RECORD +59 -0
- {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/WHEEL +1 -1
- codeanalyzer/neo4j/catalog.py +0 -245
- codeanalyzer_python-0.3.0.dist-info/RECORD +0 -38
- {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/entry_points.txt +0 -0
- {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
################################################################################
|
|
2
|
+
# Copyright IBM Corporation 2025
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
################################################################################
|
|
16
|
+
|
|
17
|
+
"""Stage 4 of the level-3/4 dataflow ladder: the primary L4 may-alias oracle.
|
|
18
|
+
|
|
19
|
+
``python-scalpel`` (SMAT-Lab/Scalpel) is the substrate decided by the Stage-0
|
|
20
|
+
spike (issue #70): not a turnkey points-to engine but an **SSA + copy/const**
|
|
21
|
+
producer. :class:`ScalpelAliasOracle` consumes its *solved* state — it never
|
|
22
|
+
forks or re-runs Scalpel's solver — and turns the copy/const records into
|
|
23
|
+
per-function copy-closure equivalence classes:
|
|
24
|
+
|
|
25
|
+
``from scalpel.SSA.const import SSA``
|
|
26
|
+
``ssa_results, const_dict = SSA().compute_SSA(func_cfg)``
|
|
27
|
+
|
|
28
|
+
``const_dict`` maps ``(name, version)`` to the ``ast`` value node that defined
|
|
29
|
+
that SSA name. A value that is an ``ast.Name`` is a whole-object **copy edge**
|
|
30
|
+
(``b = a`` ⇒ ``('b', 0) -> Name 'a'``); a value that is an ``ast.Attribute`` is
|
|
31
|
+
an attribute-path copy (``q = p.x`` ⇒ ``('q', 0) -> Attribute p.x``). The
|
|
32
|
+
transitive closure of these edges is a union-find over access-path strings.
|
|
33
|
+
|
|
34
|
+
``may_alias(path_a, path_b)`` is TRUE iff the paths are identical, or their
|
|
35
|
+
bases share a copy-closure class (or the whole paths do) *and* their field
|
|
36
|
+
suffixes are prefix-compatible (the same suffix logic the frozen
|
|
37
|
+
:class:`~codeanalyzer.dataflow.alias.TypeBasedAliasOracle` uses). Anything the
|
|
38
|
+
copy closure cannot resolve — unrelated bases, constructs Scalpel does not
|
|
39
|
+
model (heap points-to, two distinct params, container elements) — is delegated
|
|
40
|
+
to a wrapped ``TypeBasedAliasOracle``, which supplies the type-guided verdict
|
|
41
|
+
(incompatible concrete types ⇒ not aliased; unknown type ⇒ may-alias).
|
|
42
|
+
|
|
43
|
+
The oracle is **sound-leaning**: adding copy edges only ever yields *more*
|
|
44
|
+
may-alias answers, and every uncertainty widens to the type-based fallback
|
|
45
|
+
rather than silently returning ``False``. All Scalpel use is guarded — a build
|
|
46
|
+
or query failure degrades to the wrapped fallback, never an exception.
|
|
47
|
+
|
|
48
|
+
The public interface is frozen and identical to ``TypeBasedAliasOracle``:
|
|
49
|
+
``may_alias(path_a: str, path_b: str) -> bool``.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
import ast
|
|
55
|
+
import re
|
|
56
|
+
from typing import Dict, Optional
|
|
57
|
+
|
|
58
|
+
from codeanalyzer.dataflow.access_paths import base_of, suffix_of
|
|
59
|
+
from codeanalyzer.dataflow.alias import TypeBasedAliasOracle
|
|
60
|
+
from codeanalyzer.utils import logger
|
|
61
|
+
|
|
62
|
+
# All subscripts collapse to ``[*]`` to match the access-path grammar
|
|
63
|
+
# (``base(.field | [*])*``) the rest of the dataflow ladder speaks.
|
|
64
|
+
_SUBSCRIPT = re.compile(r"\[[^\[\]]*\]")
|
|
65
|
+
|
|
66
|
+
# Log the "Scalpel unavailable → fallback" notice at most once per process so a
|
|
67
|
+
# large project does not spam one line per function.
|
|
68
|
+
_fallback_logged = False
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _normalize_path(path: str) -> str:
|
|
72
|
+
return _SUBSCRIPT.sub("[*]", path)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _suffix_prefix_compatible(path_a: str, path_b: str) -> bool:
|
|
76
|
+
"""Field-suffix compatibility, identical to the frozen
|
|
77
|
+
``TypeBasedAliasOracle`` rule: identical suffixes may denote one location; a
|
|
78
|
+
bare base (whole-object access) observes every field, so an empty suffix is
|
|
79
|
+
compatible with any; k-truncation wildcards (``*``) match anything deeper."""
|
|
80
|
+
suffix_a, suffix_b = suffix_of(path_a), suffix_of(path_b)
|
|
81
|
+
sa = suffix_a.rstrip("*").rstrip(".")
|
|
82
|
+
sb = suffix_b.rstrip("*").rstrip(".")
|
|
83
|
+
return bool(
|
|
84
|
+
sa == sb
|
|
85
|
+
or sa.startswith(sb)
|
|
86
|
+
or sb.startswith(sa)
|
|
87
|
+
or suffix_a.endswith("*")
|
|
88
|
+
or suffix_b.endswith("*")
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _note_fallback(reason: str) -> None:
|
|
93
|
+
global _fallback_logged
|
|
94
|
+
if not _fallback_logged:
|
|
95
|
+
logger.info(
|
|
96
|
+
"Scalpel may-alias oracle unavailable (%s); using TypeBasedAliasOracle fallback.",
|
|
97
|
+
reason,
|
|
98
|
+
)
|
|
99
|
+
_fallback_logged = True
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class ScalpelAliasOracle:
|
|
103
|
+
"""L4 may-alias oracle backed by Scalpel's SSA copy/const facts.
|
|
104
|
+
|
|
105
|
+
Construct from a raw ``const_dict`` (``(name, version) -> ast value``) or,
|
|
106
|
+
more commonly, via :meth:`from_function`, which imports Scalpel and computes
|
|
107
|
+
the SSA state for a function AST. ``base_types`` (base name → inferred type)
|
|
108
|
+
feeds the wrapped :class:`TypeBasedAliasOracle` used for everything the copy
|
|
109
|
+
closure cannot decide.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def __init__(
|
|
113
|
+
self,
|
|
114
|
+
const_dict: Optional[dict] = None,
|
|
115
|
+
base_types: Optional[Dict[str, Optional[str]]] = None,
|
|
116
|
+
fallback: Optional[TypeBasedAliasOracle] = None,
|
|
117
|
+
):
|
|
118
|
+
self._fallback = fallback or TypeBasedAliasOracle(base_types)
|
|
119
|
+
self._parent: Dict[str, str] = {}
|
|
120
|
+
self._seen: set[str] = set()
|
|
121
|
+
try:
|
|
122
|
+
self._build_classes(const_dict or {})
|
|
123
|
+
except Exception: # pragma: no cover — never let a build quirk escape
|
|
124
|
+
logger.debug(
|
|
125
|
+
"scalpel copy-closure build failed; oracle will lean on fallback",
|
|
126
|
+
exc_info=True,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# -- construction --------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def from_function(
|
|
133
|
+
cls,
|
|
134
|
+
func_ast: ast.AST,
|
|
135
|
+
base_types: Optional[Dict[str, Optional[str]]] = None,
|
|
136
|
+
fallback: Optional[TypeBasedAliasOracle] = None,
|
|
137
|
+
name: Optional[str] = None,
|
|
138
|
+
) -> "ScalpelAliasOracle":
|
|
139
|
+
"""Build from a function AST by consuming Scalpel's solved SSA state.
|
|
140
|
+
|
|
141
|
+
Imports Scalpel lazily (``ImportError`` if the optional dependency is
|
|
142
|
+
absent) and reuses the *same source* both graphs are built from — the
|
|
143
|
+
function's unparsed text — so the join is identity, not a fuzzy match.
|
|
144
|
+
Raises on any build failure; :func:`make_alias_oracle` is the total,
|
|
145
|
+
never-raising entry point callers should prefer.
|
|
146
|
+
"""
|
|
147
|
+
from scalpel.SSA.const import SSA
|
|
148
|
+
from scalpel.cfg import CFGBuilder
|
|
149
|
+
|
|
150
|
+
src = ast.unparse(func_ast)
|
|
151
|
+
fname = name or getattr(func_ast, "name", None)
|
|
152
|
+
module_cfg = CFGBuilder().build_from_src(fname or "module", src)
|
|
153
|
+
func_cfg = cls._select_func_cfg(module_cfg, fname)
|
|
154
|
+
if func_cfg is None:
|
|
155
|
+
raise ValueError("scalpel produced no function CFG for the given AST")
|
|
156
|
+
# Consume the solved state; never re-run the solver ourselves.
|
|
157
|
+
_ssa_results, const_dict = SSA().compute_SSA(func_cfg)
|
|
158
|
+
return cls(const_dict, base_types=base_types, fallback=fallback)
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def _select_func_cfg(module_cfg, fname: Optional[str]):
|
|
162
|
+
"""Pick the target function's CFG out of the module CFG's
|
|
163
|
+
``functioncfgs`` (keyed ``(entry_id, func_name)``)."""
|
|
164
|
+
cfgs = getattr(module_cfg, "functioncfgs", None) or {}
|
|
165
|
+
if fname is not None:
|
|
166
|
+
for key, fcfg in cfgs.items():
|
|
167
|
+
if isinstance(key, tuple) and len(key) >= 2 and key[1] == fname:
|
|
168
|
+
return fcfg
|
|
169
|
+
return next(iter(cfgs.values()), None)
|
|
170
|
+
|
|
171
|
+
# -- copy-closure over const_dict ----------------------------------------
|
|
172
|
+
|
|
173
|
+
def _build_classes(self, const_dict: dict) -> None:
|
|
174
|
+
for key, value in const_dict.items():
|
|
175
|
+
lhs = self._key_to_path(key)
|
|
176
|
+
rhs = self._value_to_path(value)
|
|
177
|
+
if lhs is None or rhs is None:
|
|
178
|
+
continue
|
|
179
|
+
self._union(lhs, rhs)
|
|
180
|
+
|
|
181
|
+
@staticmethod
|
|
182
|
+
def _key_to_path(key) -> Optional[str]:
|
|
183
|
+
name = key[0] if isinstance(key, tuple) and key else key
|
|
184
|
+
if not isinstance(name, str):
|
|
185
|
+
return None
|
|
186
|
+
return _normalize_path(name)
|
|
187
|
+
|
|
188
|
+
@staticmethod
|
|
189
|
+
def _value_to_path(value) -> Optional[str]:
|
|
190
|
+
# ``ast.Name`` value ⇒ whole-object copy edge (name ↔ name).
|
|
191
|
+
if isinstance(value, ast.Name):
|
|
192
|
+
return _normalize_path(value.id)
|
|
193
|
+
# ``ast.Attribute`` value ⇒ attribute-path copy (name ↔ base.field...).
|
|
194
|
+
if isinstance(value, ast.Attribute):
|
|
195
|
+
try:
|
|
196
|
+
return _normalize_path(ast.unparse(value))
|
|
197
|
+
except Exception:
|
|
198
|
+
return None
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
# -- union-find ----------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
def _find(self, x: str) -> str:
|
|
204
|
+
self._parent.setdefault(x, x)
|
|
205
|
+
root = x
|
|
206
|
+
while self._parent[root] != root:
|
|
207
|
+
root = self._parent[root]
|
|
208
|
+
while self._parent[x] != root: # path compression
|
|
209
|
+
self._parent[x], x = root, self._parent[x]
|
|
210
|
+
return root
|
|
211
|
+
|
|
212
|
+
def _union(self, a: str, b: str) -> None:
|
|
213
|
+
self._seen.add(a)
|
|
214
|
+
self._seen.add(b)
|
|
215
|
+
ra, rb = self._find(a), self._find(b)
|
|
216
|
+
if ra != rb:
|
|
217
|
+
self._parent[rb] = ra
|
|
218
|
+
|
|
219
|
+
def _merged(self, a: str, b: str) -> bool:
|
|
220
|
+
# Only trust a shared root when *both* tokens were actually observed in
|
|
221
|
+
# the copy closure — otherwise two distinct unseen tokens are singleton
|
|
222
|
+
# classes and must not be treated as related.
|
|
223
|
+
return a in self._seen and b in self._seen and self._find(a) == self._find(b)
|
|
224
|
+
|
|
225
|
+
# -- frozen interface ----------------------------------------------------
|
|
226
|
+
|
|
227
|
+
def may_alias(self, path_a: str, path_b: str) -> bool:
|
|
228
|
+
if path_a == path_b:
|
|
229
|
+
return True
|
|
230
|
+
try:
|
|
231
|
+
na, nb = _normalize_path(path_a), _normalize_path(path_b)
|
|
232
|
+
base_a, base_b = base_of(na), base_of(nb)
|
|
233
|
+
if base_a == base_b:
|
|
234
|
+
# Same object: purely field-sensitive (distinct fields do not
|
|
235
|
+
# alias); matches the frozen TypeBasedAliasOracle decision.
|
|
236
|
+
return _suffix_prefix_compatible(na, nb)
|
|
237
|
+
# Distinct bases that Scalpel proved to be copies (or whole paths
|
|
238
|
+
# that are copies) alias iff their suffixes are prefix-compatible.
|
|
239
|
+
if self._merged(base_a, base_b) or self._merged(na, nb):
|
|
240
|
+
return _suffix_prefix_compatible(na, nb)
|
|
241
|
+
except Exception:
|
|
242
|
+
logger.debug(
|
|
243
|
+
"scalpel may_alias failed; delegating to fallback", exc_info=True
|
|
244
|
+
)
|
|
245
|
+
# Unresolved by the copy closure: hand off to the type-guided fallback
|
|
246
|
+
# (sound-leaning — when uncertain it over-approximates to True).
|
|
247
|
+
return self._fallback.may_alias(path_a, path_b)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def make_alias_oracle(pycallable, func_ast, base_types) -> object:
|
|
251
|
+
"""Total selector for the L4 may-alias oracle.
|
|
252
|
+
|
|
253
|
+
Returns a :class:`ScalpelAliasOracle` when ``python-scalpel`` is importable
|
|
254
|
+
*and* builds successfully on ``func_ast``; otherwise logs once (INFO) and
|
|
255
|
+
returns a :class:`TypeBasedAliasOracle` over ``base_types``. Never raises —
|
|
256
|
+
mirrors how ``core._get_pycg_call_graph`` degrades on a missing/failed PyCG.
|
|
257
|
+
"""
|
|
258
|
+
fallback = TypeBasedAliasOracle(base_types)
|
|
259
|
+
try:
|
|
260
|
+
return ScalpelAliasOracle.from_function(
|
|
261
|
+
func_ast, base_types=base_types, fallback=fallback
|
|
262
|
+
)
|
|
263
|
+
except ImportError:
|
|
264
|
+
_note_fallback("python-scalpel not installed")
|
|
265
|
+
return fallback
|
|
266
|
+
except Exception:
|
|
267
|
+
_note_fallback("scalpel alias build failed")
|
|
268
|
+
logger.debug("scalpel alias oracle build error", exc_info=True)
|
|
269
|
+
return fallback
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
################################################################################
|
|
2
|
+
# Copyright IBM Corporation 2025
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
################################################################################
|
|
16
|
+
|
|
17
|
+
"""Stage 5b of the level-3 dataflow ladder: SCC condensation of the call graph.
|
|
18
|
+
|
|
19
|
+
The call graph is a frozen oracle (level-1 Jedi edges, provenance-merged with
|
|
20
|
+
level-2 PyCG when enabled); Tarjan condenses it into strongly connected
|
|
21
|
+
components, and the condensation DAG in reverse topological order is the
|
|
22
|
+
bottom-up processing schedule for summary composition — callees before
|
|
23
|
+
callers, one monotone fixpoint per SCC (mutual recursion).
|
|
24
|
+
|
|
25
|
+
Iterative Tarjan (no recursion — real projects overflow Python's stack), with
|
|
26
|
+
sorted tie-breaking so the schedule is deterministic.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from typing import Dict, List, Set, Tuple
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def strongly_connected_components(
|
|
35
|
+
nodes: List[str], edges: List[Tuple[str, str]]
|
|
36
|
+
) -> List[List[str]]:
|
|
37
|
+
"""Tarjan SCCs in reverse topological order (callees before callers).
|
|
38
|
+
Deterministic: nodes are visited in sorted order and members sorted."""
|
|
39
|
+
adj: Dict[str, List[str]] = {n: [] for n in nodes}
|
|
40
|
+
for s, t in sorted(set(edges)):
|
|
41
|
+
if s in adj and t in adj:
|
|
42
|
+
adj[s].append(t)
|
|
43
|
+
|
|
44
|
+
index_of: Dict[str, int] = {}
|
|
45
|
+
lowlink: Dict[str, int] = {}
|
|
46
|
+
on_stack: Set[str] = set()
|
|
47
|
+
stack: List[str] = []
|
|
48
|
+
sccs: List[List[str]] = []
|
|
49
|
+
counter = [0]
|
|
50
|
+
|
|
51
|
+
for root in sorted(adj):
|
|
52
|
+
if root in index_of:
|
|
53
|
+
continue
|
|
54
|
+
# Iterative DFS: (node, iterator position over successors).
|
|
55
|
+
work: List[Tuple[str, int]] = [(root, 0)]
|
|
56
|
+
while work:
|
|
57
|
+
node, i = work.pop()
|
|
58
|
+
if i == 0:
|
|
59
|
+
index_of[node] = lowlink[node] = counter[0]
|
|
60
|
+
counter[0] += 1
|
|
61
|
+
stack.append(node)
|
|
62
|
+
on_stack.add(node)
|
|
63
|
+
recurse = False
|
|
64
|
+
successors = adj[node]
|
|
65
|
+
while i < len(successors):
|
|
66
|
+
succ = successors[i]
|
|
67
|
+
i += 1
|
|
68
|
+
if succ not in index_of:
|
|
69
|
+
work.append((node, i))
|
|
70
|
+
work.append((succ, 0))
|
|
71
|
+
recurse = True
|
|
72
|
+
break
|
|
73
|
+
if succ in on_stack:
|
|
74
|
+
lowlink[node] = min(lowlink[node], index_of[succ])
|
|
75
|
+
if recurse:
|
|
76
|
+
continue
|
|
77
|
+
if lowlink[node] == index_of[node]:
|
|
78
|
+
component: List[str] = []
|
|
79
|
+
while True:
|
|
80
|
+
member = stack.pop()
|
|
81
|
+
on_stack.discard(member)
|
|
82
|
+
component.append(member)
|
|
83
|
+
if member == node:
|
|
84
|
+
break
|
|
85
|
+
sccs.append(sorted(component))
|
|
86
|
+
if work:
|
|
87
|
+
parent = work[-1][0]
|
|
88
|
+
lowlink[parent] = min(lowlink[parent], lowlink[node])
|
|
89
|
+
|
|
90
|
+
# Tarjan emits SCCs in reverse topological order already.
|
|
91
|
+
return sccs
|