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.
Files changed (46) hide show
  1. codeanalyzer/__main__.py +77 -4
  2. codeanalyzer/core.py +174 -72
  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/__init__.py +1 -1
  19. codeanalyzer/neo4j/bolt.py +19 -4
  20. codeanalyzer/neo4j/cypher.py +9 -3
  21. codeanalyzer/neo4j/emit.py +10 -5
  22. codeanalyzer/neo4j/project.py +307 -60
  23. codeanalyzer/neo4j/rows.py +18 -15
  24. codeanalyzer/neo4j/schema.py +297 -15
  25. codeanalyzer/options/options.py +4 -0
  26. codeanalyzer/provenance.py +61 -0
  27. codeanalyzer/schema/__init__.py +19 -0
  28. codeanalyzer/schema/assign_ids.py +37 -0
  29. codeanalyzer/schema/call_graph_ids.py +12 -0
  30. codeanalyzer/schema/ids.py +23 -0
  31. codeanalyzer/schema/l1_body.py +29 -0
  32. codeanalyzer/schema/l2_callees.py +36 -0
  33. codeanalyzer/schema/py_schema.py +175 -26
  34. codeanalyzer/semantic_analysis/call_graph.py +24 -27
  35. codeanalyzer/semantic_analysis/pycg/pycg_analysis.py +10 -10
  36. codeanalyzer/semantic_analysis/pycg/shard_planner.py +7 -7
  37. codeanalyzer/syntactic_analysis/import_resolver.py +67 -0
  38. codeanalyzer/syntactic_analysis/symbol_table_builder.py +103 -20
  39. {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/METADATA +233 -43
  40. codeanalyzer_python-1.0.0.dist-info/RECORD +59 -0
  41. {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/WHEEL +1 -1
  42. codeanalyzer/neo4j/catalog.py +0 -245
  43. codeanalyzer_python-0.3.0.dist-info/RECORD +0 -38
  44. {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/entry_points.txt +0 -0
  45. {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/licenses/LICENSE +0 -0
  46. {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/licenses/NOTICE +0 -0
codeanalyzer/__main__.py CHANGED
@@ -1,3 +1,4 @@
1
+ from importlib.metadata import version as _pkg_version, PackageNotFoundError
1
2
  from pathlib import Path
2
3
  from typing import Optional, Annotated
3
4
 
@@ -10,7 +11,32 @@ from codeanalyzer.schema import model_dump_json
10
11
  from codeanalyzer.options import AnalysisOptions, EmitTarget, ShardStrategy
11
12
 
12
13
 
14
+ def _version_callback(value: bool) -> None:
15
+ """Print the installed ``codeanalyzer-python`` version and exit.
16
+
17
+ Eager so it fires before the rest of the CLI callback (no -i/--input
18
+ required). Reads the version from package metadata so it always reflects
19
+ what is actually installed rather than a hardcoded string."""
20
+ if not value:
21
+ return
22
+ try:
23
+ installed = _pkg_version("codeanalyzer-python")
24
+ except PackageNotFoundError:
25
+ installed = "unknown"
26
+ typer.echo(f"canpy {installed}")
27
+ raise typer.Exit()
28
+
29
+
13
30
  def main(
31
+ version: Annotated[
32
+ Optional[bool],
33
+ typer.Option(
34
+ "--version",
35
+ help="Show the canpy version and exit.",
36
+ callback=_version_callback,
37
+ is_eager=True,
38
+ ),
39
+ ] = None,
14
40
  input: Annotated[
15
41
  Optional[Path],
16
42
  typer.Option(
@@ -88,11 +114,32 @@ def main(
88
114
  typer.Option(
89
115
  "-a",
90
116
  "--analysis-level",
91
- 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).",
92
120
  min=1,
93
- max=2,
121
+ max=4,
94
122
  ),
95
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,
96
143
  using_ray: Annotated[
97
144
  bool,
98
145
  typer.Option("--ray/--no-ray", help="Enable Ray for distributed analysis."),
@@ -217,6 +264,30 @@ def main(
217
264
  ),
218
265
  ] = 50,
219
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
+
220
291
  options = AnalysisOptions(
221
292
  input=input,
222
293
  output=output,
@@ -228,6 +299,8 @@ def main(
228
299
  neo4j_password=neo4j_password,
229
300
  neo4j_database=neo4j_database,
230
301
  analysis_level=analysis_level,
302
+ graphs=",".join(selected_graphs),
303
+ graph_field_depth=graph_field_depth,
231
304
  using_ray=using_ray,
232
305
  rebuild_analysis=rebuild_analysis,
233
306
  skip_tests=skip_tests,
@@ -284,7 +357,7 @@ def main(
284
357
 
285
358
  emit_neo4j(artifacts, options)
286
359
  elif options.output is None:
287
- print(model_dump_json(artifacts, separators=(",", ":")))
360
+ print(model_dump_json(artifacts, exclude_none=True))
288
361
  else:
289
362
  options.output.mkdir(parents=True, exist_ok=True)
290
363
  _write_output(artifacts, options.output, options.format)
@@ -295,7 +368,7 @@ def _write_output(artifacts, output_dir: Path, format: OutputFormat):
295
368
  if format == OutputFormat.JSON:
296
369
  output_file = output_dir / "analysis.json"
297
370
  # Use Pydantic's model_dump_json() for compact output
298
- json_str = model_dump_json(artifacts, indent=None)
371
+ json_str = model_dump_json(artifacts, indent=None, exclude_none=True)
299
372
  with output_file.open("w") as f:
300
373
  f.write(json_str)
301
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,
@@ -26,9 +31,11 @@ from codeanalyzer.semantic_analysis.call_graph import (
26
31
  )
27
32
  from codeanalyzer.semantic_analysis.pycg import PyCG, PyCGExceptions
28
33
  from codeanalyzer.syntactic_analysis.exceptions import SymbolTableBuilderRayError
34
+ from codeanalyzer.syntactic_analysis.import_resolver import resolve_imports
29
35
  from codeanalyzer.syntactic_analysis.symbol_table_builder import SymbolTableBuilder
30
36
  from codeanalyzer.utils import ProgressBar
31
37
  from codeanalyzer.options import AnalysisOptions
38
+ from codeanalyzer.provenance import analyzer_info, repository_info
32
39
 
33
40
  @ray.remote
34
41
  def _process_file_with_ray(py_file: Union[Path, str], project_dir: Union[Path, str], virtualenv: Union[Path, str, None]) -> Dict[str, PyModule]:
@@ -47,7 +54,7 @@ def _process_file_with_ray(py_file: Union[Path, str], project_dir: Union[Path, s
47
54
  try:
48
55
  py_file = Path(py_file)
49
56
  symbol_table_builder = SymbolTableBuilder(project_dir, virtualenv)
50
- 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)
51
58
  except Exception as e:
52
59
  console.log(f"❌ Failed to process {py_file}: {e}")
53
60
  raise SymbolTableBuilderRayError(f"Ray processing error for {py_file}: {e}")
@@ -369,61 +376,52 @@ class Codeanalyzer:
369
376
  shutil.rmtree(self.cache_dir)
370
377
 
371
378
  @staticmethod
372
- def _compute_external_symbols(symbol_table, call_graph):
373
- """Build the external-symbol map: every call-graph endpoint whose signature
374
- is not a declared class/callable in the symbol table is an external (an
375
- imported library or builtin member). ``name``/``module`` are derived from
376
- the signature (best effort: split on the last dot)."""
377
- declared = set()
378
-
379
- def walk_callable(c):
380
- declared.add(c.signature)
381
- for ic in (c.inner_callables or {}).values():
382
- walk_callable(ic)
383
- for cl in (c.inner_classes or {}).values():
384
- walk_class(cl)
385
-
386
- def walk_class(cl):
387
- declared.add(cl.signature)
388
- for m in (cl.methods or {}).values():
389
- walk_callable(m)
390
- for ic in (cl.inner_classes or {}).values():
391
- walk_class(ic)
392
-
393
- for mod in symbol_table.values():
394
- for c in (mod.functions or {}).values():
395
- walk_callable(c)
396
- for cl in (mod.classes or {}).values():
397
- walk_class(cl)
398
-
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)."""
399
386
  externals: Dict[str, PyExternalSymbol] = {}
400
- for edge in call_graph:
401
- for sig in (edge.source, edge.target):
402
- 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:
403
390
  continue
404
- module, name = sig.rsplit(".", 1) if "." in sig else (sig, sig)
405
- 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
+ )
406
398
  return externals
407
399
 
408
- def analyze(self) -> PyApplication:
409
- """Analyze the project and return a PyApplication with symbol table.
410
-
400
+ def analyze(self) -> Analysis:
401
+ """Analyze the project and return the v2 ``Analysis`` envelope.
402
+
411
403
  Uses caching to avoid re-analyzing unchanged files.
412
404
  """
413
405
  cache_file = self.cache_dir / "analysis_cache.json"
414
-
415
- # Try to load existing cached analysis
416
- cached_pyapplication = None
406
+
407
+ # Try to load existing cached analysis
408
+ cached = None
417
409
  if not self.rebuild_analysis and cache_file.exists():
418
410
  try:
419
- cached_pyapplication = self._load_pyapplication_from_cache(cache_file)
420
- 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")
421
414
  except Exception as e:
422
415
  logger.warning(f"Failed to load cache: {e}. Rebuilding analysis.")
423
- cached_pyapplication = None
416
+ cached = None
417
+
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
424
422
 
425
423
  # Build symbol table from cached application if available (if no available, the build a new one)
426
- 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 {})
427
425
 
428
426
  resolve_unresolved_constructors(symbol_table)
429
427
 
@@ -441,50 +439,154 @@ class Codeanalyzer:
441
439
 
442
440
  call_graph = filter_external_edges(call_graph, symbol_table)
443
441
 
444
- # Classify call-graph endpoints that are not declared in the symbol table
445
- # (imported library / builtin members) once, so the JSON and Neo4j backends
446
- # share one authoritative external-symbol set.
447
- external_symbols = self._compute_external_symbols(symbol_table, call_graph)
448
-
449
442
  # Recreate pyapplication
450
443
  app = (
451
444
  PyApplication.builder()
452
445
  .symbol_table(symbol_table)
453
446
  .call_graph(call_graph)
454
- .external_symbols(external_symbols)
455
447
  .build()
456
448
  )
457
-
458
- # Save to cache
459
- self._save_analysis_cache(app, cache_file)
460
-
461
- return app
462
449
 
463
- def _load_pyapplication_from_cache(self, cache_file: Path) -> PyApplication:
464
- """Load cached analysis from file.
465
-
450
+ # Every run re-resolves import spellings against the analyzed module
451
+ # set -- pure and cheap; cached modules from older caches default to
452
+ # resolved_module=None and get stamped here (issue #82).
453
+ resolve_imports(app, self.project_dir)
454
+
455
+ # Single choke point for provenance: every produced app (fresh symbol
456
+ # table or reused-from-cache) passes through here before being cached
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).
460
+ app.repository = repository_info(self.project_dir)
461
+
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
527
+
528
+ @staticmethod
529
+ def _cache_analyzer_matches(cached: Optional[Analysis], current_version: str) -> bool:
530
+ """A cache written by another analyzer version (or before versions were
531
+ recorded) may lack fields the current models populate — pydantic fills
532
+ silent defaults, which would masquerade as analyzed absence. The
533
+ analyzer identity lives on the envelope (keystone home)."""
534
+ return (
535
+ cached is not None
536
+ and cached.analyzer is not None
537
+ and cached.analyzer.version == current_version
538
+ )
539
+
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
+
466
549
  Args:
467
550
  cache_file: Path to the cache file
468
-
551
+
469
552
  Returns:
470
- PyApplication: The cached application data
553
+ Optional[Analysis]: The cached envelope, or ``None`` if the cache is
554
+ stale/incompatible and should be rebuilt.
471
555
  """
472
556
  with cache_file.open('r') as f:
473
557
  data = f.read()
474
- return model_validate_json(PyApplication, data)
475
-
476
- def _save_analysis_cache(self, app: PyApplication, cache_file: Path) -> None:
477
- """Save analysis to cache file.
478
-
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
+
479
581
  Args:
480
- app: The PyApplication to cache
582
+ analysis: The Analysis envelope to cache
481
583
  cache_file: Path to save the cache file
482
584
  """
483
585
  # Ensure cache directory exists
484
586
  cache_file.parent.mkdir(parents=True, exist_ok=True)
485
-
587
+
486
588
  with cache_file.open('w') as f:
487
- f.write(model_dump_json(app, indent=2))
589
+ f.write(model_dump_json(analysis, indent=2))
488
590
 
489
591
  logger.info(f"Analysis cached to {cache_file}")
490
592
 
@@ -552,9 +654,9 @@ class Codeanalyzer:
552
654
  if self.file_name is not None:
553
655
  single_file = self.project_dir / self.file_name
554
656
  logger.info(f"Analyzing single file: {single_file}")
555
-
657
+
556
658
  # Check if file is in cache and unchanged
557
- file_key = str(single_file)
659
+ file_key = str(single_file.relative_to(self.project_dir))
558
660
  if file_key in cached_symbol_table and not self.rebuild_analysis:
559
661
  # Compute file checksum to see if it changed
560
662
  if self._file_unchanged(single_file, cached_symbol_table[file_key]):
@@ -604,7 +706,7 @@ class Codeanalyzer:
604
706
  # Separate files into cached and new/changed
605
707
  files_to_process = []
606
708
  for py_file in py_files:
607
- file_key = str(py_file)
709
+ file_key = str(py_file.relative_to(self.project_dir))
608
710
  if file_key in cached_symbol_table and not self.rebuild_analysis:
609
711
  if self._file_unchanged(py_file, cached_symbol_table[file_key]):
610
712
  # Use cached version
@@ -632,7 +734,7 @@ class Codeanalyzer:
632
734
 
633
735
  with ProgressBar(len(py_files), "Building symbol table") as progress:
634
736
  for py_file in py_files:
635
- file_key = str(py_file)
737
+ file_key = str(py_file.relative_to(self.project_dir))
636
738
 
637
739
  # Check if file is cached and unchanged
638
740
  if file_key in cached_symbol_table and not self.rebuild_analysis:
@@ -668,7 +770,7 @@ class Codeanalyzer:
668
770
  """Build PyCG-resolved call edges.
669
771
 
670
772
  Runs PyCG's iterative name-pointer analysis over the whole project
671
- and returns edges with ``provenance=["pycg"]``. Falls back to an
773
+ and returns edges with ``prov=["pycg"]``. Falls back to an
672
774
  empty list and logs a warning on any failure so the caller can
673
775
  continue with Jedi-only edges.
674
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