codeanalyzer-python 0.2.0__py3-none-any.whl → 0.3.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 (29) hide show
  1. codeanalyzer/__main__.py +99 -6
  2. codeanalyzer/core.py +192 -215
  3. codeanalyzer/neo4j/bolt.py +13 -2
  4. codeanalyzer/neo4j/catalog.py +2 -2
  5. codeanalyzer/neo4j/project.py +18 -10
  6. codeanalyzer/neo4j/rows.py +10 -5
  7. codeanalyzer/options/__init__.py +2 -2
  8. codeanalyzer/options/options.py +21 -1
  9. codeanalyzer/schema/__init__.py +2 -0
  10. codeanalyzer/schema/py_schema.py +16 -1
  11. codeanalyzer/semantic_analysis/call_graph.py +24 -3
  12. codeanalyzer/semantic_analysis/pycg/__init__.py +20 -0
  13. codeanalyzer/semantic_analysis/pycg/pycg_analysis.py +1054 -0
  14. codeanalyzer/semantic_analysis/{codeql/__init__.py → pycg/pycg_exceptions.py} +5 -8
  15. codeanalyzer/semantic_analysis/pycg/shard_planner.py +401 -0
  16. codeanalyzer/utils/logging.py +5 -2
  17. codeanalyzer/utils/progress_bar.py +15 -7
  18. codeanalyzer_python-0.3.0.dist-info/METADATA +550 -0
  19. codeanalyzer_python-0.3.0.dist-info/RECORD +38 -0
  20. codeanalyzer/semantic_analysis/codeql/codeql_analysis.py +0 -382
  21. codeanalyzer/semantic_analysis/codeql/codeql_exceptions.py +0 -12
  22. codeanalyzer/semantic_analysis/codeql/codeql_loader.py +0 -91
  23. codeanalyzer/semantic_analysis/codeql/codeql_query_runner.py +0 -185
  24. codeanalyzer_python-0.2.0.dist-info/METADATA +0 -393
  25. codeanalyzer_python-0.2.0.dist-info/RECORD +0 -39
  26. {codeanalyzer_python-0.2.0.dist-info → codeanalyzer_python-0.3.0.dist-info}/WHEEL +0 -0
  27. {codeanalyzer_python-0.2.0.dist-info → codeanalyzer_python-0.3.0.dist-info}/entry_points.txt +0 -0
  28. {codeanalyzer_python-0.2.0.dist-info → codeanalyzer_python-0.3.0.dist-info}/licenses/LICENSE +0 -0
  29. {codeanalyzer_python-0.2.0.dist-info → codeanalyzer_python-0.3.0.dist-info}/licenses/NOTICE +0 -0
@@ -83,7 +83,11 @@ class RowBuilder:
83
83
  self._nodes: Dict[str, NodeRow] = {} # key: f"{labels[0]} {value}"
84
84
  self._edges: List[EdgeRow] = []
85
85
  self._deferred: List[EdgeRow] = [] # edges gated against node existence at finish()
86
- self._keys: set = set() # every node value seen, for resolved-gating
86
+ # (merge_label, value) of every node seen, for resolved-gating. Keyed by
87
+ # label too so a :PyPackage name can't shadow a :PySymbol signature (and
88
+ # vice versa) — otherwise a call to an imported module name like ``os``
89
+ # resolves to a :PySymbol node that was never created and the edge is lost.
90
+ self._keys: set = set()
87
91
 
88
92
  def node(self, labels: List[str], key_prop: str, value: str, props: Props) -> NodeRef:
