codeanalyzer-python 0.3.0__tar.gz → 0.3.2__tar.gz
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_python-0.3.0 → codeanalyzer_python-0.3.2}/.gitignore +3 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/PKG-INFO +23 -25
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/README.md +21 -18
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/__main__.py +26 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/core.py +34 -3
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/neo4j/__init__.py +1 -1
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/neo4j/emit.py +2 -2
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/neo4j/project.py +84 -18
- codeanalyzer_python-0.3.0/codeanalyzer/neo4j/catalog.py → codeanalyzer_python-0.3.2/codeanalyzer/neo4j/schema.py +58 -18
- codeanalyzer_python-0.3.2/codeanalyzer/provenance.py +61 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/schema/py_schema.py +38 -0
- codeanalyzer_python-0.3.2/codeanalyzer/syntactic_analysis/import_resolver.py +67 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/syntactic_analysis/symbol_table_builder.py +74 -10
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/pyproject.toml +1 -8
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/schema.neo4j.json +19 -10
- codeanalyzer_python-0.3.0/codeanalyzer/neo4j/schema.py +0 -39
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/LICENSE +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/NOTICE +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/__init__.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/config/__init__.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/config/config.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/jedi/__init__.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/jedi/jedi.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/neo4j/bolt.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/neo4j/cypher.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/neo4j/rows.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/options/__init__.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/options/options.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/py.typed +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/schema/__init__.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/semantic_analysis/__init__.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/semantic_analysis/call_graph.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/semantic_analysis/pycg/__init__.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/semantic_analysis/pycg/pycg_analysis.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/semantic_analysis/pycg/pycg_exceptions.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/semantic_analysis/pycg/shard_planner.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/syntactic_analysis/__init__.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/syntactic_analysis/exceptions.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/utils/__init__.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/utils/logging.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/codeanalyzer/utils/progress_bar.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/schema-uml.drawio +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/test/fixtures/whole_applications/flask/README.md +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/test/fixtures/whole_applications/flask/examples/celery/README.md +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/test/fixtures/whole_applications/flask/requirements-skip/README.md +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/test/fixtures/whole_applications/flask/src/flask/sansio/README.md +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/test/fixtures/whole_applications/requests/LICENSE +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/test/fixtures/whole_applications/requests/NOTICE +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/test/fixtures/whole_applications/requests/README.md +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/test/fixtures/whole_applications/requests/docs/_themes/LICENSE +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/test/fixtures/whole_applications/requests/ext/LICENSE +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/test/fixtures/whole_applications/xarray/LICENSE +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/test/fixtures/whole_applications/xarray/README.md +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/test/fixtures/whole_applications/xarray/properties/README.md +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/test/fixtures/whole_applications/xarray/xarray/datatree_/LICENSE +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/test/fixtures/whole_applications/xarray/xarray/datatree_/README.md +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-0.3.2}/test/fixtures/whole_applications/xarray/xarray/datatree_/docs/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: codeanalyzer-python
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Static Analysis on Python source code using Jedi, CodeQL and Treesitter — emits analysis.json or a Neo4j property graph.
|
|
5
5
|
Author-email: Rahul Krishna <i.m.ralk@gmail.com>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -12,12 +12,7 @@ Requires-Dist: msgpack<1.0.7,>=1.0.0; python_version < '3.11'
|
|
|
12
12
|
Requires-Dist: msgpack<2.0.0,>=1.0.7; python_version >= '3.11'
|
|
13
13
|
Requires-Dist: networkx<3.2.0,>=2.6.0; python_version < '3.11'
|
|
14
14
|
Requires-Dist: networkx<4.0.0,>=3.0.0; python_version >= '3.11'
|
|
15
|
-
Requires-Dist: numpy<1.24.0,>=1.21.0; python_version < '3.11'
|
|
16
|
-
Requires-Dist: numpy<2.0.0,>=1.24.0; python_version >= '3.11' and python_version < '3.12'
|
|
17
|
-
Requires-Dist: numpy<2.0.0,>=1.26.0; python_version >= '3.12'
|
|
18
15
|
Requires-Dist: packaging>=25.0
|
|
19
|
-
Requires-Dist: pandas<2.0.0,>=1.3.0; python_version < '3.11'
|
|
20
|
-
Requires-Dist: pandas<3.0.0,>=2.0.0; python_version >= '3.11'
|
|
21
16
|
Requires-Dist: pycg>=0.0.6
|
|
22
17
|
Requires-Dist: pydantic<2.0.0,>=1.8.0; python_version < '3.11'
|
|
23
18
|
Requires-Dist: pydantic<3.0.0,>=2.0.0; python_version >= '3.11'
|
|
@@ -181,19 +176,22 @@ $ canpy --help
|
|
|
181
176
|
Static Analysis on Python source code using Jedi, PyCG and Tree sitter.
|
|
182
177
|
|
|
183
178
|
╭─ Options ────────────────────────────────────────────────────────────────────╮
|
|
184
|
-
│ --
|
|
179
|
+
│ --version Show the canpy │
|
|
180
|
+
│ version and │
|
|
181
|
+
│ exit. │
|
|
182
|
+
│ --input -i <path> Path to the │
|
|
185
183
|
│ project root │
|
|
186
184
|
│ directory (not │
|
|
187
185
|
│ required for │
|
|
188
186
|
│ --emit schema). │
|
|
189
|
-
│ --output -o
|
|
187
|
+
│ --output -o <path> Output directory │
|
|
190
188
|
│ for artifacts. │
|
|
191
|
-
│ --format -f
|
|
189
|
+
│ --format -f <json|msgpack> Output format │
|
|
192
190
|
│ for --emit json: │
|
|
193
191
|
│ json or msgpack. │
|
|
194
192
|
│ [default: json] │
|
|
195
|
-
│ --emit
|
|
196
|
-
│ ma
|
|
193
|
+
│ --emit <json|neo4j|sche Output target: │
|
|
194
|
+
│ ma> json │
|
|
197
195
|
│ (analysis.json, │
|
|
198
196
|
│ default) | neo4j │
|
|
199
197
|
│ (graph.cypher or │
|
|
@@ -203,13 +201,13 @@ $ canpy --help
|
|
|
203
201
|
│ schema.json │
|
|
204
202
|
│ contract). │
|
|
205
203
|
│ [default: json] │
|
|
206
|
-
│ --app-name
|
|
204
|
+
│ --app-name <str> Logical │
|
|
207
205
|
│ application name │
|
|
208
206
|
│ for the graph │
|
|
209
207
|
│ :PyApplication │
|
|
210
208
|
│ anchor (default: │
|
|
211
209
|
│ input dir name). │
|
|
212
|
-
│ --neo4j-uri
|
|
210
|
+
│ --neo4j-uri <str> Push the graph │
|
|
213
211
|
│ to a live Neo4j │
|
|
214
212
|
│ over Bolt │
|
|
215
213
|
│ (incremental); │
|
|
@@ -217,11 +215,11 @@ $ canpy --help
|
|
|
217
215
|
│ graph.cypher. │
|
|
218
216
|
│ [env var: │
|
|
219
217
|
│ NEO4J_URI] │
|
|
220
|
-
│ --neo4j-user
|
|
218
|
+
│ --neo4j-user <str> Neo4j username. │
|
|
221
219
|
│ [env var: │
|
|
222
220
|
│ NEO4J_USERNAME] │
|
|
223
221
|
│ [default: neo4j] │
|
|
224
|
-
│ --neo4j-password
|
|
222
|
+
│ --neo4j-password <str> Neo4j password. │
|
|
225
223
|
│ Prefer the env │
|
|
226
224
|
│ var over the │
|
|
227
225
|
│ flag (the flag │
|
|
@@ -231,12 +229,12 @@ $ canpy --help
|
|
|
231
229
|
│ [env var: │
|
|
232
230
|
│ NEO4J_PASSWORD] │
|
|
233
231
|
│ [default: neo4j] │
|
|
234
|
-
│ --neo4j-database
|
|
232
|
+
│ --neo4j-database <str> Neo4j database │
|
|
235
233
|
│ name (default: │
|
|
236
234
|
│ server default). │
|
|
237
235
|
│ [env var: │
|
|
238
236
|
│ NEO4J_DATABASE] │
|
|
239
|
-
│ --analysis-level -a
|
|
237
|
+
│ --analysis-level -a <int range> Analysis depth: │
|
|
240
238
|
│ [1<=x<=2] 1=symbol │
|
|
241
239
|
│ table+Jedi call │
|
|
242
240
|
│ graph, 2=+PyCG │
|
|
@@ -266,12 +264,12 @@ $ canpy --help
|
|
|
266
264
|
│ environment │
|
|
267
265
|
│ instead. │
|
|
268
266
|
│ [default: venv] │
|
|
269
|
-
│ --file-name
|
|
267
|
+
│ --file-name <path> Analyze only the │
|
|
270
268
|
│ specified file │
|
|
271
269
|
│ (relative to │
|
|
272
270
|
│ input │
|
|
273
271
|
│ directory). │
|
|
274
|
-
│ --cache-dir -c
|
|
272
|
+
│ --cache-dir -c <path> Directory to │
|
|
275
273
|
│ store analysis │
|
|
276
274
|
│ cache. Defaults │
|
|
277
275
|
│ to │
|
|
@@ -285,7 +283,7 @@ $ canpy --help
|
|
|
285
283
|
│ retained. │
|
|
286
284
|
│ [default: │
|
|
287
285
|
│ keep-cache] │
|
|
288
|
-
│ -v
|
|
286
|
+
│ -v <int> Increase │
|
|
289
287
|
│ verbosity: -v, │
|
|
290
288
|
│ -vv, -vvv │
|
|
291
289
|
│ [default: 0] │
|
|
@@ -312,7 +310,7 @@ $ canpy --help
|
|
|
312
310
|
│ Jedi-only edges. │
|
|
313
311
|
│ [default: │
|
|
314
312
|
│ no-pycg-shard] │
|
|
315
|
-
│ --pycg-shard-cei…
|
|
313
|
+
│ --pycg-shard-cei… <int range> Maximum files │
|
|
316
314
|
│ [x>=1] per shard when │
|
|
317
315
|
│ --pycg-shard is │
|
|
318
316
|
│ active (default │
|
|
@@ -334,7 +332,7 @@ $ canpy --help
|
|
|
334
332
|
│ heavy import │
|
|
335
333
|
│ graphs. │
|
|
336
334
|
│ [default: 100] │
|
|
337
|
-
│ --pycg-shard-tim…
|
|
335
|
+
│ --pycg-shard-tim… <int range> Per-shard │
|
|
338
336
|
│ [x>=0] wall-clock │
|
|
339
337
|
│ timeout in │
|
|
340
338
|
│ seconds when │
|
|
@@ -362,7 +360,7 @@ $ canpy --help
|
|
|
362
360
|
│ ignored on │
|
|
363
361
|
│ Windows. │
|
|
364
362
|
│ [default: 120] │
|
|
365
|
-
│ --pycg-shard-str…
|
|
363
|
+
│ --pycg-shard-str… <jedi|package> How --pycg-shard │
|
|
366
364
|
│ groups files │
|
|
367
365
|
│ (level 2 only). │
|
|
368
366
|
│ 'jedi' (default) │
|
|
@@ -384,7 +382,7 @@ $ canpy --help
|
|
|
384
382
|
│ one-shard-per-p… │
|
|
385
383
|
│ grouping. │
|
|
386
384
|
│ [default: jedi] │
|
|
387
|
-
│ --pycg-max-iter
|
|
385
|
+
│ --pycg-max-iter <int range> Cap on PyCG's │
|
|
388
386
|
│ [x>=-1] fixpoint passes │
|
|
389
387
|
│ per │
|
|
390
388
|
│ shard/project │
|
|
@@ -144,19 +144,22 @@ $ canpy --help
|
|
|
144
144
|
Static Analysis on Python source code using Jedi, PyCG and Tree sitter.
|
|
145
145
|
|
|
146
146
|
╭─ Options ────────────────────────────────────────────────────────────────────╮
|
|
147
|
-
│ --
|
|
147
|
+
│ --version Show the canpy │
|
|
148
|
+
│ version and │
|
|
149
|
+
│ exit. │
|
|
150
|
+
│ --input -i <path> Path to the │
|
|
148
151
|
│ project root │
|
|
149
152
|
│ directory (not │
|
|
150
153
|
│ required for │
|
|
151
154
|
│ --emit schema). │
|
|
152
|
-
│ --output -o
|
|
155
|
+
│ --output -o <path> Output directory │
|
|
153
156
|
│ for artifacts. │
|
|
154
|
-
│ --format -f
|
|
157
|
+
│ --format -f <json|msgpack> Output format │
|
|
155
158
|
│ for --emit json: │
|
|
156
159
|
│ json or msgpack. │
|
|
157
160
|
│ [default: json] │
|
|
158
|
-
│ --emit
|
|
159
|
-
│ ma
|
|
161
|
+
│ --emit <json|neo4j|sche Output target: │
|
|
162
|
+
│ ma> json │
|
|
160
163
|
│ (analysis.json, │
|
|
161
164
|
│ default) | neo4j │
|
|
162
165
|
│ (graph.cypher or │
|
|
@@ -166,13 +169,13 @@ $ canpy --help
|
|
|
166
169
|
│ schema.json │
|
|
167
170
|
│ contract). │
|
|
168
171
|
│ [default: json] │
|
|
169
|
-
│ --app-name
|
|
172
|
+
│ --app-name <str> Logical │
|
|
170
173
|
│ application name │
|
|
171
174
|
│ for the graph │
|
|
172
175
|
│ :PyApplication │
|
|
173
176
|
│ anchor (default: │
|
|
174
177
|
│ input dir name). │
|
|
175
|
-
│ --neo4j-uri
|
|
178
|
+
│ --neo4j-uri <str> Push the graph │
|
|
176
179
|
│ to a live Neo4j │
|
|
177
180
|
│ over Bolt │
|
|
178
181
|
│ (incremental); │
|
|
@@ -180,11 +183,11 @@ $ canpy --help
|
|
|
180
183
|
│ graph.cypher. │
|
|
181
184
|
│ [env var: │
|
|
182
185
|
│ NEO4J_URI] │
|
|
183
|
-
│ --neo4j-user
|
|
186
|
+
│ --neo4j-user <str> Neo4j username. │
|
|
184
187
|
│ [env var: │
|
|
185
188
|
│ NEO4J_USERNAME] │
|
|
186
189
|
│ [default: neo4j] │
|
|
187
|
-
│ --neo4j-password
|
|
190
|
+
│ --neo4j-password <str> Neo4j password. │
|
|
188
191
|
│ Prefer the env │
|
|
189
192
|
│ var over the │
|
|
190
193
|
│ flag (the flag │
|
|
@@ -194,12 +197,12 @@ $ canpy --help
|
|
|
194
197
|
│ [env var: │
|
|
195
198
|
│ NEO4J_PASSWORD] │
|
|
196
199
|
│ [default: neo4j] │
|
|
197
|
-
│ --neo4j-database
|
|
200
|
+
│ --neo4j-database <str> Neo4j database │
|
|
198
201
|
│ name (default: │
|
|
199
202
|
│ server default). │
|
|
200
203
|
│ [env var: │
|
|
201
204
|
│ NEO4J_DATABASE] │
|
|
202
|
-
│ --analysis-level -a
|
|
205
|
+
│ --analysis-level -a <int range> Analysis depth: │
|
|
203
206
|
│ [1<=x<=2] 1=symbol │
|
|
204
207
|
│ table+Jedi call │
|
|
205
208
|
│ graph, 2=+PyCG │
|
|
@@ -229,12 +232,12 @@ $ canpy --help
|
|
|
229
232
|
│ environment │
|
|
230
233
|
│ instead. │
|
|
231
234
|
│ [default: venv] │
|
|
232
|
-
│ --file-name
|
|
235
|
+
│ --file-name <path> Analyze only the │
|
|
233
236
|
│ specified file │
|
|
234
237
|
│ (relative to │
|
|
235
238
|
│ input │
|
|
236
239
|
│ directory). │
|
|
237
|
-
│ --cache-dir -c
|
|
240
|
+
│ --cache-dir -c <path> Directory to │
|
|
238
241
|
│ store analysis │
|
|
239
242
|
│ cache. Defaults │
|
|
240
243
|
│ to │
|
|
@@ -248,7 +251,7 @@ $ canpy --help
|
|
|
248
251
|
│ retained. │
|
|
249
252
|
│ [default: │
|
|
250
253
|
│ keep-cache] │
|
|
251
|
-
│ -v
|
|
254
|
+
│ -v <int> Increase │
|
|
252
255
|
│ verbosity: -v, │
|
|
253
256
|
│ -vv, -vvv │
|
|
254
257
|
│ [default: 0] │
|
|
@@ -275,7 +278,7 @@ $ canpy --help
|
|
|
275
278
|
│ Jedi-only edges. │
|
|
276
279
|
│ [default: │
|
|
277
280
|
│ no-pycg-shard] │
|
|
278
|
-
│ --pycg-shard-cei…
|
|
281
|
+
│ --pycg-shard-cei… <int range> Maximum files │
|
|
279
282
|
│ [x>=1] per shard when │
|
|
280
283
|
│ --pycg-shard is │
|
|
281
284
|
│ active (default │
|
|
@@ -297,7 +300,7 @@ $ canpy --help
|
|
|
297
300
|
│ heavy import │
|
|
298
301
|
│ graphs. │
|
|
299
302
|
│ [default: 100] │
|
|
300
|
-
│ --pycg-shard-tim…
|
|
303
|
+
│ --pycg-shard-tim… <int range> Per-shard │
|
|
301
304
|
│ [x>=0] wall-clock │
|
|
302
305
|
│ timeout in │
|
|
303
306
|
│ seconds when │
|
|
@@ -325,7 +328,7 @@ $ canpy --help
|
|
|
325
328
|
│ ignored on │
|
|
326
329
|
│ Windows. │
|
|
327
330
|
│ [default: 120] │
|
|
328
|
-
│ --pycg-shard-str…
|
|
331
|
+
│ --pycg-shard-str… <jedi|package> How --pycg-shard │
|
|
329
332
|
│ groups files │
|
|
330
333
|
│ (level 2 only). │
|
|
331
334
|
│ 'jedi' (default) │
|
|
@@ -347,7 +350,7 @@ $ canpy --help
|
|
|
347
350
|
│ one-shard-per-p… │
|
|
348
351
|
│ grouping. │
|
|
349
352
|
│ [default: jedi] │
|
|
350
|
-
│ --pycg-max-iter
|
|
353
|
+
│ --pycg-max-iter <int range> Cap on PyCG's │
|
|
351
354
|
│ [x>=-1] fixpoint passes │
|
|
352
355
|
│ per │
|
|
353
356
|
│ shard/project │
|
|
@@ -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(
|
|
@@ -26,9 +26,11 @@ from codeanalyzer.semantic_analysis.call_graph import (
|
|
|
26
26
|
)
|
|
27
27
|
from codeanalyzer.semantic_analysis.pycg import PyCG, PyCGExceptions
|
|
28
28
|
from codeanalyzer.syntactic_analysis.exceptions import SymbolTableBuilderRayError
|
|
29
|
+
from codeanalyzer.syntactic_analysis.import_resolver import resolve_imports
|
|
29
30
|
from codeanalyzer.syntactic_analysis.symbol_table_builder import SymbolTableBuilder
|
|
30
31
|
from codeanalyzer.utils import ProgressBar
|
|
31
32
|
from codeanalyzer.options import AnalysisOptions
|
|
33
|
+
from codeanalyzer.provenance import analyzer_info, repository_info
|
|
32
34
|
|
|
33
35
|
@ray.remote
|
|
34
36
|
def _process_file_with_ray(py_file: Union[Path, str], project_dir: Union[Path, str], virtualenv: Union[Path, str, None]) -> Dict[str, PyModule]:
|
|
@@ -411,8 +413,8 @@ class Codeanalyzer:
|
|
|
411
413
|
Uses caching to avoid re-analyzing unchanged files.
|
|
412
414
|
"""
|
|
413
415
|
cache_file = self.cache_dir / "analysis_cache.json"
|
|
414
|
-
|
|
415
|
-
# Try to load existing cached analysis
|
|
416
|
+
|
|
417
|
+
# Try to load existing cached analysis
|
|
416
418
|
cached_pyapplication = None
|
|
417
419
|
if not self.rebuild_analysis and cache_file.exists():
|
|
418
420
|
try:
|
|
@@ -422,6 +424,12 @@ class Codeanalyzer:
|
|
|
422
424
|
logger.warning(f"Failed to load cache: {e}. Rebuilding analysis.")
|
|
423
425
|
cached_pyapplication = None
|
|
424
426
|
|
|
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
|
|
432
|
+
|
|
425
433
|
# Build symbol table from cached application if available (if no available, the build a new one)
|
|
426
434
|
symbol_table = self._build_symbol_table(cached_pyapplication.symbol_table if cached_pyapplication else {})
|
|
427
435
|
|
|
@@ -454,12 +462,35 @@ class Codeanalyzer:
|
|
|
454
462
|
.external_symbols(external_symbols)
|
|
455
463
|
.build()
|
|
456
464
|
)
|
|
457
|
-
|
|
465
|
+
|
|
466
|
+
# Every run re-resolves import spellings against the analyzed module
|
|
467
|
+
# set -- pure and cheap; cached modules from older caches default to
|
|
468
|
+
# resolved_module=None and get stamped here (issue #82).
|
|
469
|
+
resolve_imports(app, self.project_dir)
|
|
470
|
+
|
|
471
|
+
# Single choke point for provenance: every produced app (fresh symbol
|
|
472
|
+
# 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)
|
|
476
|
+
app.repository = repository_info(self.project_dir)
|
|
477
|
+
|
|
458
478
|
# Save to cache
|
|
459
479
|
self._save_analysis_cache(app, cache_file)
|
|
460
480
|
|
|
461
481
|
return app
|
|
462
482
|
|
|
483
|
+
@staticmethod
|
|
484
|
+
def _cache_analyzer_matches(cached_app: Optional[PyApplication], current_version: str) -> bool:
|
|
485
|
+
"""A cache written by another analyzer version (or before versions were
|
|
486
|
+
recorded) may lack fields the current models populate — pydantic fills
|
|
487
|
+
silent defaults, which would masquerade as analyzed absence."""
|
|
488
|
+
return (
|
|
489
|
+
cached_app is not None
|
|
490
|
+
and cached_app.analyzer is not None
|
|
491
|
+
and cached_app.analyzer.version == current_version
|
|
492
|
+
)
|
|
493
|
+
|
|
463
494
|
def _load_pyapplication_from_cache(self, cache_file: Path) -> PyApplication:
|
|
464
495
|
"""Load cached analysis from file.
|
|
465
496
|
|
|
@@ -19,7 +19,7 @@ plus the two writers (cypher snapshot / bolt incremental). Nothing here runs
|
|
|
19
19
|
unless ``--emit neo4j`` (or ``--emit schema``) is selected.
|
|
20
20
|
"""
|
|
21
21
|
from codeanalyzer.neo4j.bolt import BoltConfig, bolt_writer
|
|
22
|
-
from codeanalyzer.neo4j.
|
|
22
|
+
from codeanalyzer.neo4j.schema import (
|
|
23
23
|
MARKER_LABELS,
|
|
24
24
|
NODE_LABELS,
|
|
25
25
|
REL_TYPES,
|
|
@@ -28,7 +28,7 @@ from pathlib import Path
|
|
|
28
28
|
from typing import Optional
|
|
29
29
|
|
|
30
30
|
from codeanalyzer.neo4j.bolt import BoltConfig, bolt_writer
|
|
31
|
-
from codeanalyzer.neo4j.
|
|
31
|
+
from codeanalyzer.neo4j.schema import build_schema_document
|
|
32
32
|
from codeanalyzer.neo4j.cypher import render_cypher
|
|
33
33
|
from codeanalyzer.neo4j.project import project
|
|
34
34
|
from codeanalyzer.options import AnalysisOptions
|
|
@@ -38,7 +38,7 @@ from codeanalyzer.utils import logger
|
|
|
38
38
|
|
|
39
39
|
def emit_schema(output: Optional[Path]) -> None:
|
|
40
40
|
"""Emit the Neo4j schema contract (``schema.json``) — a static artifact derived
|
|
41
|
-
from the in-repo
|
|
41
|
+
from the in-repo schema, independent of any analyzed project. With no
|
|
42
42
|
``output`` it prints to stdout."""
|
|
43
43
|
doc = json.dumps(build_schema_document(), indent=2) + "\n"
|
|
44
44
|
if output is None:
|
|
@@ -36,7 +36,7 @@ import json
|
|
|
36
36
|
from pathlib import Path
|
|
37
37
|
from typing import Any, List, Optional
|
|
38
38
|
|
|
39
|
-
from codeanalyzer.neo4j.
|
|
39
|
+
from codeanalyzer.neo4j.schema import SCHEMA_VERSION
|
|
40
40
|
from codeanalyzer.neo4j.rows import GraphRows, NodeRef, Props, RowBuilder, prune
|
|
41
41
|
from codeanalyzer.schema import (
|
|
42
42
|
PyApplication,
|
|
@@ -53,10 +53,26 @@ from codeanalyzer.schema.py_schema import PyCallsite
|
|
|
53
53
|
def project(app: PyApplication, app_name: str) -> GraphRows:
|
|
54
54
|
b = RowBuilder()
|
|
55
55
|
|
|
56
|
-
app_ref = b.node(
|
|
56
|
+
app_ref = b.node(
|
|
57
|
+
["PyApplication"],
|
|
58
|
+
"name",
|
|
59
|
+
app_name,
|
|
60
|
+
prune(
|
|
61
|
+
{
|
|
62
|
+
"schema_version": SCHEMA_VERSION,
|
|
63
|
+
"analyzer_name": app.analyzer.name if app.analyzer else None,
|
|
64
|
+
"analyzer_version": app.analyzer.version if app.analyzer else None,
|
|
65
|
+
"repo_uri": app.repository.uri if app.repository else None,
|
|
66
|
+
"source_revision": app.repository.revision if app.repository else None,
|
|
67
|
+
"repo_dirty": app.repository.dirty if app.repository else None,
|
|
68
|
+
}
|
|
69
|
+
),
|
|
70
|
+
)
|
|
57
71
|
|
|
58
72
|
for file_key, mod in app.symbol_table.items():
|
|
59
|
-
mod_ref = b.node(
|
|
73
|
+
mod_ref = b.node(
|
|
74
|
+
["PyModule"], "file_key", file_key, _module_props(mod, file_key)
|
|
75
|
+
)
|
|
60
76
|
b.edge("PY_HAS_MODULE", app_ref, mod_ref)
|
|
61
77
|
_project_module_body(b, file_key, mod_ref, mod)
|
|
62
78
|
|
|
@@ -66,7 +82,9 @@ def project(app: PyApplication, app_name: str) -> GraphRows:
|
|
|
66
82
|
for e in app.call_graph:
|
|
67
83
|
src = _call_endpoint(b, e.source, externals)
|
|
68
84
|
tgt = _call_endpoint(b, e.target, externals)
|
|
69
|
-
b.edge(
|
|
85
|
+
b.edge(
|
|
86
|
+
"PY_CALLS", src, tgt, _call_edge_props(e.weight, list(e.provenance or []))
|
|
87
|
+
)
|
|
70
88
|
|
|
71
89
|
return b.finish()
|
|
72
90
|
|
|
@@ -86,9 +104,18 @@ def _call_endpoint(b: RowBuilder, signature: str, externals: dict) -> NodeRef:
|
|
|
86
104
|
ext = externals.get(signature)
|
|
87
105
|
if ext is None and b.has_key("PySymbol", signature):
|
|
88
106
|
return _sym(signature)
|
|
89
|
-
name =
|
|
107
|
+
name = (
|
|
108
|
+
ext.name
|
|
109
|
+
if ext is not None
|
|
110
|
+
else (signature.rsplit(".", 1)[-1] if "." in signature else signature)
|
|
111
|
+
)
|
|
90
112
|
module = ext.module if ext is not None else None
|
|
91
|
-
return b.node(
|
|
113
|
+
return b.node(
|
|
114
|
+
["PySymbol", "PyExternal"],
|
|
115
|
+
"signature",
|
|
116
|
+
signature,
|
|
117
|
+
prune({"name": name, "module": module}),
|
|
118
|
+
)
|
|
92
119
|
|
|
93
120
|
|
|
94
121
|
# ----------------------------------------------------------------------------------------------
|
|
@@ -96,7 +123,9 @@ def _call_endpoint(b: RowBuilder, signature: str, externals: dict) -> NodeRef:
|
|
|
96
123
|
# ----------------------------------------------------------------------------------------------
|
|
97
124
|
|
|
98
125
|
|
|
99
|
-
def _project_module_body(
|
|
126
|
+
def _project_module_body(
|
|
127
|
+
b: RowBuilder, file_key: str, mod_ref: NodeRef, mod: PyModule
|
|
128
|
+
) -> None:
|
|
100
129
|
for fn in (mod.functions or {}).values():
|
|
101
130
|
_project_callable(b, file_key, mod_ref, "PY_DECLARES", fn)
|
|
102
131
|
for cl in (mod.classes or {}).values():
|
|
@@ -107,25 +136,47 @@ def _project_module_body(b: RowBuilder, file_key: str, mod_ref: NodeRef, mod: Py
|
|
|
107
136
|
|
|
108
137
|
|
|
109
138
|
def _project_imports(b: RowBuilder, mod_ref: NodeRef, mod: PyModule) -> None:
|
|
110
|
-
#
|
|
111
|
-
#
|
|
139
|
+
# At most one PY_IMPORTS edge per (module, target) pair -- mirrors PY_CALLS,
|
|
140
|
+
# which pre-aggregates for the same reason: both writers MERGE edges on
|
|
141
|
+
# (type, from, to) and SET their props, so a second row for the same pair
|
|
142
|
+
# would silently overwrite the first in Neo4j instead of adding an edge.
|
|
143
|
+
# Buckets key on the edge's target identity (the resolved module, or the
|
|
144
|
+
# spelling itself for unresolved/external imports), so the SAME target
|
|
145
|
+
# imported under different spellings (``from pkg import util``,
|
|
146
|
+
# ``from . import util as u``, ``from .util import helper``) collapses
|
|
147
|
+
# onto one edge; the raw spellings ride along as a ``spellings`` array.
|
|
148
|
+
# Resolved internal imports point at the real :PyModule; externals keep
|
|
149
|
+
# the shared :PyPackage. Unresolved *relative* spellings (".", ".foo")
|
|
150
|
+
# name no package -- they are dropped from the graph (the spelling
|
|
151
|
+
# survives in analysis.json), instead of minting bogus
|
|
152
|
+
# :PyPackage{name: "."} nodes.
|
|
112
153
|
agg: dict = {}
|
|
113
154
|
for im in mod.imports or []:
|
|
114
155
|
if not im.module:
|
|
115
|
-
continue
|
|
116
|
-
|
|
156
|
+
continue
|
|
157
|
+
if im.resolved_module is None and im.module.startswith("."):
|
|
158
|
+
continue
|
|
159
|
+
key = im.resolved_module or im.module
|
|
160
|
+
a = agg.setdefault(
|
|
161
|
+
key, {"spellings": set(), "names": set(), "aliases": set(), "resolved": im.resolved_module}
|
|
162
|
+
)
|
|
163
|
+
a["spellings"].add(im.module)
|
|
117
164
|
if im.name:
|
|
118
165
|
a["names"].add(im.name)
|
|
119
166
|
if im.alias:
|
|
120
167
|
a["aliases"].add(im.alias)
|
|
121
|
-
for
|
|
122
|
-
|
|
168
|
+
for key, a in agg.items():
|
|
169
|
+
if a["resolved"] is not None:
|
|
170
|
+
target = NodeRef("PyModule", "file_key", a["resolved"])
|
|
171
|
+
else:
|
|
172
|
+
target = b.node(["PyPackage"], "name", key, {})
|
|
123
173
|
b.edge(
|
|
124
174
|
"PY_IMPORTS",
|
|
125
175
|
mod_ref,
|
|
126
|
-
|
|
176
|
+
target,
|
|
127
177
|
prune(
|
|
128
178
|
{
|
|
179
|
+
"spellings": sorted(a["spellings"]),
|
|
129
180
|
"imported_names": sorted(a["names"]) or None,
|
|
130
181
|
"aliases": sorted(a["aliases"]) or None,
|
|
131
182
|
}
|
|
@@ -141,7 +192,9 @@ def _project_imports(b: RowBuilder, mod_ref: NodeRef, mod: PyModule) -> None:
|
|
|
141
192
|
def _project_class(
|
|
142
193
|
b: RowBuilder, file_key: str, parent: NodeRef, parent_rel: str, cl: PyClass
|
|
143
194
|
) -> None:
|
|
144
|
-
ref = b.node(
|
|
195
|
+
ref = b.node(
|
|
196
|
+
["PySymbol", "PyClass"], "signature", cl.signature, _class_props(cl, file_key)
|
|
197
|
+
)
|
|
145
198
|
b.edge(parent_rel, parent, ref)
|
|
146
199
|
|
|
147
200
|
for base in cl.base_classes or []:
|
|
@@ -158,7 +211,12 @@ def _project_class(
|
|
|
158
211
|
def _project_callable(
|
|
159
212
|
b: RowBuilder, file_key: str, owner: NodeRef, owner_rel: str, c: PyCallable
|
|
160
213
|
) -> None:
|
|
161
|
-
ref = b.node(
|
|
214
|
+
ref = b.node(
|
|
215
|
+
["PySymbol", "PyCallable"],
|
|
216
|
+
"signature",
|
|
217
|
+
c.signature,
|
|
218
|
+
_callable_props(c, file_key),
|
|
219
|
+
)
|
|
162
220
|
b.edge(owner_rel, owner, ref)
|
|
163
221
|
|
|
164
222
|
for d in c.decorators or []:
|
|
@@ -166,7 +224,9 @@ def _project_callable(
|
|
|
166
224
|
|
|
167
225
|
for s in c.call_sites or []:
|
|
168
226
|
# Key off the relative file (a call site lives in its callable's file) so ids stay portable.
|
|
169
|
-
cs_id =
|
|
227
|
+
cs_id = (
|
|
228
|
+
f"{file_key}#{s.start_line}:{s.start_column}-{s.end_line}:{s.end_column}"
|
|
229
|
+
)
|
|
170
230
|
cs = b.node(["PyCallSite"], "id", cs_id, _call_site_props(s, file_key))
|
|
171
231
|
b.edge("PY_HAS_CALLSITE", ref, cs)
|
|
172
232
|
if s.callee_signature:
|
|
@@ -189,7 +249,11 @@ def _project_attribute(
|
|
|
189
249
|
|
|
190
250
|
|
|
191
251
|
def _project_variable(
|
|
192
|
-
b: RowBuilder,
|
|
252
|
+
b: RowBuilder,
|
|
253
|
+
file_key: str,
|
|
254
|
+
owner: NodeRef,
|
|
255
|
+
owner_id: str,
|
|
256
|
+
v: PyVariableDeclaration,
|
|
193
257
|
) -> None:
|
|
194
258
|
var_id = f"{owner_id}#{v.name}@{v.start_line}"
|
|
195
259
|
ref = b.node(["PyVariable"], "id", var_id, _variable_props(v, var_id, file_key))
|
|
@@ -258,6 +322,7 @@ def _attribute_props(a: PyClassAttribute, attr_id: str, file_key: str) -> Props:
|
|
|
258
322
|
"id": attr_id,
|
|
259
323
|
"name": a.name,
|
|
260
324
|
"type": a.type,
|
|
325
|
+
"initializer": a.initializer,
|
|
261
326
|
"docstring": _docstring_of(a.comments),
|
|
262
327
|
"start_line": a.start_line,
|
|
263
328
|
"end_line": a.end_line,
|
|
@@ -290,6 +355,7 @@ def _call_site_props(s: PyCallsite, file_key: str) -> Props:
|
|
|
290
355
|
"receiver_expr": s.receiver_expr,
|
|
291
356
|
"receiver_type": s.receiver_type,
|
|
292
357
|
"argument_types": list(s.argument_types or []),
|
|
358
|
+
"arguments_json": _stringify_if(s.arguments),
|
|
293
359
|
"return_type": s.return_type,
|
|
294
360
|
"callee_signature": s.callee_signature,
|
|
295
361
|
"is_constructor_call": s.is_constructor_call,
|