codeanalyzer-python 0.3.1__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.
Files changed (42) hide show
  1. codeanalyzer/__main__.py +51 -4
  2. codeanalyzer/core.py +153 -82
  3. codeanalyzer/dataflow/__init__.py +35 -0
  4. codeanalyzer/dataflow/access_paths.py +563 -0
  5. codeanalyzer/dataflow/alias.py +93 -0
  6. codeanalyzer/dataflow/builder.py +688 -0
  7. codeanalyzer/dataflow/cfg.py +605 -0
  8. codeanalyzer/dataflow/defuse.py +113 -0
  9. codeanalyzer/dataflow/dominance.py +140 -0
  10. codeanalyzer/dataflow/identity.py +91 -0
  11. codeanalyzer/dataflow/pdg.py +100 -0
  12. codeanalyzer/dataflow/scalpel_oracle.py +269 -0
  13. codeanalyzer/dataflow/scc.py +91 -0
  14. codeanalyzer/dataflow/sdg.py +424 -0
  15. codeanalyzer/dataflow/slicing.py +93 -0
  16. codeanalyzer/dataflow/summaries.py +217 -0
  17. codeanalyzer/dataflow/syntactic.py +26 -0
  18. codeanalyzer/neo4j/bolt.py +19 -4
  19. codeanalyzer/neo4j/cypher.py +9 -3
  20. codeanalyzer/neo4j/emit.py +8 -3
  21. codeanalyzer/neo4j/project.py +241 -60
  22. codeanalyzer/neo4j/rows.py +18 -15
  23. codeanalyzer/neo4j/schema.py +43 -7
  24. codeanalyzer/options/options.py +4 -0
  25. codeanalyzer/schema/__init__.py +19 -0
  26. codeanalyzer/schema/assign_ids.py +37 -0
  27. codeanalyzer/schema/call_graph_ids.py +12 -0
  28. codeanalyzer/schema/ids.py +23 -0
  29. codeanalyzer/schema/l1_body.py +29 -0
  30. codeanalyzer/schema/l2_callees.py +36 -0
  31. codeanalyzer/schema/py_schema.py +141 -30
  32. codeanalyzer/semantic_analysis/call_graph.py +24 -27
  33. codeanalyzer/semantic_analysis/pycg/pycg_analysis.py +10 -10
  34. codeanalyzer/semantic_analysis/pycg/shard_planner.py +7 -7
  35. codeanalyzer/syntactic_analysis/symbol_table_builder.py +29 -10
  36. {codeanalyzer_python-0.3.1.dist-info → codeanalyzer_python-1.0.0.dist-info}/METADATA +230 -43
  37. codeanalyzer_python-1.0.0.dist-info/RECORD +59 -0
  38. codeanalyzer_python-0.3.1.dist-info/RECORD +0 -39
  39. {codeanalyzer_python-0.3.1.dist-info → codeanalyzer_python-1.0.0.dist-info}/WHEEL +0 -0
  40. {codeanalyzer_python-0.3.1.dist-info → codeanalyzer_python-1.0.0.dist-info}/entry_points.txt +0 -0
  41. {codeanalyzer_python-0.3.1.dist-info → codeanalyzer_python-1.0.0.dist-info}/licenses/LICENSE +0 -0
  42. {codeanalyzer_python-0.3.1.dist-info → codeanalyzer_python-1.0.0.dist-info}/licenses/NOTICE +0 -0
codeanalyzer/__main__.py CHANGED
@@ -114,11 +114,32 @@ def main(
114
114
  typer.Option(
115
115
  "-a",
116
116
  "--analysis-level",
117
- help="Analysis depth: 1=symbol table+Jedi call graph, 2=+PyCG call graph.",
117
+ help="Analysis depth: 1=symbol table+Jedi call graph, 2=+PyCG call "
118
+ "graph, 3=+native intraprocedural dataflow (CFG/PDG), "
119
+ "4=+interprocedural SDG (param/summary edges, alias-aware DDG).",
118
120
  min=1,
119
- max=2,
121
+ max=4,
120
122
  ),
121
123
  ] = 1,