89
93
  """Upsert a node. Re-seeing the same ``(labels[0], value)`` merges props
@@ -98,7 +102,7 @@ class RowBuilder:
98
102
  existing.labels.append(label)
99
103
  else:
100
104
  self._nodes[node_id] = NodeRow(list(labels), key_prop, value, dict(props))
101
- self._keys.add(value)
105
+ self._keys.add((labels[0], value))
102
106
  return NodeRef(labels[0], key_prop, value)
103
107
 
104
108
  def edge(self, type_: str, from_ref: NodeRef, to_ref: NodeRef, props: Optional[Props] = None) -> None:
@@ -121,12 +125,13 @@ class RowBuilder:
121
125
  )
122
126
  )
123
127
 
124
- def has_key(self, value: str) -> bool:
125
- return value in self._keys
128
+ def has_key(self, label: str, value: str) -> bool:
129
+ """Whether a node with this ``(merge_label, value)`` identity was emitted."""
130
+ return (label, value) in self._keys
126
131
 
127
132
  def finish(self) -> GraphRows:
128
133
  for e in self._deferred:
129
- if e.to_ref.value in self._keys:
134
+ if (e.to_ref.label, e.to_ref.value) in self._keys:
130
135
  self._edges.append(e)
131
136
  nodes = sorted(self._nodes.values(), key=lambda n: f"{n.labels[0]} {n.value}")
132
137
  edges = sorted(self._edges, key=lambda e: f"{e.type} {e.from_ref.value} {e.to_ref.value}")
@@ -1,3 +1,3 @@
1
- from .options import AnalysisOptions, EmitTarget, OutputFormat
1
+ from .options import AnalysisOptions, EmitTarget, OutputFormat, ShardStrategy
2
2
 
3
- __all__ = ["AnalysisOptions", "EmitTarget", "OutputFormat"]
3
+ __all__ = ["AnalysisOptions", "EmitTarget", "OutputFormat", "ShardStrategy"]
@@ -23,6 +23,20 @@ class EmitTarget(str, Enum):
23
23
  SCHEMA = "schema"
24
24
 
25
25
 
26
+ class ShardStrategy(str, Enum):
27
+ """How ``--pycg-shard`` groups files into shards (level 2 only).
28
+
29
+ - ``jedi`` : partition the Jedi module-dependency graph (strongly-
30
+ connected-component condensation + Louvain) so tightly-
31
+ coupled modules co-compute and few call edges are severed
32
+ between shards. Import cycles are never split.
33
+ - ``package`` : legacy one-shard-per-package-directory grouping.
34
+ """
35
+
36
+ JEDI = "jedi"
37
+ PACKAGE = "package"
38
+
39
+
26
40
  @dataclass
27
41
  class AnalysisOptions:
28
42
  input: Path
@@ -34,11 +48,17 @@ class AnalysisOptions:
34
48
  neo4j_user: str = "neo4j"
35
49
  neo4j_password: str = "neo4j"
36
50
  neo4j_database: Optional[str] = None
37
- using_codeql: bool = False
51
+ analysis_level: int = 1
38
52
  using_ray: bool = False
39
53
  rebuild_analysis: bool = False
40
54
  skip_tests: bool = True
55
+ no_venv: bool = False
41
56
  file_name: Optional[Path] = None
42
57
  cache_dir: Optional[Path] = None
43
58
  clear_cache: bool = False
44
59
  verbosity: int = 0
60
+ pycg_shard: bool = False
61
+ pycg_shard_ceiling: int = 100
62
+ pycg_shard_timeout: int = 120
63
+ pycg_shard_strategy: ShardStrategy = ShardStrategy.JEDI
64
+ pycg_max_iter: int = 50
@@ -8,6 +8,7 @@ from .py_schema import (
8
8
  PyClass,
9
9
  PyClassAttribute,
10
10
  PyComment,
11
+ PyExternalSymbol,
11
12
  PyImport,
12
13
  PyModule,
13
14
  PyVariableDeclaration,
@@ -15,6 +16,7 @@ from .py_schema import (
15
16
 
16
17
  __all__ = [
17
18
  "PyApplication",
19
+ "PyExternalSymbol",
18
20
  "PyImport",
19
21
  "PyComment",
20
22
  "PyModule",
@@ -355,7 +355,18 @@ class PyCallEdge(BaseModel):
355
355
  target: str # callee's PyCallable.signature
356
356
  type: Literal["CALL_DEP"] = "CALL_DEP"
357
357
  weight: int = 1
358
- provenance: List[Literal["jedi", "codeql", "joern"]] = []
358
+ provenance: List[Literal["jedi", "pycg", "joern"]] = []
359
+
360
+
361
+ @builder
362
+ @msgpk
363
+ class PyExternalSymbol(BaseModel):
364
+ """A call-graph target outside the analyzed project -- an imported library or
365
+ builtin member. Mirrors codeanalyzer-typescript's ``TSExternalSymbol`` and is
366
+ keyed in ``PyApplication.external_symbols`` by its call-graph signature."""
367
+
368
+ name: str # the member/short name, e.g. "get" for "requests.get"
369
+ module: Optional[str] = None # best-effort owning module, e.g. "requests"
359
370
 
360
371
 
361
372
  @builder
@@ -365,3 +376,7 @@ class PyApplication(BaseModel):
365
376
 
366
377
  symbol_table: Dict[str, PyModule]
367
378
  call_graph: List[PyCallEdge] = []
379
+ # Call-graph endpoints not declared in the symbol table (imported library /
380
+ # builtin members), keyed by signature. Populated by the analyzer so every
381
+ # backend (JSON and Neo4j) shares one authoritative external-symbol set.
382
+ external_symbols: Dict[str, PyExternalSymbol] = {}
@@ -173,7 +173,7 @@ def jedi_call_graph_edges(
173
173
 
174
174
  Edges are coalesced on ``(source, target)``: ``weight`` is the count of
175
175
  matching sites. Provenance is always ``["jedi"]``; combine with
176
- CodeQL-derived edges via ``merge_edges``.
176
+ PyCG-derived edges via ``merge_edges``.
177
177
  """
