Repository navigation
Refactor codeanalyzer-python #223
Description
Is your feature request related to a problem? Please describe.
The analysis code is super hard to follow and brittle as all hell. Each algorithm builds its own graph structures, results come back as bare dicts and tuples, and the order of steps is fixed in one large orchestrator. Small changes keep causing bugs where two parts disagree about the same data.
Describe the solution you'd like
I propose three changes:
- Use networkx as the one graph type. Control-flow, call, dependence and system-dependence graphs become typed wrappers around networkx graphs. Dominators, SCCs and reachability come from networkx. We keep one small worklist solver for the dataflow analyses, such as reaching definitions and function summaries.
- Build the analysis as named passes. Each pass has a name, a scope (application, module or callable) and the lowest analysis level (-a) at which it runs. The pipeline is assembled in one place with add_pass and add_edge. Compiling it for a given level checks the wiring, orders the passes and rejects cycles.
- Keep the analysis separate from the output. Passes work on the graph and know nothing about analysis.json or Neo4j. Serializers turn the finished graph into each format.
Not changing: the schema, the CLI, the analysis levels, or the meaning of any output.
It'd be considered done when analysis.json is byte-for-byte identical at every level on the test fixtures.
Each hand-written kernel is replaced one at a time and compared against the old version before the old one is deleted.
Additional context
Sketch
@dataclass(frozen=True)
class AnalysisPass:
name: str
fn: Callable[[Dict[str, Fragment]], Fragment] # sees earlier results, returns only additions
scope: str = "application" # "application" | "module" | "callable"
min_level: int = 1 # lowest -a level that runs this pass
class AnalysisPipeline:
"""Builder. Nothing runs until compile()."""
def add_pass(self, name, fn, *, scope="application", min_level=1) -> "AnalysisPipeline": ...
def add_edge(self, before: str, after: str) -> "AnalysisPipeline": ...
def compile(self, level: int) -> "CompiledPipeline":
# keep passes with min_level <= level, check every edge, then order them
# with nx.lexicographical_topological_sort (deterministic; a cycle raises)
...
@dataclass(frozen=True)
class CompiledPipeline:
plan: Tuple[AnalysisPass, ...]
def execute(self, results): ... # run each pass in order and store its resultThe whole pipeline is wired in one place:
def build_pipeline() -> AnalysisPipeline:
ap = AnalysisPipeline()
ap.add_pass("symbols", build_symbols)
ap.add_pass("calls", resolve_calls, min_level=2)
ap.add_pass("cfg", build_cfg, scope="callable", min_level=3)
ap.add_edge("symbols", "calls").add_edge("symbols", "cfg")
return ap
compiled = build_pipeline().compile(level=opts.level)
graph = compiled.execute(ctx)Compiling for -a 1, -a 2 and -a 3 gives symbols, then symbols -> calls, then symbols -> calls -> cfg. This matches the existing rule that each level only adds to the one before.
Activity
[-]Refactor codeanalyser-python[/-][+]Refactor codeanalyzer-python[/+]rahlk commentedon Oct 11, 2026
Some cli flags also need clean up...