124
+ graphs: Annotated[
125
+ str,
126
+ typer.Option(
127
+ "--graphs",
128
+ help="Level 3+ only: comma-separated program-graph sections to emit "
129
+ "(cfg, dfg, pdg, sdg). Default: cfg,dfg,pdg. `dfg` emits the PDG's data "
130
+ "edges only; `sdg` requires -a 4.",
131
+ ),
132
+ ] = "cfg,dfg,pdg",
133
+ graph_field_depth: Annotated[
134
+ int,
135
+ typer.Option(
136
+ "--graph-field-depth",
137
+ help="Level 3 only: k-limit on access-path depth (x.f.g.h with "
138
+ "k=3 becomes x.f.g.*). Mandatory bound — it is what guarantees "
139
+ "the interprocedural fixpoint terminates.",
140
+ min=1,
141
+ ),
142
+ ] = 3,
122
143
  using_ray: Annotated[
123
144
  bool,
124
145
  typer.Option("--ray/--no-ray", help="Enable Ray for distributed analysis."),
@@ -243,6 +264,30 @@ def main(
243
264
  ),
244
265
  ] = 50,
245
266
  ):
267
+ # Flag validation (strict: unrecognized values error out, never fall back).
268
+ selected_graphs = [g.strip() for g in graphs.split(",") if g.strip()]
269
+ from codeanalyzer.dataflow.builder import VALID_GRAPHS
270
+
271
+ unknown_graphs = [g for g in selected_graphs if g not in VALID_GRAPHS]
272
+ if unknown_graphs:
273
+ logger.error(
274
+ f"Unrecognized --graphs value(s): {', '.join(unknown_graphs)} "
275
+ f"(valid: {', '.join(VALID_GRAPHS)})."
276
+ )
277
+ raise typer.Exit(code=2)
278
+ if not selected_graphs:
279
+ logger.error("--graphs requires at least one of: " + ", ".join(VALID_GRAPHS))
280
+ raise typer.Exit(code=2)
281
+ if "sdg" in selected_graphs and analysis_level < 4:
282
+ logger.error("--graphs sdg requires -a 4 (interprocedural SDG).")
283
+ raise typer.Exit(code=2)
284
+ if analysis_level < 3 and graphs != "cfg,dfg,pdg":
285
+ logger.error("--graphs is a level-3 option; pass -a 3 to emit program graphs.")
286
+ raise typer.Exit(code=2)
287
+ if analysis_level < 3 and graph_field_depth != 3:
288
+ logger.error("--graph-field-depth is a level-3 option; pass -a 3.")
289
+ raise typer.Exit(code=2)
290
+
246
291
  options = AnalysisOptions(
247
292
  input=input,
248
293
  output=output,
@@ -254,6 +299,8 @@ def main(
254
299
  neo4j_password=neo4j_password,
255
300
  neo4j_database=neo4j_database,
256
301
  analysis_level=analysis_level,
302
+ graphs=",".join(selected_graphs),
303
+ graph_field_depth=graph_field_depth,
257
304
  using_ray=using_ray,
258
305
  rebuild_analysis=rebuild_analysis,
259
306
  skip_tests=skip_tests,
@@ -310,7 +357,7 @@ def main(
310
357
 
311
358
  emit_neo4j(artifacts, options)
312
359
  elif options.output is None:
313
- print(model_dump_json(artifacts, separators=(",", ":")))
360
+ print(model_dump_json(artifacts, exclude_none=True))
314
361
  else:
315
362
  options.output.mkdir(parents=True, exist_ok=True)
316
363
  _write_output(artifacts, options.output, options.format)
@@ -321,7 +368,7 @@ def _write_output(artifacts, output_dir: Path, format: OutputFormat):
321
368
  if format == OutputFormat.JSON:
322
369
  output_file = output_dir / "analysis.json"
323
370
  # Use Pydantic's model_dump_json() for compact output
324
- json_str = model_dump_json(artifacts, indent=None)
371
+ json_str = model_dump_json(artifacts, indent=None, exclude_none=True)
325
372
  with output_file.open("w") as f:
326
373
  f.write(json_str)
327
374
  logger.info(f"Analysis saved to {output_file}")
codeanalyzer/core.py CHANGED
@@ -11,12 +11,17 @@ import time
11
11
  import ray
12
12
  from codeanalyzer.utils import logger
13
13
  from codeanalyzer.schema import (
14
+ Analysis,
14
15
  PyApplication,
15
16
  PyExternalSymbol,
16
17
  PyModule,
17
18
  model_dump_json,
18
19
  model_validate_json,
19
20
  )
21
+ from codeanalyzer.schema.assign_ids import assign_ids
22
+ from codeanalyzer.schema.l1_body import populate_l1_body
23
+ from codeanalyzer.schema.l2_callees import backfill_callees
24
+ from codeanalyzer.schema.call_graph_ids import reidentify_call_graph
20
25
  from codeanalyzer.schema.py_schema import PyCallEdge
21
26
  from codeanalyzer.semantic_analysis.call_graph import (
22
27
  filter_external_edges,
@@ -49,7 +54,7 @@ def _process_file_with_ray(py_file: Union[Path, str], project_dir: Union[Path, s
49
54
  try:
50
55
  py_file = Path(py_file)
51
56
  symbol_table_builder = SymbolTableBuilder(project_dir, virtualenv)
52
- module_map[str(py_file)] = symbol_table_builder.build_pymodule_from_file(py_file)
57
+ module_map[str(py_file.relative_to(Path(project_dir)))] = symbol_table_builder.build_pymodule_from_file(py_file)
53
58
  except Exception as e:
54
59
  console.log(f"❌ Failed to process {py_file}: {e}")
55
60
  raise SymbolTableBuilderRayError(f"Ray processing error for {py_file}: {e}")
@@ -371,67 +376,52 @@ class Codeanalyzer:
371
376
  shutil.rmtree(self.cache_dir)
372
377
 
373
378
  @staticmethod
374
- def _compute_external_symbols(symbol_table, call_graph):
375
- """Build the external-symbol map: every call-graph endpoint whose signature
376
- is not a declared class/callable in the symbol table is an external (an
377
- imported library or builtin member). ``name``/``module`` are derived from
378
- the signature (best effort: split on the last dot)."""
379
- declared = set()
380
-
381
- def walk_callable(c):
382
- declared.add(c.signature)
383
- for ic in (c.inner_callables or {}).values():
384
- walk_callable(ic)
385
- for cl in (c.inner_classes or {}).values():
386
- walk_class(cl)
387
-
388
- def walk_class(cl):
389
- declared.add(cl.signature)
390
- for m in (cl.methods or {}).values():
391
- walk_callable(m)
392
- for ic in (cl.inner_classes or {}).values():
393
- walk_class(ic)
394
-
395
- for mod in symbol_table.values():
396
- for c in (mod.functions or {}).values():
397
- walk_callable(c)
398
- for cl in (mod.classes or {}).values():
399
- walk_class(cl)
400
-
379
+ def _home_external_symbols(app, app_id, sig_to_id):
380
+ """Home every call-graph endpoint that is not a declared class/callable
381
+ onto a ``can://…/@external/<module>/<name>`` id (the keystone edge-endpoint
382
+ id home). Registers each homed id in ``sig_to_id`` so callee backfill and
383
+ call-graph re-identity map the dotted signature to it, and returns the
384
+ id-keyed external-symbol map. ``name``/``module`` are derived from the
385
+ signature (best effort: split on the last dot)."""
401
386
  externals: Dict[str, PyExternalSymbol] = {}
402
- for edge in call_graph:
403
- for sig in (edge.source, edge.target):
404
- if sig in declared or sig in externals:
387
+ for edge in app.call_graph:
388
+ for sig in (edge.src, edge.dst):
389
+ if sig in sig_to_id:
405
390
  continue
406
- module, name = sig.rsplit(".", 1) if "." in sig else (sig, sig)
407
- externals[sig] = PyExternalSymbol(name=name, module=module)
391
+ module, name = sig.rsplit(".", 1) if "." in sig else (None, sig)
392
+ ext_id = f"{app_id}/@external/{module}/{name}" if module else \
393
+ f"{app_id}/@external/{name}"
394
+ sig_to_id[sig] = ext_id
395
+ externals[ext_id] = PyExternalSymbol(
396
+ id=ext_id, name=name, module=module
397
+ )
408
398
  return externals
409
399
 
410
- def analyze(self) -> PyApplication:
411
- """Analyze the project and return a PyApplication with symbol table.
412
-
400
+ def analyze(self) -> Analysis:
401
+ """Analyze the project and return the v2 ``Analysis`` envelope.
402
+
413
403
  Uses caching to avoid re-analyzing unchanged files.
414
404
  """
415
405
  cache_file = self.cache_dir / "analysis_cache.json"
416
406
 
417
407
  # Try to load existing cached analysis
418
- cached_pyapplication = None
408
+ cached = None
419
409
  if not self.rebuild_analysis and cache_file.exists():
420
410
  try:
421
- cached_pyapplication = self._load_pyapplication_from_cache(cache_file)
422
- logger.info("Loaded cached analysis")
411
+ cached = self._load_pyapplication_from_cache(cache_file)
412
+ if cached is not None:
413
+ logger.info("Loaded cached analysis")
423
414
  except Exception as e:
424
415
  logger.warning(f"Failed to load cache: {e}. Rebuilding analysis.")
425
- cached_pyapplication = None
416
+ cached = None
426
417
 
427
- if cached_pyapplication is not None and not self._cache_analyzer_matches(
428
- cached_pyapplication, analyzer_info(self.analysis_level).version
429
- ):
430
- logger.info("Analysis cache written by a different analyzer version; rebuilding.")
431
- cached_pyapplication = None
418
+ if not self._cache_analyzer_matches(cached, analyzer_info(self.analysis_level).version):
419
+ if cached is not None:
420
+ logger.info("Analysis cache written by a different analyzer version; rebuilding.")
421
+ cached = None
432
422
 
433
423
  # Build symbol table from cached application if available (if no available, the build a new one)
434
- symbol_table = self._build_symbol_table(cached_pyapplication.symbol_table if cached_pyapplication else {})
424
+ symbol_table = self._build_symbol_table(cached.application.symbol_table if cached else {})
435
425
 
436
426
  resolve_unresolved_constructors(symbol_table)
437
427
 
@@ -449,17 +439,11 @@ class Codeanalyzer:
449
439
 
450
440
  call_graph = filter_external_edges(call_graph, symbol_table)
451
441
 
452
- # Classify call-graph endpoints that are not declared in the symbol table
453
- # (imported library / builtin members) once, so the JSON and Neo4j backends
454
- # share one authoritative external-symbol set.
455
- external_symbols = self._compute_external_symbols(symbol_table, call_graph)
456
-
457
442
  # Recreate pyapplication
458
443
  app = (
459
444
  PyApplication.builder()
460
445
  .symbol_table(symbol_table)
461
446
  .call_graph(call_graph)
462
- .external_symbols(external_symbols)
463
447
  .build()
464
448
  )
465
449
 
@@ -470,52 +454,139 @@ class Codeanalyzer:
470
454
 
471
455
  # Single choke point for provenance: every produced app (fresh symbol
472
456
  # table or reused-from-cache) passes through here before being cached
473
- # or returned, so analyzer/repository always reflect *this* run/checkout
474
- # even when the symbol table itself came from the on-disk cache.
475
- app.analyzer = analyzer_info(self.analysis_level)
457
+ # or returned, so repository provenance always reflects *this* checkout
458
+ # even when the symbol table itself came from the on-disk cache. The
459
+ # analyzer identity rides the envelope below (keystone home).
476
460
  app.repository = repository_info(self.project_dir)
477
461
 
478
- # Save to cache
479
- self._save_analysis_cache(app, cache_file)
480
-
481
- return app
462
+ app_name = self.options.app_name or self.project_dir.name
463
+ sig_to_id = assign_ids(app, app_name)
464
+ # Home call-graph endpoints that are not declared in the symbol table
465
+ # (imported library / builtin members) onto @external ids once, so the
466
+ # JSON and Neo4j backends share one authoritative external-symbol set
467
+ # and every edge endpoint joins the id space (no dangling endpoints).
468
+ app.external_symbols = self._home_external_symbols(app, app.id, sig_to_id)
469
+ populate_l1_body(app)
470
+ if self.analysis_level >= 2:
471
+ backfill_callees(app, sig_to_id)
472
+ reidentify_call_graph(app, sig_to_id)
473
+
474
+ # L3: intraprocedural dataflow (CFG/CDG/DDG) emitted onto the v2 tree.
475
+ if self.analysis_level >= 3:
476
+ from codeanalyzer.dataflow.builder import (
477
+ build_function_pdgs,
478
+ emit_l3_body,
479
+ )
480
+ from codeanalyzer.dataflow.syntactic import SyntacticOracle
481
+
482
+ infos, _func_asts = build_function_pdgs(
483
+ app,
484
+ k=self.options.graph_field_depth,
485
+ oracle_factory=lambda c, fast: SyntacticOracle(),
486
+ )
487
+ emit_l3_body(app, infos, sig_to_id, set(self.options.graphs.split(",")))
488
+
489
+ # L4: interprocedural dataflow (param vertices + summary + param_in/out)
490
+ # layered on top of the L3 syntactic overlay (L3 ⊆ L4). Scalpel is the
491
+ # primary may-alias oracle, with the type-based total fallback.
492
+ if self.analysis_level >= 4:
493
+ from codeanalyzer.dataflow.builder import (
494
+ _base_types,
495
+ build_program_graphs,
496
+ emit_ddg_pointsto_delta,
497
+ emit_l4,
498
+ )
499
+ from codeanalyzer.dataflow.scalpel_oracle import make_alias_oracle
500
+
501
+ ir = build_program_graphs(
502
+ app,
503
+ k=self.options.graph_field_depth,
504
+ oracle_factory=lambda c, fast: make_alias_oracle(
505
+ c, fast, _base_types(c)
506
+ ),
507
+ )
508
+ emit_l4(app, ir, sig_to_id)
509
+ # Semantic ddg delta: the alias-derived def-use edges the real
510
+ # oracle adds beyond the L3 syntactic set, tagged prov=["points-to"].
511
+ # ``infos`` are the syntactic (L3) PDGs from the >=3 block above.
512
+ emit_ddg_pointsto_delta(app, infos, ir, sig_to_id)
513
+
514
+ # Build the v2 envelope, then persist it (the cache stores the full
515
+ # ``Analysis`` envelope so a reused cache round-trips schema_version).
516
+ # k_limit is an L3+ envelope key: below the dataflow levels it stays
517
+ # None and exclude_none drops it from the payload.
518
+ analysis = Analysis(
519
+ max_level=self.analysis_level,
520
+ k_limit=self.options.graph_field_depth if self.analysis_level >= 3 else None,
521
+ analyzer=analyzer_info(self.analysis_level),
522
+ application=app,
523
+ )
524
+ self._save_analysis_cache(analysis, cache_file)
525
+
526
+ return analysis
482
527
 
483
528
  @staticmethod
484
- def _cache_analyzer_matches(cached_app: Optional[PyApplication], current_version: str) -> bool:
529
+ def _cache_analyzer_matches(cached: Optional[Analysis], current_version: str) -> bool:
485
530
  """A cache written by another analyzer version (or before versions were
486
531
  recorded) may lack fields the current models populate — pydantic fills
487
- silent defaults, which would masquerade as analyzed absence."""
532
+ silent defaults, which would masquerade as analyzed absence. The
533
+ analyzer identity lives on the envelope (keystone home)."""
488
534
  return (
489
- cached_app is not None
490
- and cached_app.analyzer is not None
491
- and cached_app.analyzer.version == current_version
535
+ cached is not None
536
+ and cached.analyzer is not None
537
+ and cached.analyzer.version == current_version
492
538
  )
493
539
 
494
- def _load_pyapplication_from_cache(self, cache_file: Path) -> PyApplication:
495
- """Load cached analysis from file.
496
-
540
+ def _load_pyapplication_from_cache(self, cache_file: Path) -> Optional[Analysis]:
541
+ """Load a cached v2 ``Analysis`` envelope from file.
542
+
543
+ A cache written by an older (v1) analyzer stored a bare
544
+ ``PyApplication`` with no ``schema_version``; such a payload no longer
545
+ validates as an ``Analysis`` (or carries the wrong ``schema_version``).
546
+ In that case we log and return ``None`` so the caller treats it as a
547
+ cache miss and rebuilds from scratch — rather than crashing.
548
+
497
549
  Args:
498
550
  cache_file: Path to the cache file
499
-
551
+
500
552
  Returns:
501
- PyApplication: The cached application data
553
+ Optional[Analysis]: The cached envelope, or ``None`` if the cache is
554
+ stale/incompatible and should be rebuilt.
502
555
  """
503
556
  with cache_file.open('r') as f:
504
557
  data = f.read()
505
- return model_validate_json(PyApplication, data)
506
-
507
- def _save_analysis_cache(self, app: PyApplication, cache_file: Path) -> None:
508
- """Save analysis to cache file.
509
-
558
+ try:
559
+ cached = model_validate_json(Analysis, data)
560
+ except Exception:
561
+ logger.info("stale/incompatible analysis cache — rebuilding")
562
+ return None
563
+ if getattr(cached, "schema_version", None) != "2.0.0":
564
+ logger.info("stale/incompatible analysis cache (schema_version) — rebuilding")
565
+ return None
566
+ # The cache keys only on file hash/mtime/size, not on level, so a cache
567
+ # built at a different analysis_level would leak higher-level body/edge
568
+ # content (or omit content when the cached level is lower). Reject the
569
+ # mismatch and force a full rebuild at the requested level.
570
+ if cached.max_level != self.analysis_level:
571
+ logger.info(
572
+ f"cache built at level {cached.max_level} != requested "
573
+ f"{self.analysis_level} — rebuilding"
574
+ )
575
+ return None
576
+ return cached
577
+
578
+ def _save_analysis_cache(self, analysis: Analysis, cache_file: Path) -> None:
579
+ """Save the v2 ``Analysis`` envelope to the cache file.
580
+
510
581
  Args:
511
- app: The PyApplication to cache
582
+ analysis: The Analysis envelope to cache
512
583
  cache_file: Path to save the cache file
513
584
  """
514
585
  # Ensure cache directory exists
515
586
  cache_file.parent.mkdir(parents=True, exist_ok=True)
516
-
587
+
517
588
  with cache_file.open('w') as f:
518
- f.write(model_dump_json(app, indent=2))
589
+ f.write(model_dump_json(analysis, indent=2))
519
590
 
520
591
  logger.info(f"Analysis cached to {cache_file}")
521
592
 
@@ -583,9 +654,9 @@ class Codeanalyzer:
583
654
  if self.file_name is not None:
584
655
  single_file = self.project_dir / self.file_name
585
656
  logger.info(f"Analyzing single file: {single_file}")
586
-
657
+
587
658
  # Check if file is in cache and unchanged
588
- file_key = str(single_file)
659
+ file_key = str(single_file.relative_to(self.project_dir))
589
660
  if file_key in cached_symbol_table and not self.rebuild_analysis:
590
661
  # Compute file checksum to see if it changed
591
662
  if self._file_unchanged(single_file, cached_symbol_table[file_key]):
@@ -635,7 +706,7 @@ class Codeanalyzer:
635
706
  # Separate files into cached and new/changed
636
707
  files_to_process = []
637
708
  for py_file in py_files:
638
- file_key = str(py_file)
709
+ file_key = str(py_file.relative_to(self.project_dir))
639
710
  if file_key in cached_symbol_table and not self.rebuild_analysis:
640
711
  if self._file_unchanged(py_file, cached_symbol_table[file_key]):
641
712
  # Use cached version
@@ -663,7 +734,7 @@ class Codeanalyzer:
663
734
 
664
735
  with ProgressBar(len(py_files), "Building symbol table") as progress:
665
736
  for py_file in py_files:
666
- file_key = str(py_file)
737
+ file_key = str(py_file.relative_to(self.project_dir))
667
738
 
668
739
  # Check if file is cached and unchanged
669
740
  if file_key in cached_symbol_table and not self.rebuild_analysis:
@@ -699,7 +770,7 @@ class Codeanalyzer:
699
770
  """Build PyCG-resolved call edges.
700
771
 
701
772
  Runs PyCG's iterative name-pointer analysis over the whole project
702
- and returns edges with ``provenance=["pycg"]``. Falls back to an
773
+ and returns edges with ``prov=["pycg"]``. Falls back to an
703
774
  empty list and logs a warning on any failure so the caller can
704
775
  continue with Jedi-only edges.
705
776
 
@@ -0,0 +1,35 @@
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
+ """Level-3 native dataflow graphs: CFG, PDG (CDG + DDG), and the SDG.
18
+
19
+ One pass per module, mirroring the construction ladder:
20
+
21
+ - :mod:`cfg` — stage 1, exceptional statement-level CFG per callable;
22
+ - :mod:`dominance` — stage 2, post-dominators and control dependence;
23
+ - :mod:`access_paths` — stage 3a, the k-limited access-path variable model;
24
+ - :mod:`defuse` — stage 3b, reaching definitions → DDG edges;
25
+ - :mod:`alias` — stage 5, the type-based may-alias oracle (MVP stub);
26
+ - :mod:`scc` — stage 5, Tarjan SCC condensation of the call graph;
27
+ - :mod:`summaries` — stage 6, bottom-up formal-in → formal-out summaries;
28
+ - :mod:`sdg` — stage 7, parameter nodes and CALL/PARAM_IN/PARAM_OUT/SUMMARY
29
+ edges;
30
+ - :mod:`slicing` — stage 8, the two-phase context-sensitive backward slice;
31
+ - :mod:`builder` — the orchestrator ``build_program_graphs`` wired into
32
+ ``Codeanalyzer.analyze`` at ``-a 3``.
33
+ """
34
+
35
+ from codeanalyzer.dataflow.cfg import build_cfg # noqa: F401