178
178
  counts: Counter = Counter()
179
179
  for caller in iter_callables_in_symbol_table(symbol_table):
@@ -191,7 +191,7 @@ def jedi_call_graph_edges(
191
191
  def resolve_unresolved_constructors(symbol_table: Dict[str, PyModule]) -> int:
192
192
  """Fill in ``PyCallsite.callee_signature`` for unresolved constructor sites.
193
193
 
194
- When both Jedi and CodeQL fail to resolve a constructor call (commonly
194
+ When Jedi fails to resolve a constructor call (commonly
195
195
  for classes nested inside functions or methods, where static-analysis
196
196
  points-to is weakest), Jedi still flags the site as
197
197
  ``is_constructor_call=True`` with ``method_name`` set to the class's
@@ -246,12 +246,33 @@ def resolve_unresolved_constructors(symbol_table: Dict[str, PyModule]) -> int:
246
246
  return resolved
247
247
 
248
248
 
249
+ def filter_external_edges(
250
+ edges: List[PyCallEdge],
251
+ symbol_table: Dict[str, PyModule],
252
+ ) -> List[PyCallEdge]:
253
+ """Remove edges where both source and target are outside the app namespace.
254
+
255
+ Edges where an app callable calls a library function (or vice-versa) are
256
+ retained; only lib→lib edges are dropped. The app symbol set is built by
257
+ walking every callable in the symbol table recursively (including nested
258
+ functions and closures via ``inner_callables``) plus every class, so
259
+ PyCG-discovered closure nodes are correctly recognised as app symbols.
260
+ """
261
+ app_symbols: set = {c.signature for c in iter_callables_in_symbol_table(symbol_table)}
262
+ app_symbols.update(cls.signature for cls in iter_classes_in_symbol_table(symbol_table))
263
+
264
+ return [
265
+ e for e in edges
266
+ if e.source in app_symbols or e.target in app_symbols
267
+ ]
268
+
269
+
249
270
  def merge_edges(*edge_lists: list) -> list:
250
271
  """Merge multiple ``List[PyCallEdge]`` into one.
251
272
 
252
273
  Edges with the same ``(source, target)`` are coalesced: weights sum,
253
274
  provenance is the sorted union. Useful for combining edges produced
254
- by different backends (e.g. Jedi + CodeQL).
275
+ by different backends (e.g. Jedi + PyCG).
255
276
  """
256
277
  by_key: Dict[Tuple[str, str], PyCallEdge] = {}
257
278
  for edges in edge_lists:
@@ -0,0 +1,20 @@
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
+ from codeanalyzer.semantic_analysis.pycg.pycg_analysis import PyCG
18
+ from codeanalyzer.semantic_analysis.pycg.pycg_exceptions import PyCGExceptions
19
+
20
+ __all__ = ["PyCG", "PyCGExceptions"]