codeanalyzer-python 1.0.0__tar.gz → 1.0.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-1.0.0 → codeanalyzer_python-1.0.2}/PKG-INFO +21 -21
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/README.md +20 -20
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/__main__.py +35 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/core.py +22 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/neo4j/project.py +29 -14
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/semantic_analysis/pycg/pycg_analysis.py +67 -6
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/syntactic_analysis/symbol_table_builder.py +36 -17
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/docs/handoff/README.md +2 -2
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/pyproject.toml +1 -1
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/.gitignore +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/LICENSE +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/NOTICE +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/__init__.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/config/__init__.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/config/config.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/__init__.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/access_paths.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/alias.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/builder.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/cfg.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/defuse.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/dominance.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/identity.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/pdg.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/scalpel_oracle.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/scc.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/sdg.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/slicing.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/summaries.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/syntactic.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/jedi/__init__.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/jedi/jedi.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/neo4j/__init__.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/neo4j/bolt.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/neo4j/cypher.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/neo4j/emit.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/neo4j/rows.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/neo4j/schema.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/options/__init__.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/options/options.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/provenance.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/py.typed +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/schema/__init__.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/schema/assign_ids.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/schema/call_graph_ids.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/schema/ids.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/schema/l1_body.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/schema/l2_callees.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/schema/py_schema.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/semantic_analysis/__init__.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/semantic_analysis/call_graph.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/semantic_analysis/pycg/__init__.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/semantic_analysis/pycg/pycg_exceptions.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/semantic_analysis/pycg/shard_planner.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/syntactic_analysis/__init__.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/syntactic_analysis/exceptions.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/syntactic_analysis/import_resolver.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/utils/__init__.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/utils/logging.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/utils/progress_bar.py +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/docs/handoff/schema.neo4j.json +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/schema-uml.drawio +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/schema.neo4j.json +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/flask/README.md +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/flask/examples/celery/README.md +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/flask/requirements-skip/README.md +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/flask/src/flask/sansio/README.md +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/requests/LICENSE +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/requests/NOTICE +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/requests/README.md +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/requests/docs/_themes/LICENSE +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/requests/ext/LICENSE +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/xarray/LICENSE +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/xarray/README.md +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/xarray/properties/README.md +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/xarray/xarray/datatree_/LICENSE +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/xarray/xarray/datatree_/README.md +0 -0
- {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/xarray/xarray/datatree_/docs/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codeanalyzer-python
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: Static analysis for Python — canonical schema v2 (symbol table, call graph, and native CFG/PDG/SDG dataflow) as analysis.json or a Neo4j property graph.
|
|
5
5
|
Author-email: Rahul Krishna <i.m.ralk@gmail.com>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -203,19 +203,19 @@ $ canpy --help
|
|
|
203
203
|
│ --version Show the canpy │
|
|
204
204
|
│ version and │
|
|
205
205
|
│ exit. │
|
|
206
|
-
│ --input -i
|
|
206
|
+
│ --input -i <path> Path to the │
|
|
207
207
|
│ project root │
|
|
208
208
|
│ directory (not │
|
|
209
209
|
│ required for │
|
|
210
210
|
│ --emit schema). │
|
|
211
|
-
│ --output -o
|
|
211
|
+
│ --output -o <path> Output directory │
|
|
212
212
|
│ for artifacts. │
|
|
213
|
-
│ --format -f
|
|
213
|
+
│ --format -f <json|msgpack> Output format │
|
|
214
214
|
│ for --emit json: │
|
|
215
215
|
│ json or msgpack. │
|
|
216
216
|
│ [default: json] │
|
|
217
|
-
│ --emit
|
|
218
|
-
│ ma
|
|
217
|
+
│ --emit <json|neo4j|sche Output target: │
|
|
218
|
+
│ ma> json │
|
|
219
219
|
│ (analysis.json, │
|
|
220
220
|
│ default) | neo4j │
|
|
221
221
|
│ (graph.cypher or │
|
|
@@ -225,13 +225,13 @@ $ canpy --help
|
|
|
225
225
|
│ schema.json │
|
|
226
226
|
│ contract). │
|
|
227
227
|
│ [default: json] │
|
|
228
|
-
│ --app-name
|
|
228
|
+
│ --app-name <str> Logical │
|
|
229
229
|
│ application name │
|
|
230
230
|
│ for the graph │
|
|
231
231
|
│ :PyApplication │
|
|
232
232
|
│ anchor (default: │
|
|
233
233
|
│ input dir name). │
|
|
234
|
-
│ --neo4j-uri
|
|
234
|
+
│ --neo4j-uri <str> Push the graph │
|
|
235
235
|
│ to a live Neo4j │
|
|
236
236
|
│ over Bolt │
|
|
237
237
|
│ (incremental); │
|
|
@@ -239,11 +239,11 @@ $ canpy --help
|
|
|
239
239
|
│ graph.cypher. │
|
|
240
240
|
│ [env var: │
|
|
241
241
|
│ NEO4J_URI] │
|
|
242
|
-
│ --neo4j-user
|
|
242
|
+
│ --neo4j-user <str> Neo4j username. │
|
|
243
243
|
│ [env var: │
|
|
244
244
|
│ NEO4J_USERNAME] │
|
|
245
245
|
│ [default: neo4j] │
|
|
246
|
-
│ --neo4j-password
|
|
246
|
+
│ --neo4j-password <str> Neo4j password. │
|
|
247
247
|
│ Prefer the env │
|
|
248
248
|
│ var over the │
|
|
249
249
|
│ flag (the flag │
|
|
@@ -253,12 +253,12 @@ $ canpy --help
|
|
|
253
253
|
│ [env var: │
|
|
254
254
|
│ NEO4J_PASSWORD] │
|
|
255
255
|
│ [default: neo4j] │
|
|
256
|
-
│ --neo4j-database
|
|
256
|
+
│ --neo4j-database <str> Neo4j database │
|
|
257
257
|
│ name (default: │
|
|
258
258
|
│ server default). │
|
|
259
259
|
│ [env var: │
|
|
260
260
|
│ NEO4J_DATABASE] │
|
|
261
|
-
│ --analysis-level -a
|
|
261
|
+
│ --analysis-level -a <int range> Analysis depth: │
|
|
262
262
|
│ [1<=x<=4] 1=symbol │
|
|
263
263
|
│ table+Jedi call │
|
|
264
264
|
│ graph, 2=+PyCG │
|
|
@@ -274,7 +274,7 @@ $ canpy --help
|
|
|
274
274
|
│ alias-aware │
|
|
275
275
|
│ DDG). │
|
|
276
276
|
│ [default: 1] │
|
|
277
|
-
│ --graphs
|
|
277
|
+
│ --graphs <str> Level 3+ only: │
|
|
278
278
|
│ comma-separated │
|
|
279
279
|
│ program-graph │
|
|
280
280
|
│ sections to emit │
|
|
@@ -287,7 +287,7 @@ $ canpy --help
|
|
|
287
287
|
│ requires -a 4. │
|
|
288
288
|
│ [default: │
|
|
289
289
|
│ cfg,dfg,pdg] │
|
|
290
|
-
│ --graph-field-de…
|
|
290
|
+
│ --graph-field-de… <int range> Level 3 only: │
|
|
291
291
|
│ [x>=1] k-limit on │
|
|
292
292
|
│ access-path │
|
|
293
293
|
│ depth (x.f.g.h │
|
|
@@ -324,12 +324,12 @@ $ canpy --help
|
|
|
324
324
|
│ environment │
|
|
325
325
|
│ instead. │
|
|
326
326
|
│ [default: venv] │
|
|
327
|
-
│ --file-name
|
|
327
|
+
│ --file-name <path> Analyze only the │
|
|
328
328
|
│ specified file │
|
|
329
329
|
│ (relative to │
|
|
330
330
|
│ input │
|
|
331
331
|
│ directory). │
|
|
332
|
-
│ --cache-dir -c
|
|
332
|
+
│ --cache-dir -c <path> Directory to │
|
|
333
333
|
│ store analysis │
|
|
334
334
|
│ cache. Defaults │
|
|
335
335
|
│ to │
|
|
@@ -343,7 +343,7 @@ $ canpy --help
|
|
|
343
343
|
│ retained. │
|
|
344
344
|
│ [default: │
|
|
345
345
|
│ keep-cache] │
|
|
346
|
-
│ -v
|
|
346
|
+
│ -v <int> Increase │
|
|
347
347
|
│ verbosity: -v, │
|
|
348
348
|
│ -vv, -vvv │
|
|
349
349
|
│ [default: 0] │
|
|
@@ -370,7 +370,7 @@ $ canpy --help
|
|
|
370
370
|
│ Jedi-only edges. │
|
|
371
371
|
│ [default: │
|
|
372
372
|
│ no-pycg-shard] │
|
|
373
|
-
│ --pycg-shard-cei…
|
|
373
|
+
│ --pycg-shard-cei… <int range> Maximum files │
|
|
374
374
|
│ [x>=1] per shard when │
|
|
375
375
|
│ --pycg-shard is │
|
|
376
376
|
│ active (default │
|
|
@@ -392,7 +392,7 @@ $ canpy --help
|
|
|
392
392
|
│ heavy import │
|
|
393
393
|
│ graphs. │
|
|
394
394
|
│ [default: 100] │
|
|
395
|
-
│ --pycg-shard-tim…
|
|
395
|
+
│ --pycg-shard-tim… <int range> Per-shard │
|
|
396
396
|
│ [x>=0] wall-clock │
|
|
397
397
|
│ timeout in │
|
|
398
398
|
│ seconds when │
|
|
@@ -420,7 +420,7 @@ $ canpy --help
|
|
|
420
420
|
│ ignored on │
|
|
421
421
|
│ Windows. │
|
|
422
422
|
│ [default: 120] │
|
|
423
|
-
│ --pycg-shard-str…
|
|
423
|
+
│ --pycg-shard-str… <jedi|package> How --pycg-shard │
|
|
424
424
|
│ groups files │
|
|
425
425
|
│ (level 2 only). │
|
|
426
426
|
│ 'jedi' (default) │
|
|
@@ -442,7 +442,7 @@ $ canpy --help
|
|
|
442
442
|
│ one-shard-per-p… │
|
|
443
443
|
│ grouping. │
|
|
444
444
|
│ [default: jedi] │
|
|
445
|
-
│ --pycg-max-iter
|
|
445
|
+
│ --pycg-max-iter <int range> Cap on PyCG's │
|
|
446
446
|
│ [x>=-1] fixpoint passes │
|
|
447
447
|
│ per │
|
|
448
448
|
│ shard/project │
|
|
@@ -164,19 +164,19 @@ $ canpy --help
|
|
|
164
164
|
│ --version Show the canpy │
|
|
165
165
|
│ version and │
|
|
166
166
|
│ exit. │
|
|
167
|
-
│ --input -i
|
|
167
|
+
│ --input -i <path> Path to the │
|
|
168
168
|
│ project root │
|
|
169
169
|
│ directory (not │
|
|
170
170
|
│ required for │
|
|
171
171
|
│ --emit schema). │
|
|
172
|
-
│ --output -o
|
|
172
|
+
│ --output -o <path> Output directory │
|
|
173
173
|
│ for artifacts. │
|
|
174
|
-
│ --format -f
|
|
174
|
+
│ --format -f <json|msgpack> Output format │
|
|
175
175
|
│ for --emit json: │
|
|
176
176
|
│ json or msgpack. │
|
|
177
177
|
│ [default: json] │
|
|
178
|
-
│ --emit
|
|
179
|
-
│ ma
|
|
178
|
+
│ --emit <json|neo4j|sche Output target: │
|
|
179
|
+
│ ma> json │
|
|
180
180
|
│ (analysis.json, │
|
|
181
181
|
│ default) | neo4j │
|
|
182
182
|
│ (graph.cypher or │
|
|
@@ -186,13 +186,13 @@ $ canpy --help
|
|
|
186
186
|
│ schema.json │
|
|
187
187
|
│ contract). │
|
|
188
188
|
│ [default: json] │
|
|
189
|
-
│ --app-name
|
|
189
|
+
│ --app-name <str> Logical │
|
|
190
190
|
│ application name │
|
|
191
191
|
│ for the graph │
|
|
192
192
|
│ :PyApplication │
|
|
193
193
|
│ anchor (default: │
|
|
194
194
|
│ input dir name). │
|
|
195
|
-
│ --neo4j-uri
|
|
195
|
+
│ --neo4j-uri <str> Push the graph │
|
|
196
196
|
│ to a live Neo4j │
|
|
197
197
|
│ over Bolt │
|
|
198
198
|
│ (incremental); │
|
|
@@ -200,11 +200,11 @@ $ canpy --help
|
|
|
200
200
|
│ graph.cypher. │
|
|
201
201
|
│ [env var: │
|
|
202
202
|
│ NEO4J_URI] │
|
|
203
|
-
│ --neo4j-user
|
|
203
|
+
│ --neo4j-user <str> Neo4j username. │
|
|
204
204
|
│ [env var: │
|
|
205
205
|
│ NEO4J_USERNAME] │
|
|
206
206
|
│ [default: neo4j] │
|
|
207
|
-
│ --neo4j-password
|
|
207
|
+
│ --neo4j-password <str> Neo4j password. │
|
|
208
208
|
│ Prefer the env │
|
|
209
209
|
│ var over the │
|
|
210
210
|
│ flag (the flag │
|
|
@@ -214,12 +214,12 @@ $ canpy --help
|
|
|
214
214
|
│ [env var: │
|
|
215
215
|
│ NEO4J_PASSWORD] │
|
|
216
216
|
│ [default: neo4j] │
|
|
217
|
-
│ --neo4j-database
|
|
217
|
+
│ --neo4j-database <str> Neo4j database │
|
|
218
218
|
│ name (default: │
|
|
219
219
|
│ server default). │
|
|
220
220
|
│ [env var: │
|
|
221
221
|
│ NEO4J_DATABASE] │
|
|
222
|
-
│ --analysis-level -a
|
|
222
|
+
│ --analysis-level -a <int range> Analysis depth: │
|
|
223
223
|
│ [1<=x<=4] 1=symbol │
|
|
224
224
|
│ table+Jedi call │
|
|
225
225
|
│ graph, 2=+PyCG │
|
|
@@ -235,7 +235,7 @@ $ canpy --help
|
|
|
235
235
|
│ alias-aware │
|
|
236
236
|
│ DDG). │
|
|
237
237
|
│ [default: 1] │
|
|
238
|
-
│ --graphs
|
|
238
|
+
│ --graphs <str> Level 3+ only: │
|
|
239
239
|
│ comma-separated │
|
|
240
240
|
│ program-graph │
|
|
241
241
|
│ sections to emit │
|
|
@@ -248,7 +248,7 @@ $ canpy --help
|
|
|
248
248
|
│ requires -a 4. │
|
|
249
249
|
│ [default: │
|
|
250
250
|
│ cfg,dfg,pdg] │
|
|
251
|
-
│ --graph-field-de…
|
|
251
|
+
│ --graph-field-de… <int range> Level 3 only: │
|
|
252
252
|
│ [x>=1] k-limit on │
|
|
253
253
|
│ access-path │
|
|
254
254
|
│ depth (x.f.g.h │
|
|
@@ -285,12 +285,12 @@ $ canpy --help
|
|
|
285
285
|
│ environment │
|
|
286
286
|
│ instead. │
|
|
287
287
|
│ [default: venv] │
|
|
288
|
-
│ --file-name
|
|
288
|
+
│ --file-name <path> Analyze only the │
|
|
289
289
|
│ specified file │
|
|
290
290
|
│ (relative to │
|
|
291
291
|
│ input │
|
|
292
292
|
│ directory). │
|
|
293
|
-
│ --cache-dir -c
|
|
293
|
+
│ --cache-dir -c <path> Directory to │
|
|
294
294
|
│ store analysis │
|
|
295
295
|
│ cache. Defaults │
|
|
296
296
|
│ to │
|
|
@@ -304,7 +304,7 @@ $ canpy --help
|
|
|
304
304
|
│ retained. │
|
|
305
305
|
│ [default: │
|
|
306
306
|
│ keep-cache] │
|
|
307
|
-
│ -v
|
|
307
|
+
│ -v <int> Increase │
|
|
308
308
|
│ verbosity: -v, │
|
|
309
309
|
│ -vv, -vvv │
|
|
310
310
|
│ [default: 0] │
|
|
@@ -331,7 +331,7 @@ $ canpy --help
|
|
|
331
331
|
│ Jedi-only edges. │
|
|
332
332
|
│ [default: │
|
|
333
333
|
│ no-pycg-shard] │
|
|
334
|
-
│ --pycg-shard-cei…
|
|
334
|
+
│ --pycg-shard-cei… <int range> Maximum files │
|
|
335
335
|
│ [x>=1] per shard when │
|
|
336
336
|
│ --pycg-shard is │
|
|
337
337
|
│ active (default │
|
|
@@ -353,7 +353,7 @@ $ canpy --help
|
|
|
353
353
|
│ heavy import │
|
|
354
354
|
│ graphs. │
|
|
355
355
|
│ [default: 100] │
|
|
356
|
-
│ --pycg-shard-tim…
|
|
356
|
+
│ --pycg-shard-tim… <int range> Per-shard │
|
|
357
357
|
│ [x>=0] wall-clock │
|
|
358
358
|
│ timeout in │
|
|
359
359
|
│ seconds when │
|
|
@@ -381,7 +381,7 @@ $ canpy --help
|
|
|
381
381
|
│ ignored on │
|
|
382
382
|
│ Windows. │
|
|
383
383
|
│ [default: 120] │
|
|
384
|
-
│ --pycg-shard-str…
|
|
384
|
+
│ --pycg-shard-str… <jedi|package> How --pycg-shard │
|
|
385
385
|
│ groups files │
|
|
386
386
|
│ (level 2 only). │
|
|
387
387
|
│ 'jedi' (default) │
|
|
@@ -403,7 +403,7 @@ $ canpy --help
|
|
|
403
403
|
│ one-shard-per-p… │
|
|
404
404
|
│ grouping. │
|
|
405
405
|
│ [default: jedi] │
|
|
406
|
-
│ --pycg-max-iter
|
|
406
|
+
│ --pycg-max-iter <int range> Cap on PyCG's │
|
|
407
407
|
│ [x>=-1] fixpoint passes │
|
|
408
408
|
│ per │
|
|
409
409
|
│ shard/project │
|
|
@@ -1,9 +1,40 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
1
3
|
from importlib.metadata import version as _pkg_version, PackageNotFoundError
|
|
2
4
|
from pathlib import Path
|
|
3
5
|
from typing import Optional, Annotated
|
|
4
6
|
|
|
5
7
|
import typer
|
|
6
8
|
|
|
9
|
+
|
|
10
|
+
def _pin_hash_seed() -> None:
|
|
11
|
+
"""Re-exec once with ``PYTHONHASHSEED=0`` unless the caller pinned one.
|
|
12
|
+
|
|
13
|
+
PyCG's capped fixpoint (``--pycg-max-iter``) iterates hash-ordered sets
|
|
14
|
+
keyed on module/access-path strings, so an unpinned per-interpreter hash
|
|
15
|
+
seed makes the emitted L2+ call graph vary run to run (issue #99). The
|
|
16
|
+
seed cannot be set after interpreter start, hence the exec. Export
|
|
17
|
+
PYTHONHASHSEED (any value) to opt out or pin a different seed.
|
|
18
|
+
|
|
19
|
+
Only fires when this process really is the CLI (canpy / python -m
|
|
20
|
+
codeanalyzer): in-process invocations — e.g. Typer's CliRunner in the
|
|
21
|
+
test suite, or a host app calling the callback — must never have their
|
|
22
|
+
own process exec'd out from under them."""
|
|
23
|
+
if os.environ.get("PYTHONHASHSEED") is not None:
|
|
24
|
+
return
|
|
25
|
+
argv0 = os.path.basename(sys.argv[0]) if sys.argv else ""
|
|
26
|
+
is_cli = argv0 in ("canpy", "codeanalyzer") or sys.argv[0].endswith(
|
|
27
|
+
os.path.join("codeanalyzer", "__main__.py")
|
|
28
|
+
)
|
|
29
|
+
if not is_cli:
|
|
30
|
+
return
|
|
31
|
+
env = dict(os.environ, PYTHONHASHSEED="0")
|
|
32
|
+
os.execvpe(
|
|
33
|
+
sys.executable,
|
|
34
|
+
[sys.executable, "-m", "codeanalyzer", *sys.argv[1:]],
|
|
35
|
+
env,
|
|
36
|
+
)
|
|
37
|
+
|
|
7
38
|
from codeanalyzer.core import Codeanalyzer
|
|
8
39
|
from codeanalyzer.utils import _set_log_level, logger
|
|
9
40
|
from codeanalyzer.config import OutputFormat
|
|
@@ -264,6 +295,10 @@ def main(
|
|
|
264
295
|
),
|
|
265
296
|
] = 50,
|
|
266
297
|
):
|
|
298
|
+
# Determinism: pin the interpreter hash seed before any analysis (no-op
|
|
299
|
+
# when PYTHONHASHSEED is already set; --version exits before this).
|
|
300
|
+
_pin_hash_seed()
|
|
301
|
+
|
|
267
302
|
# Flag validation (strict: unrecognized values error out, never fall back).
|
|
268
303
|
selected_graphs = [g.strip() for g in graphs.split(",") if g.strip()]
|
|
269
304
|
from codeanalyzer.dataflow.builder import VALID_GRAPHS
|
|
@@ -37,6 +37,22 @@ from codeanalyzer.utils import ProgressBar
|
|
|
37
37
|
from codeanalyzer.options import AnalysisOptions
|
|
38
38
|
from codeanalyzer.provenance import analyzer_info, repository_info
|
|
39
39
|
|
|
40
|
+
def _ensure_ray() -> None:
|
|
41
|
+
"""Initialize Ray with the driver's pinned hash seed in the workers.
|
|
42
|
+
|
|
43
|
+
An implicit auto-init would not carry PYTHONHASHSEED into worker
|
|
44
|
+
interpreters, so PyCG shards (and Jedi inference) run there with random
|
|
45
|
+
set-iteration order and the emitted edges vary run to run (issue #99)."""
|
|
46
|
+
if not ray.is_initialized():
|
|
47
|
+
ray.init(
|
|
48
|
+
runtime_env={
|
|
49
|
+
"env_vars": {
|
|
50
|
+
"PYTHONHASHSEED": os.environ.get("PYTHONHASHSEED", "0")
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
40
56
|
@ray.remote
|
|
41
57
|
def _process_file_with_ray(py_file: Union[Path, str], project_dir: Union[Path, str], virtualenv: Union[Path, str, None]) -> Dict[str, PyModule]:
|
|
42
58
|
"""Processes files in the project directory using Ray for distributed processing.
|
|
@@ -438,6 +454,11 @@ class Codeanalyzer:
|
|
|
438
454
|
call_graph = merge_edges(call_graph, pycg_edges)
|
|
439
455
|
|
|
440
456
|
call_graph = filter_external_edges(call_graph, symbol_table)
|
|
457
|
+
# Canonical edge order: backend iteration order (PyCG dicts, Counter
|
|
458
|
+
# insertion) is not a contract — sort so identical edge SETS always
|
|
459
|
+
# serialize identically (issue #99 determinism gate), and so the
|
|
460
|
+
# external-symbol homing below assigns ids in a stable order.
|
|
461
|
+
call_graph.sort(key=lambda e: (e.src, e.dst))
|
|
441
462
|
|
|
442
463
|
# Recreate pyapplication
|
|
443
464
|
app = (
|
|
@@ -716,6 +737,7 @@ class Codeanalyzer:
|
|
|
716
737
|
|
|
717
738
|
# Process only new/changed files with Ray
|
|
718
739
|
if files_to_process:
|
|
740
|
+
_ensure_ray()
|
|
719
741
|
futures = [_process_file_with_ray.remote(py_file, self.project_dir, str(self.virtualenv) if self.virtualenv else None) for py_file in files_to_process]
|
|
720
742
|
|
|
721
743
|
with ProgressBar(len(futures), "Building symbol table (parallel)") as progress:
|
|
@@ -290,9 +290,11 @@ def _project_module_body(
|
|
|
290
290
|
externals: dict, sig_to_id: dict, module_id_by_key: dict,
|
|
291
291
|
) -> None:
|
|
292
292
|
for fn in (mod.functions or {}).values():
|
|
293
|
-
_project_callable(b, file_key, mod_ref, "PY_DECLARES", fn, externals, sig_to_id
|
|
293
|
+
_project_callable(b, file_key, mod_ref, "PY_DECLARES", fn, externals, sig_to_id,
|
|
294
|
+
mod.source)
|
|
294
295
|
for cl in (mod.types or {}).values():
|
|
295
|
-
_project_class(b, file_key, mod_ref, "PY_DECLARES", cl, externals, sig_to_id
|
|
296
|
+
_project_class(b, file_key, mod_ref, "PY_DECLARES", cl, externals, sig_to_id,
|
|
297
|
+
mod.source)
|
|
296
298
|
for v in mod.variables or []:
|
|
297
299
|
_project_variable(b, file_key, mod_ref, file_key, v)
|
|
298
300
|
_project_imports(b, mod_ref, mod, module_id_by_key)
|
|
@@ -360,10 +362,10 @@ def _project_imports(b: RowBuilder, mod_ref: NodeRef, mod: PyModule,
|
|
|
360
362
|
|
|
361
363
|
def _project_class(
|
|
362
364
|
b: RowBuilder, file_key: str, parent: NodeRef, parent_rel: str, cl: PyClass,
|
|
363
|
-
externals: dict, sig_to_id: dict,
|
|
365
|
+
externals: dict, sig_to_id: dict, source: str,
|
|
364
366
|
) -> None:
|
|
365
367
|
ref = b.node(
|
|
366
|
-
["PySymbol", "PyClass"], "id", cl.id, _class_props(cl, file_key)
|
|
368
|
+
["PySymbol", "PyClass"], "id", cl.id, _class_props(cl, file_key, source)
|
|
367
369
|
)
|
|
368
370
|
b.edge(parent_rel, parent, ref)
|
|
369
371
|
|
|
@@ -372,22 +374,23 @@ def _project_class(
|
|
|
372
374
|
b.edge_to_symbol("PY_EXTENDS", ref, _symbol_ref(base, externals, sig_to_id))
|
|
373
375
|
|
|
374
376
|
for m in (cl.callables or {}).values():
|
|
375
|
-
_project_callable(b, file_key, ref, "PY_HAS_METHOD", m, externals, sig_to_id
|
|
377
|
+
_project_callable(b, file_key, ref, "PY_HAS_METHOD", m, externals, sig_to_id,
|
|
378
|
+
source)
|
|
376
379
|
for a in (cl.attributes or {}).values():
|
|
377
380
|
_project_attribute(b, file_key, ref, cl.signature, a)
|
|
378
381
|
for ic in (cl.types or {}).values():
|
|
379
|
-
_project_class(b, file_key, ref, "PY_DECLARES", ic, externals, sig_to_id)
|
|
382
|
+
_project_class(b, file_key, ref, "PY_DECLARES", ic, externals, sig_to_id, source)
|
|
380
383
|
|
|
381
384
|
|
|
382
385
|
def _project_callable(
|
|
383
386
|
b: RowBuilder, file_key: str, owner: NodeRef, owner_rel: str, c: PyCallable,
|
|
384
|
-
externals: dict, sig_to_id: dict,
|
|
387
|
+
externals: dict, sig_to_id: dict, source: str,
|
|
385
388
|
) -> None:
|
|
386
389
|
ref = b.node(
|
|
387
390
|
["PySymbol", "PyCallable"],
|
|
388
391
|
"id",
|
|
389
392
|
c.id,
|
|
390
|
-
_callable_props(c, file_key),
|
|
393
|
+
_callable_props(c, file_key, source),
|
|
391
394
|
)
|
|
392
395
|
b.edge(owner_rel, owner, ref)
|
|
393
396
|
|
|
@@ -410,9 +413,10 @@ def _project_callable(
|
|
|
410
413
|
for v in c.local_variables or []:
|
|
411
414
|
_project_variable(b, file_key, ref, c.signature, v)
|
|
412
415
|
for ic in (c.callables or {}).values():
|
|
413
|
-
_project_callable(b, file_key, ref, "PY_DECLARES", ic, externals, sig_to_id
|
|
416
|
+
_project_callable(b, file_key, ref, "PY_DECLARES", ic, externals, sig_to_id,
|
|
417
|
+
source)
|
|
414
418
|
for cl in (c.types or {}).values():
|
|
415
|
-
_project_class(b, file_key, ref, "PY_DECLARES", cl, externals, sig_to_id)
|
|
419
|
+
_project_class(b, file_key, ref, "PY_DECLARES", cl, externals, sig_to_id, source)
|
|
416
420
|
|
|
417
421
|
|
|
418
422
|
def _project_attribute(
|
|
@@ -459,13 +463,24 @@ def _module_props(mod: PyModule, file_key: str) -> Props:
|
|
|
459
463
|
)
|
|
460
464
|
|
|
461
465
|
|
|
462
|
-
def
|
|
466
|
+
def _span_code(source: str, span) -> str | None:
|
|
467
|
+
"""A declaration's text: the owning module's ``source`` sliced by the node's
|
|
468
|
+
utf-8 byte span. Schema v2 stores source once per module, so the graph's
|
|
469
|
+
``code`` property (declared on :PyClass/:PyCallable and indexed by
|
|
470
|
+
``py_code_fts``) is derived here at projection time (#104)."""
|
|
471
|
+
if span is None or not source:
|
|
472
|
+
return None
|
|
473
|
+
lo, hi = span.bytes
|
|
474
|
+
return source.encode("utf-8")[lo:hi].decode("utf-8")
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _class_props(cl: PyClass, file_key: str, source: str) -> Props:
|
|
463
478
|
return prune(
|
|
464
479
|
{
|
|
465
480
|
"id": cl.id,
|
|
466
481
|
"signature": cl.signature,
|
|
467
482
|
"name": cl.name,
|
|
468
|
-
"code":
|
|
483
|
+
"code": _span_code(source, cl.span),
|
|
469
484
|
"base_classes": list(cl.base_classes or []),
|
|
470
485
|
"docstring": _docstring_of(cl.comments),
|
|
471
486
|
"start_line": cl.start_line,
|
|
@@ -475,7 +490,7 @@ def _class_props(cl: PyClass, file_key: str) -> Props:
|
|
|
475
490
|
)
|
|
476
491
|
|
|
477
492
|
|
|
478
|
-
def _callable_props(c: PyCallable, file_key: str) -> Props:
|
|
493
|
+
def _callable_props(c: PyCallable, file_key: str, source: str) -> Props:
|
|
479
494
|
return prune(
|
|
480
495
|
{
|
|
481
496
|
"id": c.id,
|
|
@@ -484,7 +499,7 @@ def _callable_props(c: PyCallable, file_key: str) -> Props:
|
|
|
484
499
|
"path": c.path,
|
|
485
500
|
"return_type": c.return_type,
|
|
486
501
|
"cyclomatic_complexity": c.cyclomatic_complexity,
|
|
487
|
-
"code":
|
|
502
|
+
"code": _span_code(source, c.span),
|
|
488
503
|
"code_start_line": c.code_start_line,
|
|
489
504
|
"start_line": c.start_line,
|
|
490
505
|
"end_line": c.end_line,
|
|
@@ -39,9 +39,12 @@ project-relative dotted paths so they align with the symbol table.
|
|
|
39
39
|
# re-enters PyCG's hook before its import graph is ready. Pre-importing
|
|
40
40
|
# these modules at import time ensures they're already in sys.modules when
|
|
41
41
|
# PyCG's hook is active, preventing the re-entrant ImportManagerError.
|
|
42
|
+
import fcntl
|
|
43
|
+
import hashlib
|
|
42
44
|
import importlib.metadata # noqa: F401
|
|
43
45
|
import importlib.util # noqa: F401
|
|
44
46
|
import contextlib
|
|
47
|
+
import os
|
|
45
48
|
import json # noqa: F401
|
|
46
49
|
import shutil
|
|
47
50
|
import signal
|
|
@@ -85,6 +88,15 @@ from codeanalyzer.semantic_analysis.pycg.shard_planner import plan_shards
|
|
|
85
88
|
from codeanalyzer.utils import ProgressBar, logger
|
|
86
89
|
|
|
87
90
|
|
|
91
|
+
def _shard_root_path(files: List[str], project_dir: Path) -> Path:
|
|
92
|
+
"""Content-derived mini-project root for a shard: same project + same file
|
|
93
|
+
set → same path on every run (determinism, issue #99)."""
|
|
94
|
+
digest = hashlib.sha1(
|
|
95
|
+
"\0".join([str(project_dir), *sorted(files)]).encode("utf-8")
|
|
96
|
+
).hexdigest()[:16]
|
|
97
|
+
return Path(tempfile.gettempdir()) / f"canpy_pycg_shard_{digest}"
|
|
98
|
+
|
|
99
|
+
|
|
88
100
|
def _materialize_shard_root(
|
|
89
101
|
files: List[str],
|
|
90
102
|
project_dir: Path,
|
|
@@ -103,10 +115,21 @@ def _materialize_shard_root(
|
|
|
103
115
|
|
|
104
116
|
The caller owns the returned *root* and must ``shutil.rmtree`` it.
|
|
105
117
|
"""
|
|
106
|
-
root
|
|
118
|
+
# Deterministic root: PyCG's capped fixpoint (--pycg-max-iter) is
|
|
119
|
+
# order-sensitive, and its internal state keys on absolute module paths —
|
|
120
|
+
# a random mkdtemp suffix changes those strings every run and shifts the
|
|
121
|
+
# iteration frontier, making the emitted edge set vary run-to-run
|
|
122
|
+
# (issue #99). Deriving the directory name from the shard's content keeps
|
|
123
|
+
# the path (and thus the analysis input) identical across runs. Callers
|
|
124
|
+
# that may run concurrently on the same shard serialize on the sidecar
|
|
125
|
+
# lock (see _shard_symlink_root).
|
|
126
|
+
root = _shard_root_path(files, project_dir)
|
|
127
|
+
if root.exists():
|
|
128
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
129
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
107
130
|
entry_points: List[str] = []
|
|
108
131
|
linked_inits: Set[Path] = set()
|
|
109
|
-
for f in files:
|
|
132
|
+
for f in sorted(files):
|
|
110
133
|
src = Path(f).resolve()
|
|
111
134
|
try:
|
|
112
135
|
rel = src.relative_to(project_dir)
|
|
@@ -142,12 +165,27 @@ def _shard_symlink_root(
|
|
|
142
165
|
"""Context-manager wrapper around :func:`_materialize_shard_root`.
|
|
143
166
|
|
|
144
167
|
Yields ``(root, entry_points)`` and removes the temp tree on exit.
|
|
168
|
+
|
|
169
|
+
The root path is content-derived (determinism, issue #99), so two
|
|
170
|
+
concurrent analyses of the same shard — e.g. a test suite and a manual
|
|
171
|
+
run on one project — would collide on it (one rmtree's the tree the
|
|
172
|
+
other is mid-analysis on). An exclusive flock on a sidecar lockfile
|
|
173
|
+
serializes them; distinct projects/shards hash to distinct roots and
|
|
174
|
+
never contend.
|
|
145
175
|
"""
|
|
146
|
-
|
|
176
|
+
digest_root = _shard_root_path(files, project_dir)
|
|
177
|
+
lock_path = digest_root.with_name(digest_root.name + ".lock")
|
|
178
|
+
lock_fd = os.open(str(lock_path), os.O_CREAT | os.O_RDWR)
|
|
147
179
|
try:
|
|
148
|
-
|
|
180
|
+
fcntl.flock(lock_fd, fcntl.LOCK_EX)
|
|
181
|
+
root, entry_points = _materialize_shard_root(files, project_dir)
|
|
182
|
+
try:
|
|
183
|
+
yield root, entry_points
|
|
184
|
+
finally:
|
|
185
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
149
186
|
finally:
|
|
150
|
-
|
|
187
|
+
fcntl.flock(lock_fd, fcntl.LOCK_UN)
|
|
188
|
+
os.close(lock_fd)
|
|
151
189
|
|
|
152
190
|
|
|
153
191
|
def _pycg_shard_worker(
|
|
@@ -469,7 +507,9 @@ class PyCG:
|
|
|
469
507
|
):
|
|
470
508
|
continue
|
|
471
509
|
paths.append(str(p))
|
|
472
|
-
|
|
510
|
+
# Sorted for run-to-run stability: rglob yields filesystem order, and
|
|
511
|
+
# PyCG's capped fixpoint is sensitive to entry-point order (issue #99).
|
|
512
|
+
return sorted(paths)
|
|
473
513
|
|
|
474
514
|
# ------------------------------------------------------------------
|
|
475
515
|
# Package-root helpers for sharding
|
|
@@ -712,11 +752,14 @@ class PyCG:
|
|
|
712
752
|
"""
|
|
713
753
|
import os
|
|
714
754
|
import ray
|
|
755
|
+
from codeanalyzer.core import _ensure_ray
|
|
756
|
+
_ensure_ray()
|
|
715
757
|
|
|
716
758
|
os.environ.setdefault("RAY_IGNORE_UNHANDLED_ERRORS", "1")
|
|
717
759
|
remote_fn = ray.remote(_pycg_shard_worker)
|
|
718
760
|
|
|
719
761
|
roots: List[Path] = []
|
|
762
|
+
lock_fds: List[int] = []
|
|
720
763
|
futures: List[Any] = []
|
|
721
764
|
meta: Dict[Any, List[str]] = {} # ObjectRef -> shard file list
|
|
722
765
|
edges_all: List[PyCallEdge] = []
|
|
@@ -724,6 +767,16 @@ class PyCG:
|
|
|
724
767
|
try:
|
|
725
768
|
with ProgressBar(len(shards), "Building call graph shards (parallel)", item_label="shards") as progress:
|
|
726
769
|
for files in shards:
|
|
770
|
+
# Deterministic roots can collide across concurrent
|
|
771
|
+
# analyses of the same project — the driver holds each
|
|
772
|
+
# shard's sidecar lock for the whole Ray fan-out (released
|
|
773
|
+
# in the finally below with the root cleanup).
|
|
774
|
+
lock_fd = os.open(
|
|
775
|
+
str(_shard_root_path(files, self.project_dir).with_suffix(".lock")),
|
|
776
|
+
os.O_CREAT | os.O_RDWR,
|
|
777
|
+
)
|
|
778
|
+
fcntl.flock(lock_fd, fcntl.LOCK_EX)
|
|
779
|
+
lock_fds.append(lock_fd)
|
|
727
780
|
root, eps = _materialize_shard_root(files, self.project_dir)
|
|
728
781
|
roots.append(root)
|
|
729
782
|
fut = remote_fn.remote(eps, str(root), "", self.max_iter)
|
|
@@ -765,6 +818,12 @@ class PyCG:
|
|
|
765
818
|
finally:
|
|
766
819
|
for root in roots:
|
|
767
820
|
shutil.rmtree(root, ignore_errors=True)
|
|
821
|
+
for fd in lock_fds:
|
|
822
|
+
try:
|
|
823
|
+
fcntl.flock(fd, fcntl.LOCK_UN)
|
|
824
|
+
os.close(fd)
|
|
825
|
+
except OSError:
|
|
826
|
+
pass
|
|
768
827
|
return edges_all, runaways
|
|
769
828
|
|
|
770
829
|
def _build_sharded(
|
|
@@ -870,6 +929,8 @@ class PyCG:
|
|
|
870
929
|
"""
|
|
871
930
|
import os
|
|
872
931
|
import ray
|
|
932
|
+
from codeanalyzer.core import _ensure_ray
|
|
933
|
+
_ensure_ray()
|
|
873
934
|
|
|
874
935
|
# force-cancel kills worker processes; suppress Ray's "worker died
|
|
875
936
|
# unexpectedly" noise since the death is intentional here.
|
|
@@ -57,13 +57,26 @@ class SymbolTableBuilder:
|
|
|
57
57
|
relative = Path(script_path).relative_to(self.project_dir)
|
|
58
58
|
return ".".join(relative.with_suffix("").parts) + f".{name}"
|
|
59
59
|
|
|
60
|
+
@staticmethod
|
|
61
|
+
def _first_definition(definitions):
|
|
62
|
+
"""Deterministic pick from Jedi's inference candidates.
|
|
63
|
+
|
|
64
|
+
On a union-typed receiver Jedi may return several candidates whose
|
|
65
|
+
ORDER varies run to run (issue #99 — e.g. ``o.seek`` on
|
|
66
|
+
``_IOBase | BufferedRandom | TextIOWrapper``); taking whichever came
|
|
67
|
+
first made the emitted call graph nondeterministic. Sort on the
|
|
68
|
+
stable identity (full_name, then name) and take the smallest."""
|
|
69
|
+
if not definitions:
|
|
70
|
+
return None
|
|
71
|
+
return min(definitions, key=lambda d: (d.full_name or "", d.name or ""))
|
|
72
|
+
|
|
60
73
|
@staticmethod
|
|
61
74
|
def _infer_type(script: Script, line: int, column: int) -> str:
|
|
62
75
|
"""Tries to infer the type at a given position using Jedi."""
|
|
63
76
|
try:
|
|
64
|
-
|
|
65
|
-
if
|
|
66
|
-
return
|
|
77
|
+
d = SymbolTableBuilder._first_definition(script.infer(line=line, column=column))
|
|
78
|
+
if d is not None:
|
|
79
|
+
return d.name # or .full_name
|
|
67
80
|
except Exception:
|
|
68
81
|
pass
|
|
69
82
|
return None
|
|
@@ -82,9 +95,9 @@ class SymbolTableBuilder:
|
|
|
82
95
|
Optional[str]: The fully qualified name if available, else None.
|
|
83
96
|
"""
|
|
84
97
|
try:
|
|
85
|
-
|
|
86
|
-
if
|
|
87
|
-
return
|
|
98
|
+
d = SymbolTableBuilder._first_definition(script.infer(line=line, column=column))
|
|
99
|
+
if d is not None:
|
|
100
|
+
return d.full_name
|
|
88
101
|
except Exception:
|
|
89
102
|
pass
|
|
90
103
|
return None
|
|
@@ -103,10 +116,9 @@ class SymbolTableBuilder:
|
|
|
103
116
|
the call graph.
|
|
104
117
|
"""
|
|
105
118
|
try:
|
|
106
|
-
|
|
107
|
-
if
|
|
119
|
+
d = SymbolTableBuilder._first_definition(script.infer(line=line, column=column))
|
|
120
|
+
if d is None:
|
|
108
121
|
return None, False
|
|
109
|
-
d = definitions[0]
|
|
110
122
|
is_class = (d.type == "class")
|
|
111
123
|
full = d.full_name
|
|
112
124
|
if is_class and full:
|
|
@@ -144,11 +156,17 @@ class SymbolTableBuilder:
|
|
|
144
156
|
as the callee's own name.
|
|
145
157
|
"""
|
|
146
158
|
try:
|
|
147
|
-
|
|
148
|
-
if
|
|
149
|
-
results
|
|
150
|
-
|
|
151
|
-
|
|
159
|
+
d = SymbolTableBuilder._first_definition(script.infer(line=line, column=column))
|
|
160
|
+
if d is not None:
|
|
161
|
+
# Drop NoneType results before picking: Jedi flaps between
|
|
162
|
+
# yielding {NoneType} and {} for the None arm of an Optional
|
|
163
|
+
# return (issue #99), and when a real type is also in the
|
|
164
|
+
# union it is the informative choice — a bare NoneType says
|
|
165
|
+
# nothing a missing return_type doesn't.
|
|
166
|
+
results = [r for r in d.execute() if r.name != "NoneType"]
|
|
167
|
+
r = SymbolTableBuilder._first_definition(results)
|
|
168
|
+
if r is not None:
|
|
169
|
+
return r.name
|
|
152
170
|
except Exception:
|
|
153
171
|
pass
|
|
154
172
|
return None
|
|
@@ -974,9 +992,10 @@ class SymbolTableBuilder:
|
|
|
974
992
|
|
|
975
993
|
if script:
|
|
976
994
|
try:
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
995
|
+
d = SymbolTableBuilder._first_definition(
|
|
996
|
+
script.infer(line=lineno, column=col_offset)
|
|
997
|
+
)
|
|
998
|
+
if d is not None:
|
|
980
999
|
inferred_type = d.name
|
|
981
1000
|
qname = d.full_name
|
|
982
1001
|
if d.type == "function":
|
|
@@ -13,14 +13,14 @@ the contract lives in the spec, the decision log, and `schema.neo4j.json`.
|
|
|
13
13
|
Pin the analyzer that produced these samples:
|
|
14
14
|
|
|
15
15
|
```
|
|
16
|
-
pip install "codeanalyzer-python==1.0.
|
|
16
|
+
pip install "codeanalyzer-python==1.0.1"
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
For L4 alias analysis (the Scalpel points-to oracle), install the optional extra —
|
|
20
20
|
without it, L4 degrades to the type-based fallback oracle (still sound, coarser):
|
|
21
21
|
|
|
22
22
|
```
|
|
23
|
-
pip install "codeanalyzer-python[scalpel]==1.0.
|
|
23
|
+
pip install "codeanalyzer-python[scalpel]==1.0.1"
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
## Schema contract
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "codeanalyzer-python"
|
|
3
|
-
version = "1.0.
|
|
3
|
+
version = "1.0.2"
|
|
4
4
|
description = "Static analysis for Python — canonical schema v2 (symbol table, call graph, and native CFG/PDG/SDG dataflow) as analysis.json or a Neo4j property graph."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/access_paths.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/scalpel_oracle.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/schema/call_graph_ids.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/semantic_analysis/__init__.py
RENAMED
|
File without changes
|
{codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/semantic_analysis/call_graph.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/syntactic_analysis/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|