okstra 0.130.4 → 0.131.1
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.
- package/README.md +0 -1
- package/docs/architecture.md +2 -2
- package/docs/cli.md +1 -1
- package/docs/for-ai/README.md +0 -2
- package/docs/for-ai/skills/okstra-run.md +13 -2
- package/docs/for-ai/skills/okstra-setup.md +4 -1
- package/docs/project-structure-overview.md +5 -4
- package/docs/task-process/implementation-planning.md +13 -0
- package/docs/task-process/implementation.md +17 -2
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/report-writer-worker.md +7 -0
- package/runtime/bin/lib/okstra/usage.sh +4 -4
- package/runtime/prompts/lead/okstra-lead-contract.md +2 -2
- package/runtime/prompts/lead/plan-body-verification.md +26 -5
- package/runtime/prompts/lead/report-writer.md +6 -2
- package/runtime/prompts/profiles/_common-contract.md +2 -1
- package/runtime/prompts/profiles/_implementation-deliverable.md +6 -1
- package/runtime/prompts/profiles/_implementation-executor.md +2 -2
- package/runtime/prompts/profiles/_implementation-verifier.md +33 -10
- package/runtime/prompts/profiles/final-verification.md +21 -6
- package/runtime/prompts/profiles/implementation-planning.md +16 -4
- package/runtime/python/okstra_ctl/codex_dispatch.py +31 -0
- package/runtime/python/okstra_ctl/conformance.py +49 -11
- package/runtime/python/okstra_ctl/report_finalize.py +52 -1
- package/runtime/python/okstra_ctl/run.py +15 -3
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +14 -6
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +19 -1
- package/runtime/python/okstra_vendor/__init__.py +0 -44
- package/runtime/schemas/final-report-v1.0.schema.json +99 -5
- package/runtime/skills/okstra-run/SKILL.md +11 -4
- package/runtime/skills/okstra-setup/SKILL.md +3 -2
- package/runtime/skills/okstra-setup/references/project-config.md +16 -7
- package/runtime/templates/reports/final-report.template.md +15 -9
- package/runtime/templates/reports/i18n/en.json +1 -0
- package/runtime/templates/reports/i18n/ko.json +1 -0
- package/runtime/validators/lib/fixtures.sh +6 -0
- package/runtime/validators/validate-run.py +2019 -96
- package/src/cli-registry.mjs +0 -7
- package/src/commands/lifecycle/doctor.mjs +0 -9
- package/src/lib/skill-catalog.mjs +0 -1
- package/docs/for-ai/skills/okstra-graphify.md +0 -64
- package/runtime/python/okstra_ctl/graphify_cmd.py +0 -225
- package/runtime/python/okstra_vendor/graphify/.vendored-version +0 -1
- package/runtime/python/okstra_vendor/graphify/__init__.py +0 -28
- package/runtime/python/okstra_vendor/graphify/__main__.py +0 -1371
- package/runtime/python/okstra_vendor/graphify/analyze.py +0 -540
- package/runtime/python/okstra_vendor/graphify/benchmark.py +0 -129
- package/runtime/python/okstra_vendor/graphify/build.py +0 -107
- package/runtime/python/okstra_vendor/graphify/cache.py +0 -169
- package/runtime/python/okstra_vendor/graphify/cluster.py +0 -137
- package/runtime/python/okstra_vendor/graphify/detect.py +0 -510
- package/runtime/python/okstra_vendor/graphify/export.py +0 -1014
- package/runtime/python/okstra_vendor/graphify/extract.py +0 -3277
- package/runtime/python/okstra_vendor/graphify/hooks.py +0 -220
- package/runtime/python/okstra_vendor/graphify/ingest.py +0 -297
- package/runtime/python/okstra_vendor/graphify/manifest.py +0 -4
- package/runtime/python/okstra_vendor/graphify/report.py +0 -175
- package/runtime/python/okstra_vendor/graphify/security.py +0 -203
- package/runtime/python/okstra_vendor/graphify/serve.py +0 -373
- package/runtime/python/okstra_vendor/graphify/skill-aider.md +0 -1184
- package/runtime/python/okstra_vendor/graphify/skill-claw.md +0 -1184
- package/runtime/python/okstra_vendor/graphify/skill-codex.md +0 -1242
- package/runtime/python/okstra_vendor/graphify/skill-copilot.md +0 -1268
- package/runtime/python/okstra_vendor/graphify/skill-droid.md +0 -1239
- package/runtime/python/okstra_vendor/graphify/skill-kiro.md +0 -1183
- package/runtime/python/okstra_vendor/graphify/skill-opencode.md +0 -1238
- package/runtime/python/okstra_vendor/graphify/skill-trae.md +0 -1208
- package/runtime/python/okstra_vendor/graphify/skill-vscode.md +0 -253
- package/runtime/python/okstra_vendor/graphify/skill-windows.md +0 -1245
- package/runtime/python/okstra_vendor/graphify/skill.md +0 -1319
- package/runtime/python/okstra_vendor/graphify/transcribe.py +0 -182
- package/runtime/python/okstra_vendor/graphify/validate.py +0 -72
- package/runtime/python/okstra_vendor/graphify/watch.py +0 -188
- package/runtime/python/okstra_vendor/graphify/wiki.py +0 -214
- package/runtime/python/okstra_vendor/networkx/__init__.py +0 -62
- package/runtime/python/okstra_vendor/networkx/algorithms/__init__.py +0 -134
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/__init__.py +0 -26
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/clique.py +0 -259
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/clustering_coefficient.py +0 -71
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/connectivity.py +0 -412
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/density.py +0 -396
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/distance_measures.py +0 -150
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/dominating_set.py +0 -149
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/kcomponents.py +0 -369
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/matching.py +0 -44
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/maxcut.py +0 -143
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/ramsey.py +0 -53
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/steinertree.py +0 -265
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/traveling_salesman.py +0 -1508
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/treewidth.py +0 -255
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/vertex_cover.py +0 -83
- package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/__init__.py +0 -5
- package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/connectivity.py +0 -122
- package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/correlation.py +0 -302
- package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/mixing.py +0 -255
- package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/neighbor_degree.py +0 -160
- package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/pairs.py +0 -127
- package/runtime/python/okstra_vendor/networkx/algorithms/asteroidal.py +0 -164
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/__init__.py +0 -88
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/basic.py +0 -322
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/centrality.py +0 -290
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/cluster.py +0 -289
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/covering.py +0 -57
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/edgelist.py +0 -360
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/extendability.py +0 -105
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/generators.py +0 -603
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/link_analysis.py +0 -316
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/matching.py +0 -590
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/matrix.py +0 -232
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/projection.py +0 -526
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/redundancy.py +0 -112
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/spectral.py +0 -69
- package/runtime/python/okstra_vendor/networkx/algorithms/boundary.py +0 -168
- package/runtime/python/okstra_vendor/networkx/algorithms/bridges.py +0 -205
- package/runtime/python/okstra_vendor/networkx/algorithms/broadcasting.py +0 -164
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/__init__.py +0 -20
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/betweenness.py +0 -591
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/betweenness_subset.py +0 -236
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/closeness.py +0 -282
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/current_flow_betweenness.py +0 -364
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/current_flow_betweenness_subset.py +0 -227
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/current_flow_closeness.py +0 -96
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/degree_alg.py +0 -150
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/dispersion.py +0 -107
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/eigenvector.py +0 -357
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/flow_matrix.py +0 -130
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/group.py +0 -787
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/harmonic.py +0 -88
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/katz.py +0 -331
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/laplacian.py +0 -150
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/load.py +0 -200
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/percolation.py +0 -128
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/reaching.py +0 -209
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/second_order.py +0 -141
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/subgraph_alg.py +0 -361
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/trophic.py +0 -181
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/voterank_alg.py +0 -95
- package/runtime/python/okstra_vendor/networkx/algorithms/chains.py +0 -172
- package/runtime/python/okstra_vendor/networkx/algorithms/chordal.py +0 -443
- package/runtime/python/okstra_vendor/networkx/algorithms/clique.py +0 -818
- package/runtime/python/okstra_vendor/networkx/algorithms/cluster.py +0 -732
- package/runtime/python/okstra_vendor/networkx/algorithms/coloring/__init__.py +0 -4
- package/runtime/python/okstra_vendor/networkx/algorithms/coloring/equitable_coloring.py +0 -505
- package/runtime/python/okstra_vendor/networkx/algorithms/coloring/greedy_coloring.py +0 -565
- package/runtime/python/okstra_vendor/networkx/algorithms/communicability_alg.py +0 -163
- package/runtime/python/okstra_vendor/networkx/algorithms/community/__init__.py +0 -28
- package/runtime/python/okstra_vendor/networkx/algorithms/community/asyn_fluid.py +0 -153
- package/runtime/python/okstra_vendor/networkx/algorithms/community/bipartitions.py +0 -354
- package/runtime/python/okstra_vendor/networkx/algorithms/community/centrality.py +0 -171
- package/runtime/python/okstra_vendor/networkx/algorithms/community/community_utils.py +0 -30
- package/runtime/python/okstra_vendor/networkx/algorithms/community/divisive.py +0 -216
- package/runtime/python/okstra_vendor/networkx/algorithms/community/kclique.py +0 -79
- package/runtime/python/okstra_vendor/networkx/algorithms/community/label_propagation.py +0 -338
- package/runtime/python/okstra_vendor/networkx/algorithms/community/leiden.py +0 -162
- package/runtime/python/okstra_vendor/networkx/algorithms/community/local.py +0 -220
- package/runtime/python/okstra_vendor/networkx/algorithms/community/louvain.py +0 -384
- package/runtime/python/okstra_vendor/networkx/algorithms/community/lukes.py +0 -227
- package/runtime/python/okstra_vendor/networkx/algorithms/community/modularity_max.py +0 -452
- package/runtime/python/okstra_vendor/networkx/algorithms/community/quality.py +0 -347
- package/runtime/python/okstra_vendor/networkx/algorithms/components/__init__.py +0 -6
- package/runtime/python/okstra_vendor/networkx/algorithms/components/attracting.py +0 -115
- package/runtime/python/okstra_vendor/networkx/algorithms/components/biconnected.py +0 -394
- package/runtime/python/okstra_vendor/networkx/algorithms/components/connected.py +0 -282
- package/runtime/python/okstra_vendor/networkx/algorithms/components/semiconnected.py +0 -71
- package/runtime/python/okstra_vendor/networkx/algorithms/components/strongly_connected.py +0 -359
- package/runtime/python/okstra_vendor/networkx/algorithms/components/weakly_connected.py +0 -196
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/__init__.py +0 -11
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/connectivity.py +0 -811
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/cuts.py +0 -616
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/disjoint_paths.py +0 -408
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/edge_augmentation.py +0 -1270
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/edge_kcomponents.py +0 -592
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/kcomponents.py +0 -220
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/kcutsets.py +0 -235
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/stoerwagner.py +0 -152
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/utils.py +0 -88
- package/runtime/python/okstra_vendor/networkx/algorithms/core.py +0 -588
- package/runtime/python/okstra_vendor/networkx/algorithms/covering.py +0 -142
- package/runtime/python/okstra_vendor/networkx/algorithms/cuts.py +0 -416
- package/runtime/python/okstra_vendor/networkx/algorithms/cycles.py +0 -1234
- package/runtime/python/okstra_vendor/networkx/algorithms/d_separation.py +0 -677
- package/runtime/python/okstra_vendor/networkx/algorithms/dag.py +0 -1392
- package/runtime/python/okstra_vendor/networkx/algorithms/distance_measures.py +0 -1095
- package/runtime/python/okstra_vendor/networkx/algorithms/distance_regular.py +0 -272
- package/runtime/python/okstra_vendor/networkx/algorithms/dominance.py +0 -142
- package/runtime/python/okstra_vendor/networkx/algorithms/dominating.py +0 -268
- package/runtime/python/okstra_vendor/networkx/algorithms/efficiency_measures.py +0 -167
- package/runtime/python/okstra_vendor/networkx/algorithms/euler.py +0 -470
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/__init__.py +0 -11
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/boykovkolmogorov.py +0 -370
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/capacityscaling.py +0 -407
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/dinitz_alg.py +0 -238
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/edmondskarp.py +0 -241
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/gomory_hu.py +0 -178
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/maxflow.py +0 -611
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/mincost.py +0 -356
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/networksimplex.py +0 -662
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/preflowpush.py +0 -425
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/shortestaugmentingpath.py +0 -300
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/utils.py +0 -194
- package/runtime/python/okstra_vendor/networkx/algorithms/graph_hashing.py +0 -435
- package/runtime/python/okstra_vendor/networkx/algorithms/graphical.py +0 -483
- package/runtime/python/okstra_vendor/networkx/algorithms/hierarchy.py +0 -57
- package/runtime/python/okstra_vendor/networkx/algorithms/hybrid.py +0 -196
- package/runtime/python/okstra_vendor/networkx/algorithms/isolate.py +0 -107
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/__init__.py +0 -7
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/ismags.py +0 -1306
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/isomorph.py +0 -336
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/isomorphvf2.py +0 -1262
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/matchhelpers.py +0 -352
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/temporalisomorphvf2.py +0 -308
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tree_isomorphism.py +0 -264
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/vf2pp.py +0 -1102
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/vf2userfunc.py +0 -192
- package/runtime/python/okstra_vendor/networkx/algorithms/link_analysis/__init__.py +0 -2
- package/runtime/python/okstra_vendor/networkx/algorithms/link_analysis/hits_alg.py +0 -337
- package/runtime/python/okstra_vendor/networkx/algorithms/link_analysis/pagerank_alg.py +0 -498
- package/runtime/python/okstra_vendor/networkx/algorithms/link_prediction.py +0 -687
- package/runtime/python/okstra_vendor/networkx/algorithms/lowest_common_ancestors.py +0 -280
- package/runtime/python/okstra_vendor/networkx/algorithms/matching.py +0 -1148
- package/runtime/python/okstra_vendor/networkx/algorithms/minors/__init__.py +0 -27
- package/runtime/python/okstra_vendor/networkx/algorithms/minors/contraction.py +0 -738
- package/runtime/python/okstra_vendor/networkx/algorithms/mis.py +0 -78
- package/runtime/python/okstra_vendor/networkx/algorithms/moral.py +0 -59
- package/runtime/python/okstra_vendor/networkx/algorithms/node_classification.py +0 -219
- package/runtime/python/okstra_vendor/networkx/algorithms/non_randomness.py +0 -155
- package/runtime/python/okstra_vendor/networkx/algorithms/operators/__init__.py +0 -4
- package/runtime/python/okstra_vendor/networkx/algorithms/operators/all.py +0 -324
- package/runtime/python/okstra_vendor/networkx/algorithms/operators/binary.py +0 -468
- package/runtime/python/okstra_vendor/networkx/algorithms/operators/product.py +0 -633
- package/runtime/python/okstra_vendor/networkx/algorithms/operators/unary.py +0 -77
- package/runtime/python/okstra_vendor/networkx/algorithms/perfect_graph.py +0 -73
- package/runtime/python/okstra_vendor/networkx/algorithms/planar_drawing.py +0 -464
- package/runtime/python/okstra_vendor/networkx/algorithms/planarity.py +0 -1463
- package/runtime/python/okstra_vendor/networkx/algorithms/polynomials.py +0 -306
- package/runtime/python/okstra_vendor/networkx/algorithms/reciprocity.py +0 -98
- package/runtime/python/okstra_vendor/networkx/algorithms/regular.py +0 -167
- package/runtime/python/okstra_vendor/networkx/algorithms/richclub.py +0 -138
- package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/__init__.py +0 -5
- package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/astar.py +0 -239
- package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/dense.py +0 -264
- package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/generic.py +0 -716
- package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/unweighted.py +0 -625
- package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/weighted.py +0 -2542
- package/runtime/python/okstra_vendor/networkx/algorithms/similarity.py +0 -2107
- package/runtime/python/okstra_vendor/networkx/algorithms/simple_paths.py +0 -966
- package/runtime/python/okstra_vendor/networkx/algorithms/smallworld.py +0 -404
- package/runtime/python/okstra_vendor/networkx/algorithms/smetric.py +0 -30
- package/runtime/python/okstra_vendor/networkx/algorithms/sparsifiers.py +0 -296
- package/runtime/python/okstra_vendor/networkx/algorithms/structuralholes.py +0 -374
- package/runtime/python/okstra_vendor/networkx/algorithms/summarization.py +0 -564
- package/runtime/python/okstra_vendor/networkx/algorithms/swap.py +0 -406
- package/runtime/python/okstra_vendor/networkx/algorithms/threshold.py +0 -981
- package/runtime/python/okstra_vendor/networkx/algorithms/time_dependent.py +0 -142
- package/runtime/python/okstra_vendor/networkx/algorithms/tournament.py +0 -406
- package/runtime/python/okstra_vendor/networkx/algorithms/traversal/__init__.py +0 -5
- package/runtime/python/okstra_vendor/networkx/algorithms/traversal/beamsearch.py +0 -90
- package/runtime/python/okstra_vendor/networkx/algorithms/traversal/breadth_first_search.py +0 -576
- package/runtime/python/okstra_vendor/networkx/algorithms/traversal/depth_first_search.py +0 -529
- package/runtime/python/okstra_vendor/networkx/algorithms/traversal/edgebfs.py +0 -185
- package/runtime/python/okstra_vendor/networkx/algorithms/traversal/edgedfs.py +0 -182
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/__init__.py +0 -7
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/branchings.py +0 -1042
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/coding.py +0 -413
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/decomposition.py +0 -88
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/distance_measures.py +0 -219
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/mst.py +0 -1281
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/operations.py +0 -106
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/recognition.py +0 -273
- package/runtime/python/okstra_vendor/networkx/algorithms/triads.py +0 -500
- package/runtime/python/okstra_vendor/networkx/algorithms/vitality.py +0 -76
- package/runtime/python/okstra_vendor/networkx/algorithms/voronoi.py +0 -86
- package/runtime/python/okstra_vendor/networkx/algorithms/walks.py +0 -77
- package/runtime/python/okstra_vendor/networkx/algorithms/wiener.py +0 -278
- package/runtime/python/okstra_vendor/networkx/classes/__init__.py +0 -13
- package/runtime/python/okstra_vendor/networkx/classes/coreviews.py +0 -435
- package/runtime/python/okstra_vendor/networkx/classes/digraph.py +0 -1363
- package/runtime/python/okstra_vendor/networkx/classes/filters.py +0 -95
- package/runtime/python/okstra_vendor/networkx/classes/function.py +0 -1549
- package/runtime/python/okstra_vendor/networkx/classes/graph.py +0 -2082
- package/runtime/python/okstra_vendor/networkx/classes/graphviews.py +0 -269
- package/runtime/python/okstra_vendor/networkx/classes/multidigraph.py +0 -977
- package/runtime/python/okstra_vendor/networkx/classes/multigraph.py +0 -1294
- package/runtime/python/okstra_vendor/networkx/classes/reportviews.py +0 -1447
- package/runtime/python/okstra_vendor/networkx/convert.py +0 -502
- package/runtime/python/okstra_vendor/networkx/convert_matrix.py +0 -1314
- package/runtime/python/okstra_vendor/networkx/drawing/__init__.py +0 -7
- package/runtime/python/okstra_vendor/networkx/drawing/layout.py +0 -2036
- package/runtime/python/okstra_vendor/networkx/drawing/nx_agraph.py +0 -470
- package/runtime/python/okstra_vendor/networkx/drawing/nx_latex.py +0 -570
- package/runtime/python/okstra_vendor/networkx/drawing/nx_pydot.py +0 -361
- package/runtime/python/okstra_vendor/networkx/drawing/nx_pylab.py +0 -2978
- package/runtime/python/okstra_vendor/networkx/exception.py +0 -131
- package/runtime/python/okstra_vendor/networkx/generators/__init__.py +0 -34
- package/runtime/python/okstra_vendor/networkx/generators/atlas.dat.gz +0 -0
- package/runtime/python/okstra_vendor/networkx/generators/atlas.py +0 -227
- package/runtime/python/okstra_vendor/networkx/generators/classic.py +0 -1091
- package/runtime/python/okstra_vendor/networkx/generators/cographs.py +0 -68
- package/runtime/python/okstra_vendor/networkx/generators/community.py +0 -1070
- package/runtime/python/okstra_vendor/networkx/generators/degree_seq.py +0 -886
- package/runtime/python/okstra_vendor/networkx/generators/directed.py +0 -572
- package/runtime/python/okstra_vendor/networkx/generators/duplication.py +0 -174
- package/runtime/python/okstra_vendor/networkx/generators/ego.py +0 -66
- package/runtime/python/okstra_vendor/networkx/generators/expanders.py +0 -499
- package/runtime/python/okstra_vendor/networkx/generators/geometric.py +0 -1037
- package/runtime/python/okstra_vendor/networkx/generators/harary_graph.py +0 -163
- package/runtime/python/okstra_vendor/networkx/generators/internet_as_graphs.py +0 -443
- package/runtime/python/okstra_vendor/networkx/generators/intersection.py +0 -125
- package/runtime/python/okstra_vendor/networkx/generators/interval_graph.py +0 -70
- package/runtime/python/okstra_vendor/networkx/generators/joint_degree_seq.py +0 -664
- package/runtime/python/okstra_vendor/networkx/generators/lattice.py +0 -405
- package/runtime/python/okstra_vendor/networkx/generators/line.py +0 -501
- package/runtime/python/okstra_vendor/networkx/generators/mycielski.py +0 -110
- package/runtime/python/okstra_vendor/networkx/generators/nonisomorphic_trees.py +0 -259
- package/runtime/python/okstra_vendor/networkx/generators/random_clustered.py +0 -117
- package/runtime/python/okstra_vendor/networkx/generators/random_graphs.py +0 -1416
- package/runtime/python/okstra_vendor/networkx/generators/small.py +0 -1070
- package/runtime/python/okstra_vendor/networkx/generators/social.py +0 -554
- package/runtime/python/okstra_vendor/networkx/generators/spectral_graph_forge.py +0 -120
- package/runtime/python/okstra_vendor/networkx/generators/stochastic.py +0 -54
- package/runtime/python/okstra_vendor/networkx/generators/sudoku.py +0 -131
- package/runtime/python/okstra_vendor/networkx/generators/time_series.py +0 -74
- package/runtime/python/okstra_vendor/networkx/generators/trees.py +0 -1070
- package/runtime/python/okstra_vendor/networkx/generators/triads.py +0 -94
- package/runtime/python/okstra_vendor/networkx/lazy_imports.py +0 -188
- package/runtime/python/okstra_vendor/networkx/linalg/__init__.py +0 -13
- package/runtime/python/okstra_vendor/networkx/linalg/algebraicconnectivity.py +0 -650
- package/runtime/python/okstra_vendor/networkx/linalg/attrmatrix.py +0 -466
- package/runtime/python/okstra_vendor/networkx/linalg/bethehessianmatrix.py +0 -77
- package/runtime/python/okstra_vendor/networkx/linalg/graphmatrix.py +0 -168
- package/runtime/python/okstra_vendor/networkx/linalg/laplacianmatrix.py +0 -512
- package/runtime/python/okstra_vendor/networkx/linalg/modularitymatrix.py +0 -166
- package/runtime/python/okstra_vendor/networkx/linalg/spectrum.py +0 -186
- package/runtime/python/okstra_vendor/networkx/readwrite/__init__.py +0 -17
- package/runtime/python/okstra_vendor/networkx/readwrite/adjlist.py +0 -330
- package/runtime/python/okstra_vendor/networkx/readwrite/edgelist.py +0 -489
- package/runtime/python/okstra_vendor/networkx/readwrite/gexf.py +0 -1084
- package/runtime/python/okstra_vendor/networkx/readwrite/gml.py +0 -879
- package/runtime/python/okstra_vendor/networkx/readwrite/graph6.py +0 -427
- package/runtime/python/okstra_vendor/networkx/readwrite/graphml.py +0 -1053
- package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/__init__.py +0 -19
- package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/adjacency.py +0 -156
- package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/cytoscape.py +0 -190
- package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/node_link.py +0 -261
- package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/tree.py +0 -137
- package/runtime/python/okstra_vendor/networkx/readwrite/leda.py +0 -108
- package/runtime/python/okstra_vendor/networkx/readwrite/multiline_adjlist.py +0 -393
- package/runtime/python/okstra_vendor/networkx/readwrite/p2g.py +0 -113
- package/runtime/python/okstra_vendor/networkx/readwrite/pajek.py +0 -286
- package/runtime/python/okstra_vendor/networkx/readwrite/sparse6.py +0 -379
- package/runtime/python/okstra_vendor/networkx/readwrite/text.py +0 -851
- package/runtime/python/okstra_vendor/networkx/relabel.py +0 -285
- package/runtime/python/okstra_vendor/networkx/utils/__init__.py +0 -8
- package/runtime/python/okstra_vendor/networkx/utils/backends.py +0 -2171
- package/runtime/python/okstra_vendor/networkx/utils/configs.py +0 -396
- package/runtime/python/okstra_vendor/networkx/utils/decorators.py +0 -1233
- package/runtime/python/okstra_vendor/networkx/utils/heaps.py +0 -338
- package/runtime/python/okstra_vendor/networkx/utils/mapped_queue.py +0 -297
- package/runtime/python/okstra_vendor/networkx/utils/misc.py +0 -703
- package/runtime/python/okstra_vendor/networkx/utils/random_sequence.py +0 -198
- package/runtime/python/okstra_vendor/networkx/utils/rcm.py +0 -159
- package/runtime/python/okstra_vendor/networkx/utils/union_find.py +0 -106
- package/runtime/skills/okstra-graphify/SKILL.md +0 -169
- package/src/commands/graphify.mjs +0 -32
|
@@ -1,3277 +0,0 @@
|
|
|
1
|
-
"""Deterministic structural extraction from source code using tree-sitter. Outputs nodes+edges dicts."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
import importlib
|
|
4
|
-
import json
|
|
5
|
-
import os
|
|
6
|
-
import re
|
|
7
|
-
import sys
|
|
8
|
-
from dataclasses import dataclass, field
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from typing import Callable, Any
|
|
11
|
-
from .cache import load_cached, save_cached
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def _make_id(*parts: str) -> str:
|
|
15
|
-
"""Build a stable node ID from one or more name parts."""
|
|
16
|
-
combined = "_".join(p.strip("_.") for p in parts if p)
|
|
17
|
-
cleaned = re.sub(r"[^a-zA-Z0-9]+", "_", combined)
|
|
18
|
-
return cleaned.strip("_").lower()
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
# ── LanguageConfig dataclass ─────────────────────────────────────────────────
|
|
22
|
-
|
|
23
|
-
@dataclass
|
|
24
|
-
class LanguageConfig:
|
|
25
|
-
ts_module: str # e.g. "tree_sitter_python"
|
|
26
|
-
ts_language_fn: str = "language" # attr to call: e.g. tslang.language()
|
|
27
|
-
|
|
28
|
-
class_types: frozenset = frozenset()
|
|
29
|
-
function_types: frozenset = frozenset()
|
|
30
|
-
import_types: frozenset = frozenset()
|
|
31
|
-
call_types: frozenset = frozenset()
|
|
32
|
-
static_prop_types: frozenset = frozenset()
|
|
33
|
-
helper_fn_names: frozenset = frozenset()
|
|
34
|
-
container_bind_methods: frozenset = frozenset()
|
|
35
|
-
event_listener_properties: frozenset = frozenset()
|
|
36
|
-
|
|
37
|
-
# Name extraction
|
|
38
|
-
name_field: str = "name"
|
|
39
|
-
name_fallback_child_types: tuple = ()
|
|
40
|
-
|
|
41
|
-
# Body detection
|
|
42
|
-
body_field: str = "body"
|
|
43
|
-
body_fallback_child_types: tuple = () # e.g. ("declaration_list", "compound_statement")
|
|
44
|
-
|
|
45
|
-
# Call name extraction
|
|
46
|
-
call_function_field: str = "function" # field on call node for callee
|
|
47
|
-
call_accessor_node_types: frozenset = frozenset() # member/attribute nodes
|
|
48
|
-
call_accessor_field: str = "attribute" # field on accessor for method name
|
|
49
|
-
|
|
50
|
-
# Stop recursion at these types in walk_calls
|
|
51
|
-
function_boundary_types: frozenset = frozenset()
|
|
52
|
-
|
|
53
|
-
# Import handler: called for import nodes instead of generic handling
|
|
54
|
-
import_handler: Callable | None = None
|
|
55
|
-
|
|
56
|
-
# Optional custom name resolver for functions (C, C++ declarator unwrapping)
|
|
57
|
-
resolve_function_name_fn: Callable | None = None
|
|
58
|
-
|
|
59
|
-
# Extra label formatting for functions: if True, functions get "name()" label
|
|
60
|
-
function_label_parens: bool = True
|
|
61
|
-
|
|
62
|
-
# Extra walk hook called after generic dispatch (for JS arrow functions, C# namespaces, etc.)
|
|
63
|
-
extra_walk_fn: Callable | None = None
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
# ── Generic helpers ───────────────────────────────────────────────────────────
|
|
67
|
-
|
|
68
|
-
def _read_text(node, source: bytes) -> str:
|
|
69
|
-
return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def _resolve_name(node, source: bytes, config: LanguageConfig) -> str | None:
|
|
73
|
-
"""Get the name from a node using config.name_field, falling back to child types."""
|
|
74
|
-
if config.resolve_function_name_fn is not None:
|
|
75
|
-
# For C/C++ where the name is inside a declarator
|
|
76
|
-
return None # caller handles this separately
|
|
77
|
-
n = node.child_by_field_name(config.name_field)
|
|
78
|
-
if n:
|
|
79
|
-
return _read_text(n, source)
|
|
80
|
-
for child in node.children:
|
|
81
|
-
if child.type in config.name_fallback_child_types:
|
|
82
|
-
return _read_text(child, source)
|
|
83
|
-
return None
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def _find_body(node, config: LanguageConfig):
|
|
87
|
-
"""Find the body node using config.body_field, falling back to child types."""
|
|
88
|
-
b = node.child_by_field_name(config.body_field)
|
|
89
|
-
if b:
|
|
90
|
-
return b
|
|
91
|
-
for child in node.children:
|
|
92
|
-
if child.type in config.body_fallback_child_types:
|
|
93
|
-
return child
|
|
94
|
-
return None
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
# ── Import handlers ───────────────────────────────────────────────────────────
|
|
98
|
-
|
|
99
|
-
def _import_python(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None:
|
|
100
|
-
t = node.type
|
|
101
|
-
if t == "import_statement":
|
|
102
|
-
for child in node.children:
|
|
103
|
-
if child.type in ("dotted_name", "aliased_import"):
|
|
104
|
-
raw = _read_text(child, source)
|
|
105
|
-
module_name = raw.split(" as ")[0].strip().lstrip(".")
|
|
106
|
-
tgt_nid = _make_id(module_name)
|
|
107
|
-
edges.append({
|
|
108
|
-
"source": file_nid,
|
|
109
|
-
"target": tgt_nid,
|
|
110
|
-
"relation": "imports",
|
|
111
|
-
"confidence": "EXTRACTED",
|
|
112
|
-
"source_file": str_path,
|
|
113
|
-
"source_location": f"L{node.start_point[0] + 1}",
|
|
114
|
-
"weight": 1.0,
|
|
115
|
-
})
|
|
116
|
-
elif t == "import_from_statement":
|
|
117
|
-
module_node = node.child_by_field_name("module_name")
|
|
118
|
-
if module_node:
|
|
119
|
-
raw = _read_text(module_node, source)
|
|
120
|
-
if raw.startswith("."):
|
|
121
|
-
# Relative import - resolve to full path so IDs match file node IDs
|
|
122
|
-
dots = len(raw) - len(raw.lstrip("."))
|
|
123
|
-
module_name = raw.lstrip(".")
|
|
124
|
-
base = Path(str_path).parent
|
|
125
|
-
for _ in range(dots - 1):
|
|
126
|
-
base = base.parent
|
|
127
|
-
rel = (module_name.replace(".", "/") + ".py") if module_name else "__init__.py"
|
|
128
|
-
tgt_nid = _make_id(str(base / rel))
|
|
129
|
-
else:
|
|
130
|
-
tgt_nid = _make_id(raw)
|
|
131
|
-
edges.append({
|
|
132
|
-
"source": file_nid,
|
|
133
|
-
"target": tgt_nid,
|
|
134
|
-
"relation": "imports_from",
|
|
135
|
-
"confidence": "EXTRACTED",
|
|
136
|
-
"source_file": str_path,
|
|
137
|
-
"source_location": f"L{node.start_point[0] + 1}",
|
|
138
|
-
"weight": 1.0,
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
def _import_js(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None:
|
|
143
|
-
for child in node.children:
|
|
144
|
-
if child.type == "string":
|
|
145
|
-
raw = _read_text(child, source).strip("'\"` ")
|
|
146
|
-
if not raw:
|
|
147
|
-
break
|
|
148
|
-
if raw.startswith("."):
|
|
149
|
-
# Relative import - resolve to full path so IDs match file node IDs
|
|
150
|
-
# normpath removes ".." segments so the ID matches the target file's own node ID
|
|
151
|
-
resolved = Path(os.path.normpath(Path(str_path).parent / raw))
|
|
152
|
-
# TypeScript ESM: imports written as .js but actual file is .ts/.tsx
|
|
153
|
-
if resolved.suffix == ".js":
|
|
154
|
-
resolved = resolved.with_suffix(".ts")
|
|
155
|
-
elif resolved.suffix == ".jsx":
|
|
156
|
-
resolved = resolved.with_suffix(".tsx")
|
|
157
|
-
tgt_nid = _make_id(str(resolved))
|
|
158
|
-
else:
|
|
159
|
-
# Bare/scoped import (node_modules) - use last segment; dropped as external
|
|
160
|
-
module_name = raw.split("/")[-1]
|
|
161
|
-
if not module_name:
|
|
162
|
-
break
|
|
163
|
-
tgt_nid = _make_id(module_name)
|
|
164
|
-
edges.append({
|
|
165
|
-
"source": file_nid,
|
|
166
|
-
"target": tgt_nid,
|
|
167
|
-
"relation": "imports_from",
|
|
168
|
-
"confidence": "EXTRACTED",
|
|
169
|
-
"source_file": str_path,
|
|
170
|
-
"source_location": f"L{node.start_point[0] + 1}",
|
|
171
|
-
"weight": 1.0,
|
|
172
|
-
})
|
|
173
|
-
break
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def _import_java(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None:
|
|
177
|
-
def _walk_scoped(n) -> str:
|
|
178
|
-
parts: list[str] = []
|
|
179
|
-
cur = n
|
|
180
|
-
while cur:
|
|
181
|
-
if cur.type == "scoped_identifier":
|
|
182
|
-
name_node = cur.child_by_field_name("name")
|
|
183
|
-
if name_node:
|
|
184
|
-
parts.append(_read_text(name_node, source))
|
|
185
|
-
cur = cur.child_by_field_name("scope")
|
|
186
|
-
elif cur.type == "identifier":
|
|
187
|
-
parts.append(_read_text(cur, source))
|
|
188
|
-
break
|
|
189
|
-
else:
|
|
190
|
-
break
|
|
191
|
-
parts.reverse()
|
|
192
|
-
return ".".join(parts)
|
|
193
|
-
|
|
194
|
-
for child in node.children:
|
|
195
|
-
if child.type in ("scoped_identifier", "identifier"):
|
|
196
|
-
path_str = _walk_scoped(child)
|
|
197
|
-
module_name = path_str.split(".")[-1].strip("*").strip(".") or (
|
|
198
|
-
path_str.split(".")[-2] if len(path_str.split(".")) > 1 else path_str
|
|
199
|
-
)
|
|
200
|
-
if module_name:
|
|
201
|
-
tgt_nid = _make_id(module_name)
|
|
202
|
-
edges.append({
|
|
203
|
-
"source": file_nid,
|
|
204
|
-
"target": tgt_nid,
|
|
205
|
-
"relation": "imports",
|
|
206
|
-
"confidence": "EXTRACTED",
|
|
207
|
-
"source_file": str_path,
|
|
208
|
-
"source_location": f"L{node.start_point[0] + 1}",
|
|
209
|
-
"weight": 1.0,
|
|
210
|
-
})
|
|
211
|
-
break
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
def _import_c(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None:
|
|
215
|
-
for child in node.children:
|
|
216
|
-
if child.type in ("string_literal", "system_lib_string", "string"):
|
|
217
|
-
raw = _read_text(child, source).strip('"<> ')
|
|
218
|
-
module_name = raw.split("/")[-1].split(".")[0]
|
|
219
|
-
if module_name:
|
|
220
|
-
tgt_nid = _make_id(module_name)
|
|
221
|
-
edges.append({
|
|
222
|
-
"source": file_nid,
|
|
223
|
-
"target": tgt_nid,
|
|
224
|
-
"relation": "imports",
|
|
225
|
-
"confidence": "EXTRACTED",
|
|
226
|
-
"source_file": str_path,
|
|
227
|
-
"source_location": f"L{node.start_point[0] + 1}",
|
|
228
|
-
"weight": 1.0,
|
|
229
|
-
})
|
|
230
|
-
break
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
def _import_csharp(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None:
|
|
234
|
-
for child in node.children:
|
|
235
|
-
if child.type in ("qualified_name", "identifier", "name_equals"):
|
|
236
|
-
raw = _read_text(child, source)
|
|
237
|
-
module_name = raw.split(".")[-1].strip()
|
|
238
|
-
if module_name:
|
|
239
|
-
tgt_nid = _make_id(module_name)
|
|
240
|
-
edges.append({
|
|
241
|
-
"source": file_nid,
|
|
242
|
-
"target": tgt_nid,
|
|
243
|
-
"relation": "imports",
|
|
244
|
-
"confidence": "EXTRACTED",
|
|
245
|
-
"source_file": str_path,
|
|
246
|
-
"source_location": f"L{node.start_point[0] + 1}",
|
|
247
|
-
"weight": 1.0,
|
|
248
|
-
})
|
|
249
|
-
break
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
def _import_kotlin(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None:
|
|
253
|
-
path_node = node.child_by_field_name("path")
|
|
254
|
-
if path_node:
|
|
255
|
-
raw = _read_text(path_node, source)
|
|
256
|
-
module_name = raw.split(".")[-1].strip()
|
|
257
|
-
if module_name:
|
|
258
|
-
tgt_nid = _make_id(module_name)
|
|
259
|
-
edges.append({
|
|
260
|
-
"source": file_nid,
|
|
261
|
-
"target": tgt_nid,
|
|
262
|
-
"relation": "imports",
|
|
263
|
-
"confidence": "EXTRACTED",
|
|
264
|
-
"source_file": str_path,
|
|
265
|
-
"source_location": f"L{node.start_point[0] + 1}",
|
|
266
|
-
"weight": 1.0,
|
|
267
|
-
})
|
|
268
|
-
return
|
|
269
|
-
# Fallback: find identifier child
|
|
270
|
-
for child in node.children:
|
|
271
|
-
if child.type == "identifier":
|
|
272
|
-
raw = _read_text(child, source)
|
|
273
|
-
tgt_nid = _make_id(raw)
|
|
274
|
-
edges.append({
|
|
275
|
-
"source": file_nid,
|
|
276
|
-
"target": tgt_nid,
|
|
277
|
-
"relation": "imports",
|
|
278
|
-
"confidence": "EXTRACTED",
|
|
279
|
-
"source_file": str_path,
|
|
280
|
-
"source_location": f"L{node.start_point[0] + 1}",
|
|
281
|
-
"weight": 1.0,
|
|
282
|
-
})
|
|
283
|
-
break
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
def _import_scala(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None:
|
|
287
|
-
for child in node.children:
|
|
288
|
-
if child.type in ("stable_id", "identifier"):
|
|
289
|
-
raw = _read_text(child, source)
|
|
290
|
-
module_name = raw.split(".")[-1].strip("{} ")
|
|
291
|
-
if module_name and module_name != "_":
|
|
292
|
-
tgt_nid = _make_id(module_name)
|
|
293
|
-
edges.append({
|
|
294
|
-
"source": file_nid,
|
|
295
|
-
"target": tgt_nid,
|
|
296
|
-
"relation": "imports",
|
|
297
|
-
"confidence": "EXTRACTED",
|
|
298
|
-
"source_file": str_path,
|
|
299
|
-
"source_location": f"L{node.start_point[0] + 1}",
|
|
300
|
-
"weight": 1.0,
|
|
301
|
-
})
|
|
302
|
-
break
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
def _import_php(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None:
|
|
306
|
-
for child in node.children:
|
|
307
|
-
if child.type in ("qualified_name", "name", "identifier"):
|
|
308
|
-
raw = _read_text(child, source)
|
|
309
|
-
module_name = raw.split("\\")[-1].strip()
|
|
310
|
-
if module_name:
|
|
311
|
-
tgt_nid = _make_id(module_name)
|
|
312
|
-
edges.append({
|
|
313
|
-
"source": file_nid,
|
|
314
|
-
"target": tgt_nid,
|
|
315
|
-
"relation": "imports",
|
|
316
|
-
"confidence": "EXTRACTED",
|
|
317
|
-
"source_file": str_path,
|
|
318
|
-
"source_location": f"L{node.start_point[0] + 1}",
|
|
319
|
-
"weight": 1.0,
|
|
320
|
-
})
|
|
321
|
-
break
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
# ── C/C++ function name helpers ───────────────────────────────────────────────
|
|
325
|
-
|
|
326
|
-
def _get_c_func_name(node, source: bytes) -> str | None:
|
|
327
|
-
"""Recursively unwrap declarator to find the innermost identifier (C)."""
|
|
328
|
-
if node.type == "identifier":
|
|
329
|
-
return _read_text(node, source)
|
|
330
|
-
decl = node.child_by_field_name("declarator")
|
|
331
|
-
if decl:
|
|
332
|
-
return _get_c_func_name(decl, source)
|
|
333
|
-
for child in node.children:
|
|
334
|
-
if child.type == "identifier":
|
|
335
|
-
return _read_text(child, source)
|
|
336
|
-
return None
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
def _get_cpp_func_name(node, source: bytes) -> str | None:
|
|
340
|
-
"""Recursively unwrap declarator to find the innermost identifier (C++)."""
|
|
341
|
-
if node.type == "identifier":
|
|
342
|
-
return _read_text(node, source)
|
|
343
|
-
if node.type == "qualified_identifier":
|
|
344
|
-
name_node = node.child_by_field_name("name")
|
|
345
|
-
if name_node:
|
|
346
|
-
return _read_text(name_node, source)
|
|
347
|
-
decl = node.child_by_field_name("declarator")
|
|
348
|
-
if decl:
|
|
349
|
-
return _get_cpp_func_name(decl, source)
|
|
350
|
-
for child in node.children:
|
|
351
|
-
if child.type == "identifier":
|
|
352
|
-
return _read_text(child, source)
|
|
353
|
-
return None
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
# ── JS/TS extra walk for arrow functions ──────────────────────────────────────
|
|
357
|
-
|
|
358
|
-
def _js_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: str,
|
|
359
|
-
nodes: list, edges: list, seen_ids: set, function_bodies: list,
|
|
360
|
-
parent_class_nid: str | None, add_node_fn, add_edge_fn) -> bool:
|
|
361
|
-
"""Handle lexical_declaration (arrow functions) for JS/TS. Returns True if handled."""
|
|
362
|
-
if node.type == "lexical_declaration":
|
|
363
|
-
for child in node.children:
|
|
364
|
-
if child.type == "variable_declarator":
|
|
365
|
-
value = child.child_by_field_name("value")
|
|
366
|
-
if value and value.type == "arrow_function":
|
|
367
|
-
name_node = child.child_by_field_name("name")
|
|
368
|
-
if name_node:
|
|
369
|
-
func_name = _read_text(name_node, source)
|
|
370
|
-
line = child.start_point[0] + 1
|
|
371
|
-
func_nid = _make_id(stem, func_name)
|
|
372
|
-
add_node_fn(func_nid, f"{func_name}()", line)
|
|
373
|
-
add_edge_fn(file_nid, func_nid, "contains", line)
|
|
374
|
-
body = value.child_by_field_name("body")
|
|
375
|
-
if body:
|
|
376
|
-
function_bodies.append((func_nid, body))
|
|
377
|
-
return True
|
|
378
|
-
return False
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
# ── C# extra walk for namespace declarations ──────────────────────────────────
|
|
382
|
-
|
|
383
|
-
def _csharp_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: str,
|
|
384
|
-
nodes: list, edges: list, seen_ids: set, function_bodies: list,
|
|
385
|
-
parent_class_nid: str | None, add_node_fn, add_edge_fn,
|
|
386
|
-
walk_fn) -> bool:
|
|
387
|
-
"""Handle namespace_declaration for C#. Returns True if handled."""
|
|
388
|
-
if node.type == "namespace_declaration":
|
|
389
|
-
name_node = node.child_by_field_name("name")
|
|
390
|
-
if name_node:
|
|
391
|
-
ns_name = _read_text(name_node, source)
|
|
392
|
-
ns_nid = _make_id(stem, ns_name)
|
|
393
|
-
line = node.start_point[0] + 1
|
|
394
|
-
add_node_fn(ns_nid, ns_name, line)
|
|
395
|
-
add_edge_fn(file_nid, ns_nid, "contains", line)
|
|
396
|
-
body = node.child_by_field_name("body")
|
|
397
|
-
if body:
|
|
398
|
-
for child in body.children:
|
|
399
|
-
walk_fn(child, parent_class_nid)
|
|
400
|
-
return True
|
|
401
|
-
return False
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
# ── Swift extra walk for enum cases ──────────────────────────────────────────
|
|
405
|
-
|
|
406
|
-
def _swift_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: str,
|
|
407
|
-
nodes: list, edges: list, seen_ids: set, function_bodies: list,
|
|
408
|
-
parent_class_nid: str | None, add_node_fn, add_edge_fn) -> bool:
|
|
409
|
-
"""Handle enum_entry for Swift. Returns True if handled."""
|
|
410
|
-
if node.type == "enum_entry" and parent_class_nid:
|
|
411
|
-
for child in node.children:
|
|
412
|
-
if child.type == "simple_identifier":
|
|
413
|
-
case_name = _read_text(child, source)
|
|
414
|
-
case_nid = _make_id(parent_class_nid, case_name)
|
|
415
|
-
line = node.start_point[0] + 1
|
|
416
|
-
add_node_fn(case_nid, case_name, line)
|
|
417
|
-
add_edge_fn(parent_class_nid, case_nid, "case_of", line)
|
|
418
|
-
return True
|
|
419
|
-
return False
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
# ── Language configs ──────────────────────────────────────────────────────────
|
|
423
|
-
|
|
424
|
-
_PYTHON_CONFIG = LanguageConfig(
|
|
425
|
-
ts_module="tree_sitter_python",
|
|
426
|
-
class_types=frozenset({"class_definition"}),
|
|
427
|
-
function_types=frozenset({"function_definition"}),
|
|
428
|
-
import_types=frozenset({"import_statement", "import_from_statement"}),
|
|
429
|
-
call_types=frozenset({"call"}),
|
|
430
|
-
call_function_field="function",
|
|
431
|
-
call_accessor_node_types=frozenset({"attribute"}),
|
|
432
|
-
call_accessor_field="attribute",
|
|
433
|
-
function_boundary_types=frozenset({"function_definition"}),
|
|
434
|
-
import_handler=_import_python,
|
|
435
|
-
)
|
|
436
|
-
|
|
437
|
-
_JS_CONFIG = LanguageConfig(
|
|
438
|
-
ts_module="tree_sitter_javascript",
|
|
439
|
-
class_types=frozenset({"class_declaration"}),
|
|
440
|
-
function_types=frozenset({"function_declaration", "method_definition"}),
|
|
441
|
-
import_types=frozenset({"import_statement"}),
|
|
442
|
-
call_types=frozenset({"call_expression"}),
|
|
443
|
-
call_function_field="function",
|
|
444
|
-
call_accessor_node_types=frozenset({"member_expression"}),
|
|
445
|
-
call_accessor_field="property",
|
|
446
|
-
function_boundary_types=frozenset({"function_declaration", "arrow_function", "method_definition"}),
|
|
447
|
-
import_handler=_import_js,
|
|
448
|
-
)
|
|
449
|
-
|
|
450
|
-
_TS_CONFIG = LanguageConfig(
|
|
451
|
-
ts_module="tree_sitter_typescript",
|
|
452
|
-
ts_language_fn="language_typescript",
|
|
453
|
-
class_types=frozenset({"class_declaration"}),
|
|
454
|
-
function_types=frozenset({"function_declaration", "method_definition"}),
|
|
455
|
-
import_types=frozenset({"import_statement"}),
|
|
456
|
-
call_types=frozenset({"call_expression"}),
|
|
457
|
-
call_function_field="function",
|
|
458
|
-
call_accessor_node_types=frozenset({"member_expression"}),
|
|
459
|
-
call_accessor_field="property",
|
|
460
|
-
function_boundary_types=frozenset({"function_declaration", "arrow_function", "method_definition"}),
|
|
461
|
-
import_handler=_import_js,
|
|
462
|
-
)
|
|
463
|
-
|
|
464
|
-
_JAVA_CONFIG = LanguageConfig(
|
|
465
|
-
ts_module="tree_sitter_java",
|
|
466
|
-
class_types=frozenset({"class_declaration", "interface_declaration"}),
|
|
467
|
-
function_types=frozenset({"method_declaration", "constructor_declaration"}),
|
|
468
|
-
import_types=frozenset({"import_declaration"}),
|
|
469
|
-
call_types=frozenset({"method_invocation"}),
|
|
470
|
-
call_function_field="name",
|
|
471
|
-
call_accessor_node_types=frozenset(),
|
|
472
|
-
function_boundary_types=frozenset({"method_declaration", "constructor_declaration"}),
|
|
473
|
-
import_handler=_import_java,
|
|
474
|
-
)
|
|
475
|
-
|
|
476
|
-
_C_CONFIG = LanguageConfig(
|
|
477
|
-
ts_module="tree_sitter_c",
|
|
478
|
-
class_types=frozenset(),
|
|
479
|
-
function_types=frozenset({"function_definition"}),
|
|
480
|
-
import_types=frozenset({"preproc_include"}),
|
|
481
|
-
call_types=frozenset({"call_expression"}),
|
|
482
|
-
call_function_field="function",
|
|
483
|
-
call_accessor_node_types=frozenset({"field_expression"}),
|
|
484
|
-
call_accessor_field="field",
|
|
485
|
-
function_boundary_types=frozenset({"function_definition"}),
|
|
486
|
-
import_handler=_import_c,
|
|
487
|
-
resolve_function_name_fn=_get_c_func_name,
|
|
488
|
-
)
|
|
489
|
-
|
|
490
|
-
_CPP_CONFIG = LanguageConfig(
|
|
491
|
-
ts_module="tree_sitter_cpp",
|
|
492
|
-
class_types=frozenset({"class_specifier"}),
|
|
493
|
-
function_types=frozenset({"function_definition"}),
|
|
494
|
-
import_types=frozenset({"preproc_include"}),
|
|
495
|
-
call_types=frozenset({"call_expression"}),
|
|
496
|
-
call_function_field="function",
|
|
497
|
-
call_accessor_node_types=frozenset({"field_expression", "qualified_identifier"}),
|
|
498
|
-
call_accessor_field="field",
|
|
499
|
-
function_boundary_types=frozenset({"function_definition"}),
|
|
500
|
-
import_handler=_import_c,
|
|
501
|
-
resolve_function_name_fn=_get_cpp_func_name,
|
|
502
|
-
)
|
|
503
|
-
|
|
504
|
-
_RUBY_CONFIG = LanguageConfig(
|
|
505
|
-
ts_module="tree_sitter_ruby",
|
|
506
|
-
class_types=frozenset({"class"}),
|
|
507
|
-
function_types=frozenset({"method", "singleton_method"}),
|
|
508
|
-
import_types=frozenset(),
|
|
509
|
-
call_types=frozenset({"call"}),
|
|
510
|
-
call_function_field="method",
|
|
511
|
-
call_accessor_node_types=frozenset(),
|
|
512
|
-
name_fallback_child_types=("constant", "scope_resolution", "identifier"),
|
|
513
|
-
body_fallback_child_types=("body_statement",),
|
|
514
|
-
function_boundary_types=frozenset({"method", "singleton_method"}),
|
|
515
|
-
)
|
|
516
|
-
|
|
517
|
-
_CSHARP_CONFIG = LanguageConfig(
|
|
518
|
-
ts_module="tree_sitter_c_sharp",
|
|
519
|
-
class_types=frozenset({"class_declaration", "interface_declaration"}),
|
|
520
|
-
function_types=frozenset({"method_declaration"}),
|
|
521
|
-
import_types=frozenset({"using_directive"}),
|
|
522
|
-
call_types=frozenset({"invocation_expression"}),
|
|
523
|
-
call_function_field="function",
|
|
524
|
-
call_accessor_node_types=frozenset({"member_access_expression"}),
|
|
525
|
-
call_accessor_field="name",
|
|
526
|
-
body_fallback_child_types=("declaration_list",),
|
|
527
|
-
function_boundary_types=frozenset({"method_declaration"}),
|
|
528
|
-
import_handler=_import_csharp,
|
|
529
|
-
)
|
|
530
|
-
|
|
531
|
-
_KOTLIN_CONFIG = LanguageConfig(
|
|
532
|
-
ts_module="tree_sitter_kotlin",
|
|
533
|
-
class_types=frozenset({"class_declaration", "object_declaration"}),
|
|
534
|
-
function_types=frozenset({"function_declaration"}),
|
|
535
|
-
import_types=frozenset({"import_header"}),
|
|
536
|
-
call_types=frozenset({"call_expression"}),
|
|
537
|
-
call_function_field="",
|
|
538
|
-
call_accessor_node_types=frozenset({"navigation_expression"}),
|
|
539
|
-
call_accessor_field="",
|
|
540
|
-
name_fallback_child_types=("simple_identifier",),
|
|
541
|
-
body_fallback_child_types=("function_body", "class_body"),
|
|
542
|
-
function_boundary_types=frozenset({"function_declaration"}),
|
|
543
|
-
import_handler=_import_kotlin,
|
|
544
|
-
)
|
|
545
|
-
|
|
546
|
-
_SCALA_CONFIG = LanguageConfig(
|
|
547
|
-
ts_module="tree_sitter_scala",
|
|
548
|
-
class_types=frozenset({"class_definition", "object_definition"}),
|
|
549
|
-
function_types=frozenset({"function_definition"}),
|
|
550
|
-
import_types=frozenset({"import_declaration"}),
|
|
551
|
-
call_types=frozenset({"call_expression"}),
|
|
552
|
-
call_function_field="",
|
|
553
|
-
call_accessor_node_types=frozenset({"field_expression"}),
|
|
554
|
-
call_accessor_field="field",
|
|
555
|
-
name_fallback_child_types=("identifier",),
|
|
556
|
-
body_fallback_child_types=("template_body",),
|
|
557
|
-
function_boundary_types=frozenset({"function_definition"}),
|
|
558
|
-
import_handler=_import_scala,
|
|
559
|
-
)
|
|
560
|
-
|
|
561
|
-
_PHP_CONFIG = LanguageConfig(
|
|
562
|
-
ts_module="tree_sitter_php",
|
|
563
|
-
ts_language_fn="language_php",
|
|
564
|
-
class_types=frozenset({"class_declaration"}),
|
|
565
|
-
function_types=frozenset({"function_definition", "method_declaration"}),
|
|
566
|
-
import_types=frozenset({"namespace_use_clause"}),
|
|
567
|
-
call_types=frozenset({"function_call_expression", "member_call_expression", "scoped_call_expression", "class_constant_access_expression"}),
|
|
568
|
-
static_prop_types=frozenset({"scoped_property_access_expression"}),
|
|
569
|
-
helper_fn_names=frozenset({"config"}),
|
|
570
|
-
container_bind_methods=frozenset({"bind", "singleton", "scoped", "instance"}),
|
|
571
|
-
event_listener_properties=frozenset({"listen", "subscribe"}),
|
|
572
|
-
call_function_field="function",
|
|
573
|
-
call_accessor_node_types=frozenset({"member_call_expression"}),
|
|
574
|
-
call_accessor_field="name",
|
|
575
|
-
name_fallback_child_types=("name",),
|
|
576
|
-
body_fallback_child_types=("declaration_list", "compound_statement"),
|
|
577
|
-
function_boundary_types=frozenset({"function_definition", "method_declaration"}),
|
|
578
|
-
import_handler=_import_php,
|
|
579
|
-
)
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
def _import_lua(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None:
|
|
583
|
-
"""Extract require('module') from Lua variable_declaration nodes."""
|
|
584
|
-
text = _read_text(node, source)
|
|
585
|
-
import re
|
|
586
|
-
m = re.search(r"""require\s*[\('"]\s*['"]?([^'")\s]+)""", text)
|
|
587
|
-
if m:
|
|
588
|
-
module_name = m.group(1).split(".")[-1]
|
|
589
|
-
if module_name:
|
|
590
|
-
edges.append({
|
|
591
|
-
"source": file_nid,
|
|
592
|
-
"target": module_name,
|
|
593
|
-
"relation": "imports",
|
|
594
|
-
"confidence": "EXTRACTED",
|
|
595
|
-
"confidence_score": 1.0,
|
|
596
|
-
"source_file": str_path,
|
|
597
|
-
"source_location": str(node.start_point[0] + 1),
|
|
598
|
-
"weight": 1.0,
|
|
599
|
-
})
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
_LUA_CONFIG = LanguageConfig(
|
|
603
|
-
ts_module="tree_sitter_lua",
|
|
604
|
-
ts_language_fn="language",
|
|
605
|
-
class_types=frozenset(),
|
|
606
|
-
function_types=frozenset({"function_declaration"}),
|
|
607
|
-
import_types=frozenset({"variable_declaration"}),
|
|
608
|
-
call_types=frozenset({"function_call"}),
|
|
609
|
-
call_function_field="name",
|
|
610
|
-
call_accessor_node_types=frozenset({"method_index_expression"}),
|
|
611
|
-
call_accessor_field="name",
|
|
612
|
-
name_fallback_child_types=("identifier", "method_index_expression"),
|
|
613
|
-
body_fallback_child_types=("block",),
|
|
614
|
-
function_boundary_types=frozenset({"function_declaration"}),
|
|
615
|
-
import_handler=_import_lua,
|
|
616
|
-
)
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
def _import_swift(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None:
|
|
620
|
-
for child in node.children:
|
|
621
|
-
if child.type == "identifier":
|
|
622
|
-
raw = _read_text(child, source)
|
|
623
|
-
tgt_nid = _make_id(raw)
|
|
624
|
-
edges.append({
|
|
625
|
-
"source": file_nid,
|
|
626
|
-
"target": tgt_nid,
|
|
627
|
-
"relation": "imports",
|
|
628
|
-
"confidence": "EXTRACTED",
|
|
629
|
-
"source_file": str_path,
|
|
630
|
-
"source_location": f"L{node.start_point[0] + 1}",
|
|
631
|
-
"weight": 1.0,
|
|
632
|
-
})
|
|
633
|
-
break
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
_SWIFT_CONFIG = LanguageConfig(
|
|
637
|
-
ts_module="tree_sitter_swift",
|
|
638
|
-
class_types=frozenset({"class_declaration", "protocol_declaration"}),
|
|
639
|
-
function_types=frozenset({"function_declaration", "init_declaration", "deinit_declaration", "subscript_declaration"}),
|
|
640
|
-
import_types=frozenset({"import_declaration"}),
|
|
641
|
-
call_types=frozenset({"call_expression"}),
|
|
642
|
-
call_function_field="",
|
|
643
|
-
call_accessor_node_types=frozenset({"navigation_expression"}),
|
|
644
|
-
call_accessor_field="",
|
|
645
|
-
name_fallback_child_types=("simple_identifier", "type_identifier", "user_type"),
|
|
646
|
-
body_fallback_child_types=("class_body", "protocol_body", "function_body", "enum_class_body"),
|
|
647
|
-
function_boundary_types=frozenset({"function_declaration", "init_declaration", "deinit_declaration", "subscript_declaration"}),
|
|
648
|
-
import_handler=_import_swift,
|
|
649
|
-
)
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
# ── Generic extractor ─────────────────────────────────────────────────────────
|
|
653
|
-
|
|
654
|
-
def _extract_generic(path: Path, config: LanguageConfig) -> dict:
|
|
655
|
-
"""Generic AST extractor driven by LanguageConfig."""
|
|
656
|
-
try:
|
|
657
|
-
mod = importlib.import_module(config.ts_module)
|
|
658
|
-
from tree_sitter import Language, Parser
|
|
659
|
-
lang_fn = getattr(mod, config.ts_language_fn, None)
|
|
660
|
-
if lang_fn is None:
|
|
661
|
-
# Fallback for PHP: try "language_php" then "language"
|
|
662
|
-
lang_fn = getattr(mod, "language", None)
|
|
663
|
-
if lang_fn is None:
|
|
664
|
-
return {"nodes": [], "edges": [], "error": f"No language function in {config.ts_module}"}
|
|
665
|
-
language = Language(lang_fn())
|
|
666
|
-
except ImportError:
|
|
667
|
-
return {"nodes": [], "edges": [], "error": f"{config.ts_module} not installed"}
|
|
668
|
-
except Exception as e:
|
|
669
|
-
return {"nodes": [], "edges": [], "error": str(e)}
|
|
670
|
-
|
|
671
|
-
try:
|
|
672
|
-
parser = Parser(language)
|
|
673
|
-
source = path.read_bytes()
|
|
674
|
-
tree = parser.parse(source)
|
|
675
|
-
root = tree.root_node
|
|
676
|
-
except Exception as e:
|
|
677
|
-
return {"nodes": [], "edges": [], "error": str(e)}
|
|
678
|
-
|
|
679
|
-
stem = path.stem
|
|
680
|
-
str_path = str(path)
|
|
681
|
-
nodes: list[dict] = []
|
|
682
|
-
edges: list[dict] = []
|
|
683
|
-
seen_ids: set[str] = set()
|
|
684
|
-
function_bodies: list[tuple[str, object]] = []
|
|
685
|
-
pending_listen_edges: list[tuple[str, str, int]] = []
|
|
686
|
-
|
|
687
|
-
def add_node(nid: str, label: str, line: int) -> None:
|
|
688
|
-
if nid not in seen_ids:
|
|
689
|
-
seen_ids.add(nid)
|
|
690
|
-
nodes.append({
|
|
691
|
-
"id": nid,
|
|
692
|
-
"label": label,
|
|
693
|
-
"file_type": "code",
|
|
694
|
-
"source_file": str_path,
|
|
695
|
-
"source_location": f"L{line}",
|
|
696
|
-
})
|
|
697
|
-
|
|
698
|
-
def add_edge(src: str, tgt: str, relation: str, line: int,
|
|
699
|
-
confidence: str = "EXTRACTED", weight: float = 1.0) -> None:
|
|
700
|
-
edges.append({
|
|
701
|
-
"source": src,
|
|
702
|
-
"target": tgt,
|
|
703
|
-
"relation": relation,
|
|
704
|
-
"confidence": confidence,
|
|
705
|
-
"source_file": str_path,
|
|
706
|
-
"source_location": f"L{line}",
|
|
707
|
-
"weight": weight,
|
|
708
|
-
})
|
|
709
|
-
|
|
710
|
-
file_nid = _make_id(str(path))
|
|
711
|
-
add_node(file_nid, path.name, 1)
|
|
712
|
-
|
|
713
|
-
def walk(node, parent_class_nid: str | None = None) -> None:
|
|
714
|
-
t = node.type
|
|
715
|
-
|
|
716
|
-
# Import types
|
|
717
|
-
if t in config.import_types:
|
|
718
|
-
if config.import_handler:
|
|
719
|
-
config.import_handler(node, source, file_nid, stem, edges, str_path)
|
|
720
|
-
return
|
|
721
|
-
|
|
722
|
-
# Class types
|
|
723
|
-
if t in config.class_types:
|
|
724
|
-
# Resolve class name
|
|
725
|
-
name_node = node.child_by_field_name(config.name_field)
|
|
726
|
-
if name_node is None:
|
|
727
|
-
for child in node.children:
|
|
728
|
-
if child.type in config.name_fallback_child_types:
|
|
729
|
-
name_node = child
|
|
730
|
-
break
|
|
731
|
-
if not name_node:
|
|
732
|
-
return
|
|
733
|
-
class_name = _read_text(name_node, source)
|
|
734
|
-
class_nid = _make_id(stem, class_name)
|
|
735
|
-
line = node.start_point[0] + 1
|
|
736
|
-
add_node(class_nid, class_name, line)
|
|
737
|
-
add_edge(file_nid, class_nid, "contains", line)
|
|
738
|
-
|
|
739
|
-
# Python-specific: inheritance
|
|
740
|
-
if config.ts_module == "tree_sitter_python":
|
|
741
|
-
args = node.child_by_field_name("superclasses")
|
|
742
|
-
if args:
|
|
743
|
-
for arg in args.children:
|
|
744
|
-
if arg.type == "identifier":
|
|
745
|
-
base = _read_text(arg, source)
|
|
746
|
-
base_nid = _make_id(stem, base)
|
|
747
|
-
if base_nid not in seen_ids:
|
|
748
|
-
base_nid = _make_id(base)
|
|
749
|
-
if base_nid not in seen_ids:
|
|
750
|
-
nodes.append({
|
|
751
|
-
"id": base_nid,
|
|
752
|
-
"label": base,
|
|
753
|
-
"file_type": "code",
|
|
754
|
-
"source_file": "",
|
|
755
|
-
"source_location": "",
|
|
756
|
-
})
|
|
757
|
-
seen_ids.add(base_nid)
|
|
758
|
-
add_edge(class_nid, base_nid, "inherits", line)
|
|
759
|
-
|
|
760
|
-
# Swift-specific: conformance / inheritance
|
|
761
|
-
if config.ts_module == "tree_sitter_swift":
|
|
762
|
-
for child in node.children:
|
|
763
|
-
if child.type == "inheritance_specifier":
|
|
764
|
-
for sub in child.children:
|
|
765
|
-
if sub.type in ("user_type", "type_identifier"):
|
|
766
|
-
base = _read_text(sub, source)
|
|
767
|
-
base_nid = _make_id(stem, base)
|
|
768
|
-
if base_nid not in seen_ids:
|
|
769
|
-
base_nid = _make_id(base)
|
|
770
|
-
if base_nid not in seen_ids:
|
|
771
|
-
nodes.append({
|
|
772
|
-
"id": base_nid,
|
|
773
|
-
"label": base,
|
|
774
|
-
"file_type": "code",
|
|
775
|
-
"source_file": "",
|
|
776
|
-
"source_location": "",
|
|
777
|
-
})
|
|
778
|
-
seen_ids.add(base_nid)
|
|
779
|
-
add_edge(class_nid, base_nid, "inherits", line)
|
|
780
|
-
|
|
781
|
-
# C#-specific: inheritance / interface implementation via base_list
|
|
782
|
-
if config.ts_module == "tree_sitter_c_sharp":
|
|
783
|
-
for child in node.children:
|
|
784
|
-
if child.type == "base_list":
|
|
785
|
-
for sub in child.children:
|
|
786
|
-
if sub.type in ("identifier", "generic_name"):
|
|
787
|
-
if sub.type == "generic_name":
|
|
788
|
-
name_child = sub.child_by_field_name("name")
|
|
789
|
-
base = _read_text(name_child, source) if name_child else _read_text(sub.children[0], source)
|
|
790
|
-
else:
|
|
791
|
-
base = _read_text(sub, source)
|
|
792
|
-
base_nid = _make_id(stem, base)
|
|
793
|
-
if base_nid not in seen_ids:
|
|
794
|
-
base_nid = _make_id(base)
|
|
795
|
-
if base_nid not in seen_ids:
|
|
796
|
-
nodes.append({
|
|
797
|
-
"id": base_nid,
|
|
798
|
-
"label": base,
|
|
799
|
-
"file_type": "code",
|
|
800
|
-
"source_file": "",
|
|
801
|
-
"source_location": "",
|
|
802
|
-
})
|
|
803
|
-
seen_ids.add(base_nid)
|
|
804
|
-
add_edge(class_nid, base_nid, "inherits", line)
|
|
805
|
-
|
|
806
|
-
# Find body and recurse
|
|
807
|
-
body = _find_body(node, config)
|
|
808
|
-
if body:
|
|
809
|
-
for child in body.children:
|
|
810
|
-
walk(child, parent_class_nid=class_nid)
|
|
811
|
-
return
|
|
812
|
-
|
|
813
|
-
# Event listener property arrays: $listen = [Event::class => [Listener::class]]
|
|
814
|
-
if (t == "property_declaration"
|
|
815
|
-
and parent_class_nid
|
|
816
|
-
and config.event_listener_properties):
|
|
817
|
-
for element in node.children:
|
|
818
|
-
if element.type != "property_element":
|
|
819
|
-
continue
|
|
820
|
-
prop_name: str | None = None
|
|
821
|
-
array_node = None
|
|
822
|
-
for c in element.children:
|
|
823
|
-
if c.type == "variable_name":
|
|
824
|
-
for sc in c.children:
|
|
825
|
-
if sc.type == "name":
|
|
826
|
-
prop_name = _read_text(sc, source)
|
|
827
|
-
break
|
|
828
|
-
elif c.type == "array_creation_expression":
|
|
829
|
-
array_node = c
|
|
830
|
-
if (prop_name is None
|
|
831
|
-
or prop_name not in config.event_listener_properties
|
|
832
|
-
or array_node is None):
|
|
833
|
-
continue
|
|
834
|
-
for entry in array_node.children:
|
|
835
|
-
if entry.type != "array_element_initializer":
|
|
836
|
-
continue
|
|
837
|
-
event_cls: str | None = None
|
|
838
|
-
listener_arr = None
|
|
839
|
-
for sub in entry.children:
|
|
840
|
-
if sub.type == "class_constant_access_expression" and event_cls is None:
|
|
841
|
-
for sc in sub.children:
|
|
842
|
-
if sc.is_named and sc.type in ("name", "qualified_name"):
|
|
843
|
-
event_cls = _read_text(sc, source)
|
|
844
|
-
break
|
|
845
|
-
elif sub.type == "array_creation_expression":
|
|
846
|
-
listener_arr = sub
|
|
847
|
-
if not event_cls or listener_arr is None:
|
|
848
|
-
continue
|
|
849
|
-
for listener_entry in listener_arr.children:
|
|
850
|
-
if listener_entry.type != "array_element_initializer":
|
|
851
|
-
continue
|
|
852
|
-
for item in listener_entry.children:
|
|
853
|
-
if item.type != "class_constant_access_expression":
|
|
854
|
-
continue
|
|
855
|
-
for sc in item.children:
|
|
856
|
-
if sc.is_named and sc.type in ("name", "qualified_name"):
|
|
857
|
-
listener_cls = _read_text(sc, source)
|
|
858
|
-
line_no = item.start_point[0] + 1
|
|
859
|
-
pending_listen_edges.append((event_cls, listener_cls, line_no))
|
|
860
|
-
break
|
|
861
|
-
break
|
|
862
|
-
return
|
|
863
|
-
|
|
864
|
-
# Function types
|
|
865
|
-
if t in config.function_types:
|
|
866
|
-
# Swift deinit/subscript have no name field — resolve before generic fallback
|
|
867
|
-
if t == "deinit_declaration":
|
|
868
|
-
func_name: str | None = "deinit"
|
|
869
|
-
elif t == "subscript_declaration":
|
|
870
|
-
func_name = "subscript"
|
|
871
|
-
elif config.resolve_function_name_fn is not None:
|
|
872
|
-
# C/C++ style: use declarator
|
|
873
|
-
declarator = node.child_by_field_name("declarator")
|
|
874
|
-
func_name = None
|
|
875
|
-
if declarator:
|
|
876
|
-
func_name = config.resolve_function_name_fn(declarator, source)
|
|
877
|
-
else:
|
|
878
|
-
name_node = node.child_by_field_name(config.name_field)
|
|
879
|
-
if name_node is None:
|
|
880
|
-
for child in node.children:
|
|
881
|
-
if child.type in config.name_fallback_child_types:
|
|
882
|
-
name_node = child
|
|
883
|
-
break
|
|
884
|
-
func_name = _read_text(name_node, source) if name_node else None
|
|
885
|
-
|
|
886
|
-
if not func_name:
|
|
887
|
-
return
|
|
888
|
-
|
|
889
|
-
line = node.start_point[0] + 1
|
|
890
|
-
if parent_class_nid:
|
|
891
|
-
func_nid = _make_id(parent_class_nid, func_name)
|
|
892
|
-
add_node(func_nid, f".{func_name}()", line)
|
|
893
|
-
add_edge(parent_class_nid, func_nid, "method", line)
|
|
894
|
-
else:
|
|
895
|
-
func_nid = _make_id(stem, func_name)
|
|
896
|
-
add_node(func_nid, f"{func_name}()", line)
|
|
897
|
-
add_edge(file_nid, func_nid, "contains", line)
|
|
898
|
-
|
|
899
|
-
body = _find_body(node, config)
|
|
900
|
-
if body:
|
|
901
|
-
function_bodies.append((func_nid, body))
|
|
902
|
-
return
|
|
903
|
-
|
|
904
|
-
# JS/TS arrow functions and C# namespaces — language-specific extra handling
|
|
905
|
-
if config.ts_module in ("tree_sitter_javascript", "tree_sitter_typescript"):
|
|
906
|
-
if _js_extra_walk(node, source, file_nid, stem, str_path,
|
|
907
|
-
nodes, edges, seen_ids, function_bodies,
|
|
908
|
-
parent_class_nid, add_node, add_edge):
|
|
909
|
-
return
|
|
910
|
-
|
|
911
|
-
if config.ts_module == "tree_sitter_c_sharp":
|
|
912
|
-
if _csharp_extra_walk(node, source, file_nid, stem, str_path,
|
|
913
|
-
nodes, edges, seen_ids, function_bodies,
|
|
914
|
-
parent_class_nid, add_node, add_edge, walk):
|
|
915
|
-
return
|
|
916
|
-
|
|
917
|
-
if config.ts_module == "tree_sitter_swift":
|
|
918
|
-
if _swift_extra_walk(node, source, file_nid, stem, str_path,
|
|
919
|
-
nodes, edges, seen_ids, function_bodies,
|
|
920
|
-
parent_class_nid, add_node, add_edge):
|
|
921
|
-
return
|
|
922
|
-
|
|
923
|
-
# Default: recurse
|
|
924
|
-
for child in node.children:
|
|
925
|
-
walk(child, parent_class_nid=None)
|
|
926
|
-
|
|
927
|
-
walk(root)
|
|
928
|
-
|
|
929
|
-
# ── Call-graph pass ───────────────────────────────────────────────────────
|
|
930
|
-
label_to_nid: dict[str, str] = {}
|
|
931
|
-
for n in nodes:
|
|
932
|
-
raw = n["label"]
|
|
933
|
-
normalised = raw.strip("()").lstrip(".")
|
|
934
|
-
label_to_nid[normalised.lower()] = n["id"]
|
|
935
|
-
|
|
936
|
-
seen_call_pairs: set[tuple[str, str]] = set()
|
|
937
|
-
seen_static_ref_pairs: set[tuple[str, str, str]] = set()
|
|
938
|
-
seen_helper_ref_pairs: set[tuple[str, str, str]] = set()
|
|
939
|
-
seen_bind_pairs: set[tuple[str, str, str]] = set()
|
|
940
|
-
raw_calls: list[dict] = [] # unresolved calls for cross-file resolution in extract()
|
|
941
|
-
|
|
942
|
-
def _php_class_const_scope(n) -> str | None:
|
|
943
|
-
scope = n.child_by_field_name("scope")
|
|
944
|
-
if scope is None:
|
|
945
|
-
for c in n.children:
|
|
946
|
-
if c.is_named and c.type in ("name", "qualified_name", "identifier"):
|
|
947
|
-
scope = c
|
|
948
|
-
break
|
|
949
|
-
if scope is None:
|
|
950
|
-
return None
|
|
951
|
-
return _read_text(scope, source)
|
|
952
|
-
|
|
953
|
-
def walk_calls(node, caller_nid: str) -> None:
|
|
954
|
-
if node.type in config.function_boundary_types:
|
|
955
|
-
return
|
|
956
|
-
|
|
957
|
-
if node.type in config.call_types:
|
|
958
|
-
callee_name: str | None = None
|
|
959
|
-
|
|
960
|
-
# Special handling per language
|
|
961
|
-
if config.ts_module == "tree_sitter_swift":
|
|
962
|
-
# Swift: first child may be simple_identifier or navigation_expression
|
|
963
|
-
first = node.children[0] if node.children else None
|
|
964
|
-
if first:
|
|
965
|
-
if first.type == "simple_identifier":
|
|
966
|
-
callee_name = _read_text(first, source)
|
|
967
|
-
elif first.type == "navigation_expression":
|
|
968
|
-
for child in first.children:
|
|
969
|
-
if child.type == "navigation_suffix":
|
|
970
|
-
for sc in child.children:
|
|
971
|
-
if sc.type == "simple_identifier":
|
|
972
|
-
callee_name = _read_text(sc, source)
|
|
973
|
-
elif config.ts_module == "tree_sitter_kotlin":
|
|
974
|
-
# Kotlin: first child may be simple_identifier or navigation_expression
|
|
975
|
-
first = node.children[0] if node.children else None
|
|
976
|
-
if first:
|
|
977
|
-
if first.type == "simple_identifier":
|
|
978
|
-
callee_name = _read_text(first, source)
|
|
979
|
-
elif first.type == "navigation_expression":
|
|
980
|
-
for child in reversed(first.children):
|
|
981
|
-
if child.type == "simple_identifier":
|
|
982
|
-
callee_name = _read_text(child, source)
|
|
983
|
-
break
|
|
984
|
-
elif config.ts_module == "tree_sitter_scala":
|
|
985
|
-
# Scala: first child
|
|
986
|
-
first = node.children[0] if node.children else None
|
|
987
|
-
if first:
|
|
988
|
-
if first.type == "identifier":
|
|
989
|
-
callee_name = _read_text(first, source)
|
|
990
|
-
elif first.type == "field_expression":
|
|
991
|
-
field = first.child_by_field_name("field")
|
|
992
|
-
if field:
|
|
993
|
-
callee_name = _read_text(field, source)
|
|
994
|
-
else:
|
|
995
|
-
for child in reversed(first.children):
|
|
996
|
-
if child.type == "identifier":
|
|
997
|
-
callee_name = _read_text(child, source)
|
|
998
|
-
break
|
|
999
|
-
elif config.ts_module == "tree_sitter_c_sharp" and node.type == "invocation_expression":
|
|
1000
|
-
# C#: try name field, then first named child
|
|
1001
|
-
name_node = node.child_by_field_name("name")
|
|
1002
|
-
if name_node:
|
|
1003
|
-
callee_name = _read_text(name_node, source)
|
|
1004
|
-
else:
|
|
1005
|
-
for child in node.children:
|
|
1006
|
-
if child.is_named:
|
|
1007
|
-
raw = _read_text(child, source)
|
|
1008
|
-
if "." in raw:
|
|
1009
|
-
callee_name = raw.split(".")[-1]
|
|
1010
|
-
else:
|
|
1011
|
-
callee_name = raw
|
|
1012
|
-
break
|
|
1013
|
-
elif config.ts_module == "tree_sitter_php":
|
|
1014
|
-
# PHP: distinguish call expression subtypes
|
|
1015
|
-
if node.type == "function_call_expression":
|
|
1016
|
-
func_node = node.child_by_field_name("function")
|
|
1017
|
-
if func_node:
|
|
1018
|
-
callee_name = _read_text(func_node, source)
|
|
1019
|
-
elif node.type == "scoped_call_expression":
|
|
1020
|
-
# Static method call: Helper::format() → callee = "Helper"
|
|
1021
|
-
scope_node = node.child_by_field_name("scope")
|
|
1022
|
-
if scope_node:
|
|
1023
|
-
callee_name = _read_text(scope_node, source)
|
|
1024
|
-
else:
|
|
1025
|
-
name_node = node.child_by_field_name("name")
|
|
1026
|
-
if name_node:
|
|
1027
|
-
callee_name = _read_text(name_node, source)
|
|
1028
|
-
elif config.ts_module == "tree_sitter_cpp":
|
|
1029
|
-
# C++: function field, then field_expression/qualified_identifier
|
|
1030
|
-
func_node = node.child_by_field_name(config.call_function_field) if config.call_function_field else None
|
|
1031
|
-
if func_node:
|
|
1032
|
-
if func_node.type == "identifier":
|
|
1033
|
-
callee_name = _read_text(func_node, source)
|
|
1034
|
-
elif func_node.type in ("field_expression", "qualified_identifier"):
|
|
1035
|
-
name = func_node.child_by_field_name("field") or func_node.child_by_field_name("name")
|
|
1036
|
-
if name:
|
|
1037
|
-
callee_name = _read_text(name, source)
|
|
1038
|
-
else:
|
|
1039
|
-
# Generic: get callee from call_function_field
|
|
1040
|
-
func_node = node.child_by_field_name(config.call_function_field) if config.call_function_field else None
|
|
1041
|
-
if func_node:
|
|
1042
|
-
if func_node.type == "identifier":
|
|
1043
|
-
callee_name = _read_text(func_node, source)
|
|
1044
|
-
elif func_node.type in config.call_accessor_node_types:
|
|
1045
|
-
if config.call_accessor_field:
|
|
1046
|
-
attr = func_node.child_by_field_name(config.call_accessor_field)
|
|
1047
|
-
if attr:
|
|
1048
|
-
callee_name = _read_text(attr, source)
|
|
1049
|
-
else:
|
|
1050
|
-
# Try reading the node directly (e.g. Java name field is the callee)
|
|
1051
|
-
callee_name = _read_text(func_node, source)
|
|
1052
|
-
|
|
1053
|
-
if callee_name:
|
|
1054
|
-
tgt_nid = label_to_nid.get(callee_name.lower())
|
|
1055
|
-
if tgt_nid and tgt_nid != caller_nid:
|
|
1056
|
-
pair = (caller_nid, tgt_nid)
|
|
1057
|
-
if pair not in seen_call_pairs:
|
|
1058
|
-
seen_call_pairs.add(pair)
|
|
1059
|
-
line = node.start_point[0] + 1
|
|
1060
|
-
edges.append({
|
|
1061
|
-
"source": caller_nid,
|
|
1062
|
-
"target": tgt_nid,
|
|
1063
|
-
"relation": "calls",
|
|
1064
|
-
"confidence": "EXTRACTED",
|
|
1065
|
-
"source_file": str_path,
|
|
1066
|
-
"source_location": f"L{line}",
|
|
1067
|
-
"weight": 1.0,
|
|
1068
|
-
})
|
|
1069
|
-
elif callee_name and not tgt_nid:
|
|
1070
|
-
# Callee not in this file — save for cross-file resolution in extract()
|
|
1071
|
-
raw_calls.append({
|
|
1072
|
-
"caller_nid": caller_nid,
|
|
1073
|
-
"callee": callee_name,
|
|
1074
|
-
"source_file": str_path,
|
|
1075
|
-
"source_location": f"L{node.start_point[0] + 1}",
|
|
1076
|
-
})
|
|
1077
|
-
|
|
1078
|
-
# Helper function calls: config('foo.bar') → uses_config edge to "foo"
|
|
1079
|
-
if (callee_name and callee_name in config.helper_fn_names):
|
|
1080
|
-
args_node = node.child_by_field_name("arguments")
|
|
1081
|
-
first_key: str | None = None
|
|
1082
|
-
if args_node:
|
|
1083
|
-
for arg in args_node.children:
|
|
1084
|
-
if arg.type != "argument":
|
|
1085
|
-
continue
|
|
1086
|
-
for inner in arg.children:
|
|
1087
|
-
if inner.type == "string":
|
|
1088
|
-
for sc in inner.children:
|
|
1089
|
-
if sc.type == "string_content":
|
|
1090
|
-
first_key = _read_text(sc, source)
|
|
1091
|
-
break
|
|
1092
|
-
break
|
|
1093
|
-
if first_key:
|
|
1094
|
-
break
|
|
1095
|
-
if first_key:
|
|
1096
|
-
segment = first_key.split(".")[0]
|
|
1097
|
-
tgt_nid = (label_to_nid.get(segment.lower())
|
|
1098
|
-
or label_to_nid.get(f"{segment}.php".lower()))
|
|
1099
|
-
if tgt_nid and tgt_nid != caller_nid:
|
|
1100
|
-
relation = f"uses_{callee_name}"
|
|
1101
|
-
pair3 = (caller_nid, tgt_nid, relation)
|
|
1102
|
-
if pair3 not in seen_helper_ref_pairs:
|
|
1103
|
-
seen_helper_ref_pairs.add(pair3)
|
|
1104
|
-
line = node.start_point[0] + 1
|
|
1105
|
-
edges.append({
|
|
1106
|
-
"source": caller_nid,
|
|
1107
|
-
"target": tgt_nid,
|
|
1108
|
-
"relation": relation,
|
|
1109
|
-
"confidence": "EXTRACTED",
|
|
1110
|
-
"confidence_score": 1.0,
|
|
1111
|
-
"source_file": str_path,
|
|
1112
|
-
"source_location": f"L{line}",
|
|
1113
|
-
"weight": 1.0,
|
|
1114
|
-
})
|
|
1115
|
-
|
|
1116
|
-
# Service container bindings: $this->app->bind(Foo::class, Bar::class)
|
|
1117
|
-
if (node.type == "member_call_expression"
|
|
1118
|
-
and callee_name
|
|
1119
|
-
and callee_name in config.container_bind_methods):
|
|
1120
|
-
args_node = node.child_by_field_name("arguments")
|
|
1121
|
-
class_args: list[str] = []
|
|
1122
|
-
if args_node:
|
|
1123
|
-
for arg in args_node.children:
|
|
1124
|
-
if arg.type != "argument":
|
|
1125
|
-
continue
|
|
1126
|
-
for inner in arg.children:
|
|
1127
|
-
if inner.type == "class_constant_access_expression":
|
|
1128
|
-
cls = _php_class_const_scope(inner)
|
|
1129
|
-
if cls:
|
|
1130
|
-
class_args.append(cls)
|
|
1131
|
-
break
|
|
1132
|
-
if len(class_args) >= 2:
|
|
1133
|
-
break
|
|
1134
|
-
if len(class_args) == 2:
|
|
1135
|
-
contract_name, impl_name = class_args
|
|
1136
|
-
contract_nid = label_to_nid.get(contract_name.lower())
|
|
1137
|
-
impl_nid = label_to_nid.get(impl_name.lower())
|
|
1138
|
-
if contract_nid and impl_nid and contract_nid != impl_nid:
|
|
1139
|
-
pair3 = (contract_nid, impl_nid, "bound_to")
|
|
1140
|
-
if pair3 not in seen_bind_pairs:
|
|
1141
|
-
seen_bind_pairs.add(pair3)
|
|
1142
|
-
line = node.start_point[0] + 1
|
|
1143
|
-
edges.append({
|
|
1144
|
-
"source": contract_nid,
|
|
1145
|
-
"target": impl_nid,
|
|
1146
|
-
"relation": "bound_to",
|
|
1147
|
-
"confidence": "EXTRACTED",
|
|
1148
|
-
"confidence_score": 1.0,
|
|
1149
|
-
"source_file": str_path,
|
|
1150
|
-
"source_location": f"L{line}",
|
|
1151
|
-
"weight": 1.0,
|
|
1152
|
-
})
|
|
1153
|
-
|
|
1154
|
-
# Static property access: Foo::$bar → uses_static_prop edge
|
|
1155
|
-
if node.type in config.static_prop_types:
|
|
1156
|
-
scope_node = node.child_by_field_name("scope")
|
|
1157
|
-
if scope_node is None:
|
|
1158
|
-
for child in node.children:
|
|
1159
|
-
if child.is_named and child.type in ("name", "qualified_name", "identifier"):
|
|
1160
|
-
scope_node = child
|
|
1161
|
-
break
|
|
1162
|
-
if scope_node is not None:
|
|
1163
|
-
class_name = _read_text(scope_node, source)
|
|
1164
|
-
tgt_nid = label_to_nid.get(class_name.lower())
|
|
1165
|
-
if tgt_nid and tgt_nid != caller_nid:
|
|
1166
|
-
pair3 = (caller_nid, tgt_nid, "uses_static_prop")
|
|
1167
|
-
if pair3 not in seen_static_ref_pairs:
|
|
1168
|
-
seen_static_ref_pairs.add(pair3)
|
|
1169
|
-
line = node.start_point[0] + 1
|
|
1170
|
-
edges.append({
|
|
1171
|
-
"source": caller_nid,
|
|
1172
|
-
"target": tgt_nid,
|
|
1173
|
-
"relation": "uses_static_prop",
|
|
1174
|
-
"confidence": "EXTRACTED",
|
|
1175
|
-
"confidence_score": 1.0,
|
|
1176
|
-
"source_file": str_path,
|
|
1177
|
-
"source_location": f"L{line}",
|
|
1178
|
-
"weight": 1.0,
|
|
1179
|
-
})
|
|
1180
|
-
|
|
1181
|
-
# PHP class constant access: Foo::BAR → references_constant edge
|
|
1182
|
-
if config.ts_module == "tree_sitter_php" and node.type == "class_constant_access_expression":
|
|
1183
|
-
class_name = _php_class_const_scope(node)
|
|
1184
|
-
if class_name:
|
|
1185
|
-
tgt_nid = label_to_nid.get(class_name.lower())
|
|
1186
|
-
if tgt_nid and tgt_nid != caller_nid:
|
|
1187
|
-
pair3 = (caller_nid, tgt_nid, "references_constant")
|
|
1188
|
-
if pair3 not in seen_static_ref_pairs:
|
|
1189
|
-
seen_static_ref_pairs.add(pair3)
|
|
1190
|
-
line = node.start_point[0] + 1
|
|
1191
|
-
edges.append({
|
|
1192
|
-
"source": caller_nid,
|
|
1193
|
-
"target": tgt_nid,
|
|
1194
|
-
"relation": "references_constant",
|
|
1195
|
-
"confidence": "EXTRACTED",
|
|
1196
|
-
"confidence_score": 1.0,
|
|
1197
|
-
"source_file": str_path,
|
|
1198
|
-
"source_location": f"L{line}",
|
|
1199
|
-
"weight": 1.0,
|
|
1200
|
-
})
|
|
1201
|
-
|
|
1202
|
-
for child in node.children:
|
|
1203
|
-
walk_calls(child, caller_nid)
|
|
1204
|
-
|
|
1205
|
-
for caller_nid, body_node in function_bodies:
|
|
1206
|
-
walk_calls(body_node, caller_nid)
|
|
1207
|
-
|
|
1208
|
-
# ── Event listener pass ───────────────────────────────────────────────────
|
|
1209
|
-
seen_listen_pairs: set[tuple[str, str]] = set()
|
|
1210
|
-
for event_name, listener_name, line in pending_listen_edges:
|
|
1211
|
-
event_nid = label_to_nid.get(event_name.lower())
|
|
1212
|
-
listener_nid = label_to_nid.get(listener_name.lower())
|
|
1213
|
-
if not event_nid or not listener_nid or event_nid == listener_nid:
|
|
1214
|
-
continue
|
|
1215
|
-
pair2 = (event_nid, listener_nid)
|
|
1216
|
-
if pair2 in seen_listen_pairs:
|
|
1217
|
-
continue
|
|
1218
|
-
seen_listen_pairs.add(pair2)
|
|
1219
|
-
edges.append({
|
|
1220
|
-
"source": event_nid,
|
|
1221
|
-
"target": listener_nid,
|
|
1222
|
-
"relation": "listened_by",
|
|
1223
|
-
"confidence": "EXTRACTED",
|
|
1224
|
-
"confidence_score": 1.0,
|
|
1225
|
-
"source_file": str_path,
|
|
1226
|
-
"source_location": f"L{line}",
|
|
1227
|
-
"weight": 1.0,
|
|
1228
|
-
})
|
|
1229
|
-
|
|
1230
|
-
# ── Clean edges ───────────────────────────────────────────────────────────
|
|
1231
|
-
valid_ids = seen_ids
|
|
1232
|
-
clean_edges = []
|
|
1233
|
-
for edge in edges:
|
|
1234
|
-
src, tgt = edge["source"], edge["target"]
|
|
1235
|
-
if src in valid_ids and (tgt in valid_ids or edge["relation"] in ("imports", "imports_from")):
|
|
1236
|
-
clean_edges.append(edge)
|
|
1237
|
-
|
|
1238
|
-
return {"nodes": nodes, "edges": clean_edges, "raw_calls": raw_calls}
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
# ── Python rationale extraction ───────────────────────────────────────────────
|
|
1242
|
-
|
|
1243
|
-
_RATIONALE_PREFIXES = ("# NOTE:", "# IMPORTANT:", "# HACK:", "# WHY:", "# RATIONALE:", "# TODO:", "# FIXME:")
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
def _extract_python_rationale(path: Path, result: dict) -> None:
|
|
1247
|
-
"""Post-pass: extract docstrings and rationale comments from Python source.
|
|
1248
|
-
Mutates result in-place by appending to result['nodes'] and result['edges'].
|
|
1249
|
-
"""
|
|
1250
|
-
try:
|
|
1251
|
-
import tree_sitter_python as tspython
|
|
1252
|
-
from tree_sitter import Language, Parser
|
|
1253
|
-
language = Language(tspython.language())
|
|
1254
|
-
parser = Parser(language)
|
|
1255
|
-
source = path.read_bytes()
|
|
1256
|
-
tree = parser.parse(source)
|
|
1257
|
-
root = tree.root_node
|
|
1258
|
-
except Exception:
|
|
1259
|
-
return
|
|
1260
|
-
|
|
1261
|
-
stem = path.stem
|
|
1262
|
-
str_path = str(path)
|
|
1263
|
-
nodes = result["nodes"]
|
|
1264
|
-
edges = result["edges"]
|
|
1265
|
-
seen_ids = {n["id"] for n in nodes}
|
|
1266
|
-
file_nid = _make_id(str(path))
|
|
1267
|
-
|
|
1268
|
-
def _get_docstring(body_node) -> tuple[str, int] | None:
|
|
1269
|
-
if not body_node:
|
|
1270
|
-
return None
|
|
1271
|
-
for child in body_node.children:
|
|
1272
|
-
if child.type == "expression_statement":
|
|
1273
|
-
for sub in child.children:
|
|
1274
|
-
if sub.type in ("string", "concatenated_string"):
|
|
1275
|
-
text = source[sub.start_byte:sub.end_byte].decode("utf-8", errors="replace")
|
|
1276
|
-
text = text.strip("\"'").strip('"""').strip("'''").strip()
|
|
1277
|
-
if len(text) > 20:
|
|
1278
|
-
return text, child.start_point[0] + 1
|
|
1279
|
-
break
|
|
1280
|
-
return None
|
|
1281
|
-
|
|
1282
|
-
def _add_rationale(text: str, line: int, parent_nid: str) -> None:
|
|
1283
|
-
label = text[:80].replace("\r\n", " ").replace("\r", " ").replace("\n", " ").strip()
|
|
1284
|
-
rid = _make_id(stem, "rationale", str(line))
|
|
1285
|
-
if rid not in seen_ids:
|
|
1286
|
-
seen_ids.add(rid)
|
|
1287
|
-
nodes.append({
|
|
1288
|
-
"id": rid,
|
|
1289
|
-
"label": label,
|
|
1290
|
-
"file_type": "rationale",
|
|
1291
|
-
"source_file": str_path,
|
|
1292
|
-
"source_location": f"L{line}",
|
|
1293
|
-
})
|
|
1294
|
-
edges.append({
|
|
1295
|
-
"source": rid,
|
|
1296
|
-
"target": parent_nid,
|
|
1297
|
-
"relation": "rationale_for",
|
|
1298
|
-
"confidence": "EXTRACTED",
|
|
1299
|
-
"source_file": str_path,
|
|
1300
|
-
"source_location": f"L{line}",
|
|
1301
|
-
"weight": 1.0,
|
|
1302
|
-
})
|
|
1303
|
-
|
|
1304
|
-
# Module-level docstring
|
|
1305
|
-
ds = _get_docstring(root)
|
|
1306
|
-
if ds:
|
|
1307
|
-
_add_rationale(ds[0], ds[1], file_nid)
|
|
1308
|
-
|
|
1309
|
-
# Class and function docstrings
|
|
1310
|
-
def walk_docstrings(node, parent_nid: str) -> None:
|
|
1311
|
-
t = node.type
|
|
1312
|
-
if t == "class_definition":
|
|
1313
|
-
name_node = node.child_by_field_name("name")
|
|
1314
|
-
body = node.child_by_field_name("body")
|
|
1315
|
-
if name_node and body:
|
|
1316
|
-
class_name = source[name_node.start_byte:name_node.end_byte].decode("utf-8", errors="replace")
|
|
1317
|
-
nid = _make_id(stem, class_name)
|
|
1318
|
-
ds = _get_docstring(body)
|
|
1319
|
-
if ds:
|
|
1320
|
-
_add_rationale(ds[0], ds[1], nid)
|
|
1321
|
-
for child in body.children:
|
|
1322
|
-
walk_docstrings(child, nid)
|
|
1323
|
-
return
|
|
1324
|
-
if t == "function_definition":
|
|
1325
|
-
name_node = node.child_by_field_name("name")
|
|
1326
|
-
body = node.child_by_field_name("body")
|
|
1327
|
-
if name_node and body:
|
|
1328
|
-
func_name = source[name_node.start_byte:name_node.end_byte].decode("utf-8", errors="replace")
|
|
1329
|
-
nid = _make_id(parent_nid, func_name) if parent_nid != file_nid else _make_id(stem, func_name)
|
|
1330
|
-
ds = _get_docstring(body)
|
|
1331
|
-
if ds:
|
|
1332
|
-
_add_rationale(ds[0], ds[1], nid)
|
|
1333
|
-
return
|
|
1334
|
-
for child in node.children:
|
|
1335
|
-
walk_docstrings(child, parent_nid)
|
|
1336
|
-
|
|
1337
|
-
walk_docstrings(root, file_nid)
|
|
1338
|
-
|
|
1339
|
-
# Rationale comments (# NOTE:, # IMPORTANT:, etc.)
|
|
1340
|
-
source_text = source.decode("utf-8", errors="replace")
|
|
1341
|
-
for lineno, line_text in enumerate(source_text.splitlines(), start=1):
|
|
1342
|
-
stripped = line_text.strip()
|
|
1343
|
-
if any(stripped.startswith(p) for p in _RATIONALE_PREFIXES):
|
|
1344
|
-
_add_rationale(stripped, lineno, file_nid)
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
# ── Public API ────────────────────────────────────────────────────────────────
|
|
1348
|
-
|
|
1349
|
-
def extract_python(path: Path) -> dict:
|
|
1350
|
-
"""Extract classes, functions, and imports from a .py file via tree-sitter AST."""
|
|
1351
|
-
result = _extract_generic(path, _PYTHON_CONFIG)
|
|
1352
|
-
if "error" not in result:
|
|
1353
|
-
_extract_python_rationale(path, result)
|
|
1354
|
-
return result
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
def extract_js(path: Path) -> dict:
|
|
1358
|
-
"""Extract classes, functions, arrow functions, and imports from a .js/.ts/.tsx file."""
|
|
1359
|
-
config = _TS_CONFIG if path.suffix in (".ts", ".tsx") else _JS_CONFIG
|
|
1360
|
-
return _extract_generic(path, config)
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
def extract_java(path: Path) -> dict:
|
|
1364
|
-
"""Extract classes, interfaces, methods, constructors, and imports from a .java file."""
|
|
1365
|
-
return _extract_generic(path, _JAVA_CONFIG)
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
def extract_c(path: Path) -> dict:
|
|
1369
|
-
"""Extract functions and includes from a .c/.h file."""
|
|
1370
|
-
return _extract_generic(path, _C_CONFIG)
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
def extract_cpp(path: Path) -> dict:
|
|
1374
|
-
"""Extract functions, classes, and includes from a .cpp/.cc/.cxx/.hpp file."""
|
|
1375
|
-
return _extract_generic(path, _CPP_CONFIG)
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
def extract_ruby(path: Path) -> dict:
|
|
1379
|
-
"""Extract classes, methods, singleton methods, and calls from a .rb file."""
|
|
1380
|
-
return _extract_generic(path, _RUBY_CONFIG)
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
def extract_csharp(path: Path) -> dict:
|
|
1384
|
-
"""Extract classes, interfaces, methods, namespaces, and usings from a .cs file."""
|
|
1385
|
-
return _extract_generic(path, _CSHARP_CONFIG)
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
def extract_kotlin(path: Path) -> dict:
|
|
1389
|
-
"""Extract classes, objects, functions, and imports from a .kt/.kts file."""
|
|
1390
|
-
return _extract_generic(path, _KOTLIN_CONFIG)
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
def extract_scala(path: Path) -> dict:
|
|
1394
|
-
"""Extract classes, objects, functions, and imports from a .scala file."""
|
|
1395
|
-
return _extract_generic(path, _SCALA_CONFIG)
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
def extract_php(path: Path) -> dict:
|
|
1399
|
-
"""Extract classes, functions, methods, namespace uses, and calls from a .php file."""
|
|
1400
|
-
return _extract_generic(path, _PHP_CONFIG)
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
def extract_blade(path: Path) -> dict:
|
|
1404
|
-
"""Extract @include, <livewire:> components, and wire:click bindings from Blade templates."""
|
|
1405
|
-
import re
|
|
1406
|
-
try:
|
|
1407
|
-
src = path.read_text(encoding="utf-8", errors="replace")
|
|
1408
|
-
except OSError:
|
|
1409
|
-
return {"error": f"cannot read {path}"}
|
|
1410
|
-
|
|
1411
|
-
file_nid = _make_id(str(path))
|
|
1412
|
-
nodes = [{"id": file_nid, "label": path.name, "file_type": "code",
|
|
1413
|
-
"source_file": str(path), "source_location": None}]
|
|
1414
|
-
edges = []
|
|
1415
|
-
|
|
1416
|
-
# @include('path.to.partial') or @include("path.to.partial")
|
|
1417
|
-
for m in re.finditer(r"@include\(['\"]([^'\"]+)['\"]", src):
|
|
1418
|
-
tgt = m.group(1).replace(".", "/")
|
|
1419
|
-
tgt_nid = _make_id(tgt)
|
|
1420
|
-
if tgt_nid not in {n["id"] for n in nodes}:
|
|
1421
|
-
nodes.append({"id": tgt_nid, "label": m.group(1), "file_type": "code",
|
|
1422
|
-
"source_file": str(path), "source_location": None})
|
|
1423
|
-
edges.append({"source": file_nid, "target": tgt_nid, "relation": "includes",
|
|
1424
|
-
"confidence": "EXTRACTED", "confidence_score": 1.0,
|
|
1425
|
-
"source_file": str(path), "source_location": None, "weight": 1.0})
|
|
1426
|
-
|
|
1427
|
-
# <livewire:component.name /> or <livewire:component.name>
|
|
1428
|
-
for m in re.finditer(r"<livewire:([\w.\-]+)", src):
|
|
1429
|
-
tgt_nid = _make_id(m.group(1))
|
|
1430
|
-
if tgt_nid not in {n["id"] for n in nodes}:
|
|
1431
|
-
nodes.append({"id": tgt_nid, "label": m.group(1), "file_type": "code",
|
|
1432
|
-
"source_file": str(path), "source_location": None})
|
|
1433
|
-
edges.append({"source": file_nid, "target": tgt_nid, "relation": "uses_component",
|
|
1434
|
-
"confidence": "EXTRACTED", "confidence_score": 1.0,
|
|
1435
|
-
"source_file": str(path), "source_location": None, "weight": 1.0})
|
|
1436
|
-
|
|
1437
|
-
# wire:click="methodName"
|
|
1438
|
-
for m in re.finditer(r'wire:click=["\']([^"\']+)["\']', src):
|
|
1439
|
-
tgt_nid = _make_id(m.group(1))
|
|
1440
|
-
if tgt_nid not in {n["id"] for n in nodes}:
|
|
1441
|
-
nodes.append({"id": tgt_nid, "label": m.group(1), "file_type": "code",
|
|
1442
|
-
"source_file": str(path), "source_location": None})
|
|
1443
|
-
edges.append({"source": file_nid, "target": tgt_nid, "relation": "binds_method",
|
|
1444
|
-
"confidence": "EXTRACTED", "confidence_score": 1.0,
|
|
1445
|
-
"source_file": str(path), "source_location": None, "weight": 1.0})
|
|
1446
|
-
|
|
1447
|
-
return {"nodes": nodes, "edges": edges}
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
def extract_dart(path: Path) -> dict:
|
|
1451
|
-
"""Extract classes, mixins, functions, imports, and calls from a .dart file using regex."""
|
|
1452
|
-
try:
|
|
1453
|
-
src = path.read_text(encoding="utf-8", errors="replace")
|
|
1454
|
-
except OSError:
|
|
1455
|
-
return {"error": f"cannot read {path}"}
|
|
1456
|
-
|
|
1457
|
-
file_nid = _make_id(str(path))
|
|
1458
|
-
nodes = [{"id": file_nid, "label": path.name, "file_type": "code",
|
|
1459
|
-
"source_file": str(path), "source_location": None}]
|
|
1460
|
-
edges = []
|
|
1461
|
-
defined: set[str] = set()
|
|
1462
|
-
|
|
1463
|
-
# Classes and mixins
|
|
1464
|
-
for m in re.finditer(r"^\s*(?:abstract\s+)?(?:class|mixin)\s+(\w+)", src, re.MULTILINE):
|
|
1465
|
-
nid = _make_id(str(path), m.group(1))
|
|
1466
|
-
if nid not in defined:
|
|
1467
|
-
nodes.append({"id": nid, "label": m.group(1), "file_type": "code",
|
|
1468
|
-
"source_file": str(path), "source_location": None})
|
|
1469
|
-
edges.append({"source": file_nid, "target": nid, "relation": "defines",
|
|
1470
|
-
"confidence": "EXTRACTED", "confidence_score": 1.0,
|
|
1471
|
-
"source_file": str(path), "source_location": None, "weight": 1.0})
|
|
1472
|
-
defined.add(nid)
|
|
1473
|
-
|
|
1474
|
-
# Top-level and member functions/methods
|
|
1475
|
-
for m in re.finditer(r"^\s*(?:static\s+|async\s+)?(?:\w+\s+)+(\w+)\s*\(", src, re.MULTILINE):
|
|
1476
|
-
name = m.group(1)
|
|
1477
|
-
if name in {"if", "for", "while", "switch", "catch", "return"}:
|
|
1478
|
-
continue
|
|
1479
|
-
nid = _make_id(str(path), name)
|
|
1480
|
-
if nid not in defined:
|
|
1481
|
-
nodes.append({"id": nid, "label": name, "file_type": "code",
|
|
1482
|
-
"source_file": str(path), "source_location": None})
|
|
1483
|
-
edges.append({"source": file_nid, "target": nid, "relation": "defines",
|
|
1484
|
-
"confidence": "EXTRACTED", "confidence_score": 1.0,
|
|
1485
|
-
"source_file": str(path), "source_location": None, "weight": 1.0})
|
|
1486
|
-
defined.add(nid)
|
|
1487
|
-
|
|
1488
|
-
# import 'package:...' or import '...'
|
|
1489
|
-
for m in re.finditer(r"""^import\s+['"]([^'"]+)['"]""", src, re.MULTILINE):
|
|
1490
|
-
pkg = m.group(1)
|
|
1491
|
-
tgt_nid = _make_id(pkg)
|
|
1492
|
-
if tgt_nid not in defined:
|
|
1493
|
-
nodes.append({"id": tgt_nid, "label": pkg, "file_type": "code",
|
|
1494
|
-
"source_file": str(path), "source_location": None})
|
|
1495
|
-
defined.add(tgt_nid)
|
|
1496
|
-
edges.append({"source": file_nid, "target": tgt_nid, "relation": "imports",
|
|
1497
|
-
"confidence": "EXTRACTED", "confidence_score": 1.0,
|
|
1498
|
-
"source_file": str(path), "source_location": None, "weight": 1.0})
|
|
1499
|
-
|
|
1500
|
-
return {"nodes": nodes, "edges": edges}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
def extract_verilog(path: Path) -> dict:
|
|
1504
|
-
"""Extract modules, functions, tasks, package imports, and instantiations from .v/.sv files."""
|
|
1505
|
-
try:
|
|
1506
|
-
import tree_sitter_verilog as tsverilog
|
|
1507
|
-
from tree_sitter import Language, Parser
|
|
1508
|
-
except ImportError:
|
|
1509
|
-
return {"nodes": [], "edges": [], "error": "tree_sitter_verilog not installed"}
|
|
1510
|
-
|
|
1511
|
-
try:
|
|
1512
|
-
language = Language(tsverilog.language())
|
|
1513
|
-
parser = Parser(language)
|
|
1514
|
-
source = path.read_bytes()
|
|
1515
|
-
tree = parser.parse(source)
|
|
1516
|
-
root = tree.root_node
|
|
1517
|
-
except Exception as e:
|
|
1518
|
-
return {"nodes": [], "edges": [], "error": str(e)}
|
|
1519
|
-
|
|
1520
|
-
stem = path.stem
|
|
1521
|
-
str_path = str(path)
|
|
1522
|
-
nodes: list[dict] = []
|
|
1523
|
-
edges: list[dict] = []
|
|
1524
|
-
seen_ids: set[str] = set()
|
|
1525
|
-
|
|
1526
|
-
def add_node(nid: str, label: str, line: int) -> None:
|
|
1527
|
-
if nid not in seen_ids:
|
|
1528
|
-
seen_ids.add(nid)
|
|
1529
|
-
nodes.append({"id": nid, "label": label, "file_type": "code",
|
|
1530
|
-
"source_file": str_path, "source_location": f"L{line}",
|
|
1531
|
-
"confidence_score": 1.0})
|
|
1532
|
-
|
|
1533
|
-
def add_edge(src: str, tgt: str, relation: str, line: int,
|
|
1534
|
-
confidence: str = "EXTRACTED", score: float = 1.0) -> None:
|
|
1535
|
-
edges.append({"source": src, "target": tgt, "relation": relation,
|
|
1536
|
-
"confidence": confidence, "confidence_score": score,
|
|
1537
|
-
"source_file": str_path, "source_location": f"L{line}", "weight": 1.0})
|
|
1538
|
-
|
|
1539
|
-
file_nid = _make_id(str(path))
|
|
1540
|
-
add_node(file_nid, path.name, 1)
|
|
1541
|
-
|
|
1542
|
-
def walk(node, module_nid: str | None = None) -> None:
|
|
1543
|
-
t = node.type
|
|
1544
|
-
|
|
1545
|
-
if t == "module_declaration":
|
|
1546
|
-
name_node = node.child_by_field_name("name")
|
|
1547
|
-
if name_node:
|
|
1548
|
-
mod_name = _read_text(name_node, source)
|
|
1549
|
-
line = node.start_point[0] + 1
|
|
1550
|
-
nid = _make_id(stem, mod_name)
|
|
1551
|
-
add_node(nid, mod_name, line)
|
|
1552
|
-
add_edge(file_nid, nid, "defines", line)
|
|
1553
|
-
for child in node.children:
|
|
1554
|
-
walk(child, nid)
|
|
1555
|
-
return
|
|
1556
|
-
|
|
1557
|
-
elif t in ("function_declaration", "function_prototype"):
|
|
1558
|
-
name_node = node.child_by_field_name("name")
|
|
1559
|
-
if name_node:
|
|
1560
|
-
func_name = _read_text(name_node, source)
|
|
1561
|
-
line = node.start_point[0] + 1
|
|
1562
|
-
parent = module_nid or file_nid
|
|
1563
|
-
nid = _make_id(parent, func_name)
|
|
1564
|
-
add_node(nid, f"{func_name}()", line)
|
|
1565
|
-
add_edge(parent, nid, "contains", line)
|
|
1566
|
-
|
|
1567
|
-
elif t == "task_declaration":
|
|
1568
|
-
name_node = node.child_by_field_name("name")
|
|
1569
|
-
if name_node:
|
|
1570
|
-
task_name = _read_text(name_node, source)
|
|
1571
|
-
line = node.start_point[0] + 1
|
|
1572
|
-
parent = module_nid or file_nid
|
|
1573
|
-
nid = _make_id(parent, task_name)
|
|
1574
|
-
add_node(nid, task_name, line)
|
|
1575
|
-
add_edge(parent, nid, "contains", line)
|
|
1576
|
-
|
|
1577
|
-
elif t == "package_import_declaration":
|
|
1578
|
-
for child in node.children:
|
|
1579
|
-
if child.type == "package_import_item":
|
|
1580
|
-
pkg_text = _read_text(child, source)
|
|
1581
|
-
pkg_name = pkg_text.split("::")[0].strip()
|
|
1582
|
-
if pkg_name:
|
|
1583
|
-
line = node.start_point[0] + 1
|
|
1584
|
-
tgt_nid = _make_id(pkg_name)
|
|
1585
|
-
add_node(tgt_nid, pkg_name, line)
|
|
1586
|
-
src = module_nid or file_nid
|
|
1587
|
-
add_edge(src, tgt_nid, "imports_from", line)
|
|
1588
|
-
|
|
1589
|
-
elif t == "module_instantiation":
|
|
1590
|
-
# module_type instantiates another module
|
|
1591
|
-
type_node = node.child_by_field_name("module_type")
|
|
1592
|
-
if type_node and module_nid:
|
|
1593
|
-
inst_type = _read_text(type_node, source).strip()
|
|
1594
|
-
if inst_type:
|
|
1595
|
-
line = node.start_point[0] + 1
|
|
1596
|
-
tgt_nid = _make_id(inst_type)
|
|
1597
|
-
add_node(tgt_nid, inst_type, line)
|
|
1598
|
-
add_edge(module_nid, tgt_nid, "instantiates", line)
|
|
1599
|
-
|
|
1600
|
-
for child in node.children:
|
|
1601
|
-
walk(child, module_nid)
|
|
1602
|
-
|
|
1603
|
-
walk(root)
|
|
1604
|
-
return {"nodes": nodes, "edges": edges}
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
def extract_lua(path: Path) -> dict:
|
|
1608
|
-
"""Extract functions, methods, require() imports, and calls from a .lua file."""
|
|
1609
|
-
return _extract_generic(path, _LUA_CONFIG)
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
def extract_swift(path: Path) -> dict:
|
|
1613
|
-
"""Extract classes, structs, protocols, functions, imports, and calls from a .swift file."""
|
|
1614
|
-
return _extract_generic(path, _SWIFT_CONFIG)
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
# ── Julia extractor (custom walk) ────────────────────────────────────────────
|
|
1618
|
-
|
|
1619
|
-
def extract_julia(path: Path) -> dict:
|
|
1620
|
-
"""Extract modules, structs, functions, imports, and calls from a .jl file."""
|
|
1621
|
-
try:
|
|
1622
|
-
import tree_sitter_julia as tsjulia
|
|
1623
|
-
from tree_sitter import Language, Parser
|
|
1624
|
-
except ImportError:
|
|
1625
|
-
return {"nodes": [], "edges": [], "error": "tree-sitter-julia not installed"}
|
|
1626
|
-
|
|
1627
|
-
try:
|
|
1628
|
-
language = Language(tsjulia.language())
|
|
1629
|
-
parser = Parser(language)
|
|
1630
|
-
source = path.read_bytes()
|
|
1631
|
-
tree = parser.parse(source)
|
|
1632
|
-
root = tree.root_node
|
|
1633
|
-
except Exception as e:
|
|
1634
|
-
return {"nodes": [], "edges": [], "error": str(e)}
|
|
1635
|
-
|
|
1636
|
-
stem = path.stem
|
|
1637
|
-
str_path = str(path)
|
|
1638
|
-
nodes: list[dict] = []
|
|
1639
|
-
edges: list[dict] = []
|
|
1640
|
-
seen_ids: set[str] = set()
|
|
1641
|
-
function_bodies: list[tuple[str, object]] = []
|
|
1642
|
-
|
|
1643
|
-
def add_node(nid: str, label: str, line: int) -> None:
|
|
1644
|
-
if nid not in seen_ids:
|
|
1645
|
-
seen_ids.add(nid)
|
|
1646
|
-
nodes.append({
|
|
1647
|
-
"id": nid,
|
|
1648
|
-
"label": label,
|
|
1649
|
-
"file_type": "code",
|
|
1650
|
-
"source_file": str_path,
|
|
1651
|
-
"source_location": f"L{line}",
|
|
1652
|
-
})
|
|
1653
|
-
|
|
1654
|
-
def add_edge(src: str, tgt: str, relation: str, line: int,
|
|
1655
|
-
confidence: str = "EXTRACTED", weight: float = 1.0) -> None:
|
|
1656
|
-
edges.append({
|
|
1657
|
-
"source": src,
|
|
1658
|
-
"target": tgt,
|
|
1659
|
-
"relation": relation,
|
|
1660
|
-
"confidence": confidence,
|
|
1661
|
-
"source_file": str_path,
|
|
1662
|
-
"source_location": f"L{line}",
|
|
1663
|
-
"weight": weight,
|
|
1664
|
-
})
|
|
1665
|
-
|
|
1666
|
-
file_nid = _make_id(str(path))
|
|
1667
|
-
add_node(file_nid, path.name, 1)
|
|
1668
|
-
|
|
1669
|
-
def _func_name_from_signature(sig_node) -> str | None:
|
|
1670
|
-
"""Extract function name from a Julia signature node (call_expression > identifier)."""
|
|
1671
|
-
for child in sig_node.children:
|
|
1672
|
-
if child.type == "call_expression":
|
|
1673
|
-
callee = child.children[0] if child.children else None
|
|
1674
|
-
if callee and callee.type == "identifier":
|
|
1675
|
-
return _read_text(callee, source)
|
|
1676
|
-
return None
|
|
1677
|
-
|
|
1678
|
-
def walk_calls(body_node, func_nid: str) -> None:
|
|
1679
|
-
if body_node is None:
|
|
1680
|
-
return
|
|
1681
|
-
t = body_node.type
|
|
1682
|
-
if t in ("function_definition", "short_function_definition"):
|
|
1683
|
-
return
|
|
1684
|
-
if t == "call_expression" and body_node.children:
|
|
1685
|
-
callee = body_node.children[0]
|
|
1686
|
-
# Direct call: foo(...)
|
|
1687
|
-
if callee.type == "identifier":
|
|
1688
|
-
callee_name = _read_text(callee, source)
|
|
1689
|
-
target_nid = _make_id(stem, callee_name)
|
|
1690
|
-
add_edge(func_nid, target_nid, "calls", body_node.start_point[0] + 1,
|
|
1691
|
-
confidence="EXTRACTED")
|
|
1692
|
-
# Method call: obj.method(...)
|
|
1693
|
-
elif callee.type == "field_expression" and len(callee.children) >= 3:
|
|
1694
|
-
method_node = callee.children[-1]
|
|
1695
|
-
method_name = _read_text(method_node, source)
|
|
1696
|
-
target_nid = _make_id(stem, method_name)
|
|
1697
|
-
add_edge(func_nid, target_nid, "calls", body_node.start_point[0] + 1,
|
|
1698
|
-
confidence="EXTRACTED")
|
|
1699
|
-
for child in body_node.children:
|
|
1700
|
-
walk_calls(child, func_nid)
|
|
1701
|
-
|
|
1702
|
-
def walk(node, scope_nid: str) -> None:
|
|
1703
|
-
t = node.type
|
|
1704
|
-
|
|
1705
|
-
# Module
|
|
1706
|
-
if t == "module_definition":
|
|
1707
|
-
name_node = next((c for c in node.children if c.type == "identifier"), None)
|
|
1708
|
-
if name_node:
|
|
1709
|
-
mod_name = _read_text(name_node, source)
|
|
1710
|
-
mod_nid = _make_id(stem, mod_name)
|
|
1711
|
-
line = node.start_point[0] + 1
|
|
1712
|
-
add_node(mod_nid, mod_name, line)
|
|
1713
|
-
add_edge(file_nid, mod_nid, "defines", line)
|
|
1714
|
-
for child in node.children:
|
|
1715
|
-
walk(child, mod_nid)
|
|
1716
|
-
return
|
|
1717
|
-
|
|
1718
|
-
# Struct (struct / mutable struct — both map to struct_definition in tree-sitter-julia)
|
|
1719
|
-
if t == "struct_definition":
|
|
1720
|
-
# type_head may contain: identifier (simple) or binary_expression (Foo <: Bar)
|
|
1721
|
-
type_head = next((c for c in node.children if c.type == "type_head"), None)
|
|
1722
|
-
if type_head:
|
|
1723
|
-
bin_expr = next((c for c in type_head.children if c.type == "binary_expression"), None)
|
|
1724
|
-
if bin_expr:
|
|
1725
|
-
# First identifier is the struct name, last is the supertype
|
|
1726
|
-
identifiers = [c for c in bin_expr.children if c.type == "identifier"]
|
|
1727
|
-
if identifiers:
|
|
1728
|
-
struct_name = _read_text(identifiers[0], source)
|
|
1729
|
-
struct_nid = _make_id(stem, struct_name)
|
|
1730
|
-
line = node.start_point[0] + 1
|
|
1731
|
-
add_node(struct_nid, struct_name, line)
|
|
1732
|
-
add_edge(scope_nid, struct_nid, "defines", line)
|
|
1733
|
-
if len(identifiers) >= 2:
|
|
1734
|
-
super_name = _read_text(identifiers[-1], source)
|
|
1735
|
-
add_edge(struct_nid, _make_id(stem, super_name), "inherits",
|
|
1736
|
-
line, confidence="EXTRACTED")
|
|
1737
|
-
else:
|
|
1738
|
-
name_node = next((c for c in type_head.children if c.type == "identifier"), None)
|
|
1739
|
-
if name_node:
|
|
1740
|
-
struct_name = _read_text(name_node, source)
|
|
1741
|
-
struct_nid = _make_id(stem, struct_name)
|
|
1742
|
-
line = node.start_point[0] + 1
|
|
1743
|
-
add_node(struct_nid, struct_name, line)
|
|
1744
|
-
add_edge(scope_nid, struct_nid, "defines", line)
|
|
1745
|
-
return
|
|
1746
|
-
|
|
1747
|
-
# Abstract type
|
|
1748
|
-
if t == "abstract_definition":
|
|
1749
|
-
# type_head > identifier
|
|
1750
|
-
type_head = next((c for c in node.children if c.type == "type_head"), None)
|
|
1751
|
-
if type_head:
|
|
1752
|
-
name_node = next((c for c in type_head.children if c.type == "identifier"), None)
|
|
1753
|
-
if name_node:
|
|
1754
|
-
abs_name = _read_text(name_node, source)
|
|
1755
|
-
abs_nid = _make_id(stem, abs_name)
|
|
1756
|
-
line = node.start_point[0] + 1
|
|
1757
|
-
add_node(abs_nid, abs_name, line)
|
|
1758
|
-
add_edge(scope_nid, abs_nid, "defines", line)
|
|
1759
|
-
return
|
|
1760
|
-
|
|
1761
|
-
# Function: function foo(...) ... end
|
|
1762
|
-
if t == "function_definition":
|
|
1763
|
-
sig_node = next((c for c in node.children if c.type == "signature"), None)
|
|
1764
|
-
if sig_node:
|
|
1765
|
-
func_name = _func_name_from_signature(sig_node)
|
|
1766
|
-
if func_name:
|
|
1767
|
-
func_nid = _make_id(stem, func_name)
|
|
1768
|
-
line = node.start_point[0] + 1
|
|
1769
|
-
add_node(func_nid, f"{func_name}()", line)
|
|
1770
|
-
add_edge(scope_nid, func_nid, "defines", line)
|
|
1771
|
-
function_bodies.append((func_nid, node))
|
|
1772
|
-
return
|
|
1773
|
-
|
|
1774
|
-
# Short function: foo(x) = expr
|
|
1775
|
-
if t == "assignment":
|
|
1776
|
-
lhs = node.children[0] if node.children else None
|
|
1777
|
-
if lhs and lhs.type == "call_expression" and lhs.children:
|
|
1778
|
-
callee = lhs.children[0]
|
|
1779
|
-
if callee.type == "identifier":
|
|
1780
|
-
func_name = _read_text(callee, source)
|
|
1781
|
-
func_nid = _make_id(stem, func_name)
|
|
1782
|
-
line = node.start_point[0] + 1
|
|
1783
|
-
add_node(func_nid, f"{func_name}()", line)
|
|
1784
|
-
add_edge(scope_nid, func_nid, "defines", line)
|
|
1785
|
-
# Only walk the RHS (index 2 after lhs and operator) to avoid self-loops
|
|
1786
|
-
rhs = node.children[-1] if len(node.children) >= 3 else None
|
|
1787
|
-
if rhs:
|
|
1788
|
-
function_bodies.append((func_nid, rhs))
|
|
1789
|
-
return
|
|
1790
|
-
|
|
1791
|
-
# Using / Import
|
|
1792
|
-
if t in ("using_statement", "import_statement"):
|
|
1793
|
-
line = node.start_point[0] + 1
|
|
1794
|
-
for child in node.children:
|
|
1795
|
-
if child.type == "identifier":
|
|
1796
|
-
mod_name = _read_text(child, source)
|
|
1797
|
-
imp_nid = _make_id(mod_name)
|
|
1798
|
-
add_node(imp_nid, mod_name, line)
|
|
1799
|
-
add_edge(scope_nid, imp_nid, "imports", line)
|
|
1800
|
-
elif child.type == "selected_import":
|
|
1801
|
-
identifiers = [c for c in child.children if c.type == "identifier"]
|
|
1802
|
-
if identifiers:
|
|
1803
|
-
pkg_name = _read_text(identifiers[0], source)
|
|
1804
|
-
pkg_nid = _make_id(pkg_name)
|
|
1805
|
-
add_node(pkg_nid, pkg_name, line)
|
|
1806
|
-
add_edge(scope_nid, pkg_nid, "imports", line)
|
|
1807
|
-
return
|
|
1808
|
-
|
|
1809
|
-
for child in node.children:
|
|
1810
|
-
walk(child, scope_nid)
|
|
1811
|
-
|
|
1812
|
-
walk(root, file_nid)
|
|
1813
|
-
|
|
1814
|
-
for func_nid, body_node in function_bodies:
|
|
1815
|
-
# For function_definition nodes, walk children directly to avoid
|
|
1816
|
-
# the boundary check returning early on the top-level node itself.
|
|
1817
|
-
# Skip the "signature" child — it contains the function's own call_expression
|
|
1818
|
-
# which would create a self-loop.
|
|
1819
|
-
if body_node.type == "function_definition":
|
|
1820
|
-
for child in body_node.children:
|
|
1821
|
-
if child.type != "signature":
|
|
1822
|
-
walk_calls(child, func_nid)
|
|
1823
|
-
else:
|
|
1824
|
-
walk_calls(body_node, func_nid)
|
|
1825
|
-
|
|
1826
|
-
return {"nodes": nodes, "edges": edges}
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
# ── Go extractor (custom walk) ────────────────────────────────────────────────
|
|
1830
|
-
|
|
1831
|
-
def extract_go(path: Path) -> dict:
|
|
1832
|
-
"""Extract functions, methods, type declarations, and imports from a .go file."""
|
|
1833
|
-
try:
|
|
1834
|
-
import tree_sitter_go as tsgo
|
|
1835
|
-
from tree_sitter import Language, Parser
|
|
1836
|
-
except ImportError:
|
|
1837
|
-
return {"nodes": [], "edges": [], "error": "tree-sitter-go not installed"}
|
|
1838
|
-
|
|
1839
|
-
try:
|
|
1840
|
-
language = Language(tsgo.language())
|
|
1841
|
-
parser = Parser(language)
|
|
1842
|
-
source = path.read_bytes()
|
|
1843
|
-
tree = parser.parse(source)
|
|
1844
|
-
root = tree.root_node
|
|
1845
|
-
except Exception as e:
|
|
1846
|
-
return {"nodes": [], "edges": [], "error": str(e)}
|
|
1847
|
-
|
|
1848
|
-
stem = path.stem
|
|
1849
|
-
# Use directory name as package scope so methods on the same type across
|
|
1850
|
-
# multiple files in a package share one canonical type node.
|
|
1851
|
-
pkg_scope = path.parent.name or stem
|
|
1852
|
-
str_path = str(path)
|
|
1853
|
-
nodes: list[dict] = []
|
|
1854
|
-
edges: list[dict] = []
|
|
1855
|
-
seen_ids: set[str] = set()
|
|
1856
|
-
function_bodies: list[tuple[str, object]] = []
|
|
1857
|
-
|
|
1858
|
-
def add_node(nid: str, label: str, line: int) -> None:
|
|
1859
|
-
if nid not in seen_ids:
|
|
1860
|
-
seen_ids.add(nid)
|
|
1861
|
-
nodes.append({
|
|
1862
|
-
"id": nid,
|
|
1863
|
-
"label": label,
|
|
1864
|
-
"file_type": "code",
|
|
1865
|
-
"source_file": str_path,
|
|
1866
|
-
"source_location": f"L{line}",
|
|
1867
|
-
})
|
|
1868
|
-
|
|
1869
|
-
def add_edge(src: str, tgt: str, relation: str, line: int,
|
|
1870
|
-
confidence: str = "EXTRACTED", weight: float = 1.0) -> None:
|
|
1871
|
-
edges.append({
|
|
1872
|
-
"source": src,
|
|
1873
|
-
"target": tgt,
|
|
1874
|
-
"relation": relation,
|
|
1875
|
-
"confidence": confidence,
|
|
1876
|
-
"source_file": str_path,
|
|
1877
|
-
"source_location": f"L{line}",
|
|
1878
|
-
"weight": weight,
|
|
1879
|
-
})
|
|
1880
|
-
|
|
1881
|
-
file_nid = _make_id(str(path))
|
|
1882
|
-
add_node(file_nid, path.name, 1)
|
|
1883
|
-
|
|
1884
|
-
def walk(node) -> None:
|
|
1885
|
-
t = node.type
|
|
1886
|
-
|
|
1887
|
-
if t == "function_declaration":
|
|
1888
|
-
name_node = node.child_by_field_name("name")
|
|
1889
|
-
if name_node:
|
|
1890
|
-
func_name = _read_text(name_node, source)
|
|
1891
|
-
line = node.start_point[0] + 1
|
|
1892
|
-
func_nid = _make_id(stem, func_name)
|
|
1893
|
-
add_node(func_nid, f"{func_name}()", line)
|
|
1894
|
-
add_edge(file_nid, func_nid, "contains", line)
|
|
1895
|
-
body = node.child_by_field_name("body")
|
|
1896
|
-
if body:
|
|
1897
|
-
function_bodies.append((func_nid, body))
|
|
1898
|
-
return
|
|
1899
|
-
|
|
1900
|
-
if t == "method_declaration":
|
|
1901
|
-
receiver = node.child_by_field_name("receiver")
|
|
1902
|
-
receiver_type: str | None = None
|
|
1903
|
-
if receiver:
|
|
1904
|
-
for param in receiver.children:
|
|
1905
|
-
if param.type == "parameter_declaration":
|
|
1906
|
-
type_node = param.child_by_field_name("type")
|
|
1907
|
-
if type_node:
|
|
1908
|
-
raw = _read_text(type_node, source).lstrip("*").strip()
|
|
1909
|
-
receiver_type = raw
|
|
1910
|
-
break
|
|
1911
|
-
name_node = node.child_by_field_name("name")
|
|
1912
|
-
if name_node:
|
|
1913
|
-
method_name = _read_text(name_node, source)
|
|
1914
|
-
line = node.start_point[0] + 1
|
|
1915
|
-
if receiver_type:
|
|
1916
|
-
parent_nid = _make_id(pkg_scope, receiver_type)
|
|
1917
|
-
add_node(parent_nid, receiver_type, line)
|
|
1918
|
-
method_nid = _make_id(parent_nid, method_name)
|
|
1919
|
-
add_node(method_nid, f".{method_name}()", line)
|
|
1920
|
-
add_edge(parent_nid, method_nid, "method", line)
|
|
1921
|
-
else:
|
|
1922
|
-
method_nid = _make_id(stem, method_name)
|
|
1923
|
-
add_node(method_nid, f"{method_name}()", line)
|
|
1924
|
-
add_edge(file_nid, method_nid, "contains", line)
|
|
1925
|
-
body = node.child_by_field_name("body")
|
|
1926
|
-
if body:
|
|
1927
|
-
function_bodies.append((method_nid, body))
|
|
1928
|
-
return
|
|
1929
|
-
|
|
1930
|
-
if t == "type_declaration":
|
|
1931
|
-
for child in node.children:
|
|
1932
|
-
if child.type == "type_spec":
|
|
1933
|
-
name_node = child.child_by_field_name("name")
|
|
1934
|
-
if name_node:
|
|
1935
|
-
type_name = _read_text(name_node, source)
|
|
1936
|
-
line = child.start_point[0] + 1
|
|
1937
|
-
type_nid = _make_id(pkg_scope, type_name)
|
|
1938
|
-
add_node(type_nid, type_name, line)
|
|
1939
|
-
add_edge(file_nid, type_nid, "contains", line)
|
|
1940
|
-
return
|
|
1941
|
-
|
|
1942
|
-
if t == "import_declaration":
|
|
1943
|
-
for child in node.children:
|
|
1944
|
-
if child.type == "import_spec_list":
|
|
1945
|
-
for spec in child.children:
|
|
1946
|
-
if spec.type == "import_spec":
|
|
1947
|
-
path_node = spec.child_by_field_name("path")
|
|
1948
|
-
if path_node:
|
|
1949
|
-
raw = _read_text(path_node, source).strip('"')
|
|
1950
|
-
module_name = raw.split("/")[-1]
|
|
1951
|
-
tgt_nid = _make_id(module_name)
|
|
1952
|
-
add_edge(file_nid, tgt_nid, "imports_from", spec.start_point[0] + 1)
|
|
1953
|
-
elif child.type == "import_spec":
|
|
1954
|
-
path_node = child.child_by_field_name("path")
|
|
1955
|
-
if path_node:
|
|
1956
|
-
raw = _read_text(path_node, source).strip('"')
|
|
1957
|
-
module_name = raw.split("/")[-1]
|
|
1958
|
-
tgt_nid = _make_id(module_name)
|
|
1959
|
-
add_edge(file_nid, tgt_nid, "imports_from", child.start_point[0] + 1)
|
|
1960
|
-
return
|
|
1961
|
-
|
|
1962
|
-
for child in node.children:
|
|
1963
|
-
walk(child)
|
|
1964
|
-
|
|
1965
|
-
walk(root)
|
|
1966
|
-
|
|
1967
|
-
label_to_nid: dict[str, str] = {}
|
|
1968
|
-
for n in nodes:
|
|
1969
|
-
raw = n["label"]
|
|
1970
|
-
normalised = raw.strip("()").lstrip(".")
|
|
1971
|
-
label_to_nid[normalised.lower()] = n["id"]
|
|
1972
|
-
|
|
1973
|
-
seen_call_pairs: set[tuple[str, str]] = set()
|
|
1974
|
-
raw_calls: list[dict] = []
|
|
1975
|
-
|
|
1976
|
-
def walk_calls(node, caller_nid: str) -> None:
|
|
1977
|
-
if node.type in ("function_declaration", "method_declaration"):
|
|
1978
|
-
return
|
|
1979
|
-
if node.type == "call_expression":
|
|
1980
|
-
func_node = node.child_by_field_name("function")
|
|
1981
|
-
callee_name: str | None = None
|
|
1982
|
-
if func_node:
|
|
1983
|
-
if func_node.type == "identifier":
|
|
1984
|
-
callee_name = _read_text(func_node, source)
|
|
1985
|
-
elif func_node.type == "selector_expression":
|
|
1986
|
-
field = func_node.child_by_field_name("field")
|
|
1987
|
-
if field:
|
|
1988
|
-
callee_name = _read_text(field, source)
|
|
1989
|
-
if callee_name:
|
|
1990
|
-
tgt_nid = label_to_nid.get(callee_name.lower())
|
|
1991
|
-
if tgt_nid and tgt_nid != caller_nid:
|
|
1992
|
-
pair = (caller_nid, tgt_nid)
|
|
1993
|
-
if pair not in seen_call_pairs:
|
|
1994
|
-
seen_call_pairs.add(pair)
|
|
1995
|
-
line = node.start_point[0] + 1
|
|
1996
|
-
edges.append({
|
|
1997
|
-
"source": caller_nid,
|
|
1998
|
-
"target": tgt_nid,
|
|
1999
|
-
"relation": "calls",
|
|
2000
|
-
"confidence": "EXTRACTED",
|
|
2001
|
-
"source_file": str_path,
|
|
2002
|
-
"source_location": f"L{line}",
|
|
2003
|
-
"weight": 1.0,
|
|
2004
|
-
})
|
|
2005
|
-
elif callee_name:
|
|
2006
|
-
raw_calls.append({
|
|
2007
|
-
"caller_nid": caller_nid,
|
|
2008
|
-
"callee": callee_name,
|
|
2009
|
-
"source_file": str_path,
|
|
2010
|
-
"source_location": f"L{node.start_point[0] + 1}",
|
|
2011
|
-
})
|
|
2012
|
-
for child in node.children:
|
|
2013
|
-
walk_calls(child, caller_nid)
|
|
2014
|
-
|
|
2015
|
-
for caller_nid, body_node in function_bodies:
|
|
2016
|
-
walk_calls(body_node, caller_nid)
|
|
2017
|
-
|
|
2018
|
-
valid_ids = seen_ids
|
|
2019
|
-
clean_edges = []
|
|
2020
|
-
for edge in edges:
|
|
2021
|
-
src, tgt = edge["source"], edge["target"]
|
|
2022
|
-
if src in valid_ids and (tgt in valid_ids or edge["relation"] in ("imports", "imports_from")):
|
|
2023
|
-
clean_edges.append(edge)
|
|
2024
|
-
|
|
2025
|
-
return {"nodes": nodes, "edges": clean_edges, "raw_calls": raw_calls}
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
# ── Rust extractor (custom walk) ──────────────────────────────────────────────
|
|
2029
|
-
|
|
2030
|
-
def extract_rust(path: Path) -> dict:
|
|
2031
|
-
"""Extract functions, structs, enums, traits, impl methods, and use declarations from a .rs file."""
|
|
2032
|
-
try:
|
|
2033
|
-
import tree_sitter_rust as tsrust
|
|
2034
|
-
from tree_sitter import Language, Parser
|
|
2035
|
-
except ImportError:
|
|
2036
|
-
return {"nodes": [], "edges": [], "error": "tree-sitter-rust not installed"}
|
|
2037
|
-
|
|
2038
|
-
try:
|
|
2039
|
-
language = Language(tsrust.language())
|
|
2040
|
-
parser = Parser(language)
|
|
2041
|
-
source = path.read_bytes()
|
|
2042
|
-
tree = parser.parse(source)
|
|
2043
|
-
root = tree.root_node
|
|
2044
|
-
except Exception as e:
|
|
2045
|
-
return {"nodes": [], "edges": [], "error": str(e)}
|
|
2046
|
-
|
|
2047
|
-
stem = path.stem
|
|
2048
|
-
str_path = str(path)
|
|
2049
|
-
nodes: list[dict] = []
|
|
2050
|
-
edges: list[dict] = []
|
|
2051
|
-
seen_ids: set[str] = set()
|
|
2052
|
-
function_bodies: list[tuple[str, object]] = []
|
|
2053
|
-
|
|
2054
|
-
def add_node(nid: str, label: str, line: int) -> None:
|
|
2055
|
-
if nid not in seen_ids:
|
|
2056
|
-
seen_ids.add(nid)
|
|
2057
|
-
nodes.append({
|
|
2058
|
-
"id": nid,
|
|
2059
|
-
"label": label,
|
|
2060
|
-
"file_type": "code",
|
|
2061
|
-
"source_file": str_path,
|
|
2062
|
-
"source_location": f"L{line}",
|
|
2063
|
-
})
|
|
2064
|
-
|
|
2065
|
-
def add_edge(src: str, tgt: str, relation: str, line: int,
|
|
2066
|
-
confidence: str = "EXTRACTED", weight: float = 1.0) -> None:
|
|
2067
|
-
edges.append({
|
|
2068
|
-
"source": src,
|
|
2069
|
-
"target": tgt,
|
|
2070
|
-
"relation": relation,
|
|
2071
|
-
"confidence": confidence,
|
|
2072
|
-
"source_file": str_path,
|
|
2073
|
-
"source_location": f"L{line}",
|
|
2074
|
-
"weight": weight,
|
|
2075
|
-
})
|
|
2076
|
-
|
|
2077
|
-
file_nid = _make_id(str(path))
|
|
2078
|
-
add_node(file_nid, path.name, 1)
|
|
2079
|
-
|
|
2080
|
-
def walk(node, parent_impl_nid: str | None = None) -> None:
|
|
2081
|
-
t = node.type
|
|
2082
|
-
|
|
2083
|
-
if t == "function_item":
|
|
2084
|
-
name_node = node.child_by_field_name("name")
|
|
2085
|
-
if name_node:
|
|
2086
|
-
func_name = _read_text(name_node, source)
|
|
2087
|
-
line = node.start_point[0] + 1
|
|
2088
|
-
if parent_impl_nid:
|
|
2089
|
-
func_nid = _make_id(parent_impl_nid, func_name)
|
|
2090
|
-
add_node(func_nid, f".{func_name}()", line)
|
|
2091
|
-
add_edge(parent_impl_nid, func_nid, "method", line)
|
|
2092
|
-
else:
|
|
2093
|
-
func_nid = _make_id(stem, func_name)
|
|
2094
|
-
add_node(func_nid, f"{func_name}()", line)
|
|
2095
|
-
add_edge(file_nid, func_nid, "contains", line)
|
|
2096
|
-
body = node.child_by_field_name("body")
|
|
2097
|
-
if body:
|
|
2098
|
-
function_bodies.append((func_nid, body))
|
|
2099
|
-
return
|
|
2100
|
-
|
|
2101
|
-
if t in ("struct_item", "enum_item", "trait_item"):
|
|
2102
|
-
name_node = node.child_by_field_name("name")
|
|
2103
|
-
if name_node:
|
|
2104
|
-
item_name = _read_text(name_node, source)
|
|
2105
|
-
line = node.start_point[0] + 1
|
|
2106
|
-
item_nid = _make_id(stem, item_name)
|
|
2107
|
-
add_node(item_nid, item_name, line)
|
|
2108
|
-
add_edge(file_nid, item_nid, "contains", line)
|
|
2109
|
-
return
|
|
2110
|
-
|
|
2111
|
-
if t == "impl_item":
|
|
2112
|
-
type_node = node.child_by_field_name("type")
|
|
2113
|
-
impl_nid: str | None = None
|
|
2114
|
-
if type_node:
|
|
2115
|
-
type_name = _read_text(type_node, source).strip()
|
|
2116
|
-
impl_nid = _make_id(stem, type_name)
|
|
2117
|
-
add_node(impl_nid, type_name, node.start_point[0] + 1)
|
|
2118
|
-
body = node.child_by_field_name("body")
|
|
2119
|
-
if body:
|
|
2120
|
-
for child in body.children:
|
|
2121
|
-
walk(child, parent_impl_nid=impl_nid)
|
|
2122
|
-
return
|
|
2123
|
-
|
|
2124
|
-
if t == "use_declaration":
|
|
2125
|
-
arg = node.child_by_field_name("argument")
|
|
2126
|
-
if arg:
|
|
2127
|
-
raw = _read_text(arg, source)
|
|
2128
|
-
clean = raw.split("{")[0].rstrip(":").rstrip("*").rstrip(":")
|
|
2129
|
-
module_name = clean.split("::")[-1].strip()
|
|
2130
|
-
if module_name:
|
|
2131
|
-
tgt_nid = _make_id(module_name)
|
|
2132
|
-
add_edge(file_nid, tgt_nid, "imports_from", node.start_point[0] + 1)
|
|
2133
|
-
return
|
|
2134
|
-
|
|
2135
|
-
for child in node.children:
|
|
2136
|
-
walk(child, parent_impl_nid=None)
|
|
2137
|
-
|
|
2138
|
-
walk(root)
|
|
2139
|
-
|
|
2140
|
-
label_to_nid: dict[str, str] = {}
|
|
2141
|
-
for n in nodes:
|
|
2142
|
-
raw = n["label"]
|
|
2143
|
-
normalised = raw.strip("()").lstrip(".")
|
|
2144
|
-
label_to_nid[normalised.lower()] = n["id"]
|
|
2145
|
-
|
|
2146
|
-
seen_call_pairs: set[tuple[str, str]] = set()
|
|
2147
|
-
raw_calls: list[dict] = []
|
|
2148
|
-
|
|
2149
|
-
def walk_calls(node, caller_nid: str) -> None:
|
|
2150
|
-
if node.type == "function_item":
|
|
2151
|
-
return
|
|
2152
|
-
if node.type == "call_expression":
|
|
2153
|
-
func_node = node.child_by_field_name("function")
|
|
2154
|
-
callee_name: str | None = None
|
|
2155
|
-
if func_node:
|
|
2156
|
-
if func_node.type == "identifier":
|
|
2157
|
-
callee_name = _read_text(func_node, source)
|
|
2158
|
-
elif func_node.type == "field_expression":
|
|
2159
|
-
field = func_node.child_by_field_name("field")
|
|
2160
|
-
if field:
|
|
2161
|
-
callee_name = _read_text(field, source)
|
|
2162
|
-
elif func_node.type == "scoped_identifier":
|
|
2163
|
-
name = func_node.child_by_field_name("name")
|
|
2164
|
-
if name:
|
|
2165
|
-
callee_name = _read_text(name, source)
|
|
2166
|
-
if callee_name:
|
|
2167
|
-
tgt_nid = label_to_nid.get(callee_name.lower())
|
|
2168
|
-
if tgt_nid and tgt_nid != caller_nid:
|
|
2169
|
-
pair = (caller_nid, tgt_nid)
|
|
2170
|
-
if pair not in seen_call_pairs:
|
|
2171
|
-
seen_call_pairs.add(pair)
|
|
2172
|
-
line = node.start_point[0] + 1
|
|
2173
|
-
edges.append({
|
|
2174
|
-
"source": caller_nid,
|
|
2175
|
-
"target": tgt_nid,
|
|
2176
|
-
"relation": "calls",
|
|
2177
|
-
"confidence": "EXTRACTED",
|
|
2178
|
-
"source_file": str_path,
|
|
2179
|
-
"source_location": f"L{line}",
|
|
2180
|
-
"weight": 1.0,
|
|
2181
|
-
})
|
|
2182
|
-
else:
|
|
2183
|
-
raw_calls.append({
|
|
2184
|
-
"caller_nid": caller_nid,
|
|
2185
|
-
"callee": callee_name,
|
|
2186
|
-
"source_file": str_path,
|
|
2187
|
-
"source_location": f"L{node.start_point[0] + 1}",
|
|
2188
|
-
})
|
|
2189
|
-
for child in node.children:
|
|
2190
|
-
walk_calls(child, caller_nid)
|
|
2191
|
-
|
|
2192
|
-
for caller_nid, body_node in function_bodies:
|
|
2193
|
-
walk_calls(body_node, caller_nid)
|
|
2194
|
-
|
|
2195
|
-
valid_ids = seen_ids
|
|
2196
|
-
clean_edges = []
|
|
2197
|
-
for edge in edges:
|
|
2198
|
-
src, tgt = edge["source"], edge["target"]
|
|
2199
|
-
if src in valid_ids and (tgt in valid_ids or edge["relation"] in ("imports", "imports_from")):
|
|
2200
|
-
clean_edges.append(edge)
|
|
2201
|
-
|
|
2202
|
-
return {"nodes": nodes, "edges": clean_edges, "raw_calls": raw_calls}
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
# ── Zig ───────────────────────────────────────────────────────────────────────
|
|
2206
|
-
|
|
2207
|
-
def extract_zig(path: Path) -> dict:
|
|
2208
|
-
"""Extract functions, structs, enums, unions, and imports from a .zig file."""
|
|
2209
|
-
try:
|
|
2210
|
-
import tree_sitter_zig as tszig
|
|
2211
|
-
from tree_sitter import Language, Parser
|
|
2212
|
-
except ImportError:
|
|
2213
|
-
return {"nodes": [], "edges": [], "error": "tree_sitter_zig not installed"}
|
|
2214
|
-
|
|
2215
|
-
try:
|
|
2216
|
-
language = Language(tszig.language())
|
|
2217
|
-
parser = Parser(language)
|
|
2218
|
-
source = path.read_bytes()
|
|
2219
|
-
tree = parser.parse(source)
|
|
2220
|
-
root = tree.root_node
|
|
2221
|
-
except Exception as e:
|
|
2222
|
-
return {"nodes": [], "edges": [], "error": str(e)}
|
|
2223
|
-
|
|
2224
|
-
stem = path.stem
|
|
2225
|
-
str_path = str(path)
|
|
2226
|
-
nodes: list[dict] = []
|
|
2227
|
-
edges: list[dict] = []
|
|
2228
|
-
seen_ids: set[str] = set()
|
|
2229
|
-
function_bodies: list[tuple[str, Any]] = []
|
|
2230
|
-
|
|
2231
|
-
def add_node(nid: str, label: str, line: int) -> None:
|
|
2232
|
-
if nid not in seen_ids:
|
|
2233
|
-
seen_ids.add(nid)
|
|
2234
|
-
nodes.append({"id": nid, "label": label, "file_type": "code",
|
|
2235
|
-
"source_file": str_path, "source_location": f"L{line}"})
|
|
2236
|
-
|
|
2237
|
-
def add_edge(src: str, tgt: str, relation: str, line: int,
|
|
2238
|
-
confidence: str = "EXTRACTED", weight: float = 1.0) -> None:
|
|
2239
|
-
edges.append({"source": src, "target": tgt, "relation": relation,
|
|
2240
|
-
"confidence": confidence, "source_file": str_path,
|
|
2241
|
-
"source_location": f"L{line}", "weight": weight})
|
|
2242
|
-
|
|
2243
|
-
file_nid = _make_id(str(path))
|
|
2244
|
-
add_node(file_nid, path.name, 1)
|
|
2245
|
-
|
|
2246
|
-
def _extract_import(node) -> None:
|
|
2247
|
-
for child in node.children:
|
|
2248
|
-
if child.type == "builtin_function":
|
|
2249
|
-
bi = None
|
|
2250
|
-
args = None
|
|
2251
|
-
for c in child.children:
|
|
2252
|
-
if c.type == "builtin_identifier":
|
|
2253
|
-
bi = _read_text(c, source)
|
|
2254
|
-
elif c.type == "arguments":
|
|
2255
|
-
args = c
|
|
2256
|
-
if bi in ("@import", "@cImport") and args:
|
|
2257
|
-
for arg in args.children:
|
|
2258
|
-
if arg.type in ("string_literal", "string"):
|
|
2259
|
-
raw = _read_text(arg, source).strip('"')
|
|
2260
|
-
module_name = raw.split("/")[-1].split(".")[0]
|
|
2261
|
-
if module_name:
|
|
2262
|
-
tgt_nid = _make_id(module_name)
|
|
2263
|
-
add_edge(file_nid, tgt_nid, "imports_from",
|
|
2264
|
-
node.start_point[0] + 1)
|
|
2265
|
-
return
|
|
2266
|
-
elif child.type == "field_expression":
|
|
2267
|
-
_extract_import(child)
|
|
2268
|
-
return
|
|
2269
|
-
|
|
2270
|
-
def walk(node, parent_struct_nid: str | None = None) -> None:
|
|
2271
|
-
t = node.type
|
|
2272
|
-
|
|
2273
|
-
if t == "function_declaration":
|
|
2274
|
-
name_node = node.child_by_field_name("name")
|
|
2275
|
-
if name_node:
|
|
2276
|
-
func_name = _read_text(name_node, source)
|
|
2277
|
-
line = node.start_point[0] + 1
|
|
2278
|
-
if parent_struct_nid:
|
|
2279
|
-
func_nid = _make_id(parent_struct_nid, func_name)
|
|
2280
|
-
add_node(func_nid, f".{func_name}()", line)
|
|
2281
|
-
add_edge(parent_struct_nid, func_nid, "method", line)
|
|
2282
|
-
else:
|
|
2283
|
-
func_nid = _make_id(stem, func_name)
|
|
2284
|
-
add_node(func_nid, f"{func_name}()", line)
|
|
2285
|
-
add_edge(file_nid, func_nid, "contains", line)
|
|
2286
|
-
body = node.child_by_field_name("body")
|
|
2287
|
-
if body:
|
|
2288
|
-
function_bodies.append((func_nid, body))
|
|
2289
|
-
return
|
|
2290
|
-
|
|
2291
|
-
if t == "variable_declaration":
|
|
2292
|
-
name_node = None
|
|
2293
|
-
value_node = None
|
|
2294
|
-
for child in node.children:
|
|
2295
|
-
if child.type == "identifier":
|
|
2296
|
-
name_node = child
|
|
2297
|
-
elif child.type in ("struct_declaration", "enum_declaration",
|
|
2298
|
-
"union_declaration", "builtin_function",
|
|
2299
|
-
"field_expression"):
|
|
2300
|
-
value_node = child
|
|
2301
|
-
|
|
2302
|
-
if value_node and value_node.type == "struct_declaration":
|
|
2303
|
-
if name_node:
|
|
2304
|
-
struct_name = _read_text(name_node, source)
|
|
2305
|
-
line = node.start_point[0] + 1
|
|
2306
|
-
struct_nid = _make_id(stem, struct_name)
|
|
2307
|
-
add_node(struct_nid, struct_name, line)
|
|
2308
|
-
add_edge(file_nid, struct_nid, "contains", line)
|
|
2309
|
-
for child in value_node.children:
|
|
2310
|
-
walk(child, parent_struct_nid=struct_nid)
|
|
2311
|
-
return
|
|
2312
|
-
|
|
2313
|
-
if value_node and value_node.type in ("enum_declaration", "union_declaration"):
|
|
2314
|
-
if name_node:
|
|
2315
|
-
type_name = _read_text(name_node, source)
|
|
2316
|
-
line = node.start_point[0] + 1
|
|
2317
|
-
type_nid = _make_id(stem, type_name)
|
|
2318
|
-
add_node(type_nid, type_name, line)
|
|
2319
|
-
add_edge(file_nid, type_nid, "contains", line)
|
|
2320
|
-
return
|
|
2321
|
-
|
|
2322
|
-
if value_node and value_node.type in ("builtin_function", "field_expression"):
|
|
2323
|
-
_extract_import(node)
|
|
2324
|
-
return
|
|
2325
|
-
|
|
2326
|
-
for child in node.children:
|
|
2327
|
-
walk(child, parent_struct_nid)
|
|
2328
|
-
|
|
2329
|
-
walk(root)
|
|
2330
|
-
|
|
2331
|
-
seen_call_pairs: set[tuple[str, str]] = set()
|
|
2332
|
-
raw_calls: list[dict] = []
|
|
2333
|
-
|
|
2334
|
-
def walk_calls(node, caller_nid: str) -> None:
|
|
2335
|
-
if node.type == "function_declaration":
|
|
2336
|
-
return
|
|
2337
|
-
if node.type == "call_expression":
|
|
2338
|
-
fn = node.child_by_field_name("function")
|
|
2339
|
-
if fn:
|
|
2340
|
-
callee = _read_text(fn, source).split(".")[-1]
|
|
2341
|
-
tgt_nid = next((n["id"] for n in nodes if n["label"] in
|
|
2342
|
-
(f"{callee}()", f".{callee}()")), None)
|
|
2343
|
-
if tgt_nid and tgt_nid != caller_nid:
|
|
2344
|
-
pair = (caller_nid, tgt_nid)
|
|
2345
|
-
if pair not in seen_call_pairs:
|
|
2346
|
-
seen_call_pairs.add(pair)
|
|
2347
|
-
add_edge(caller_nid, tgt_nid, "calls",
|
|
2348
|
-
node.start_point[0] + 1,
|
|
2349
|
-
confidence="EXTRACTED", weight=1.0)
|
|
2350
|
-
elif callee:
|
|
2351
|
-
raw_calls.append({
|
|
2352
|
-
"caller_nid": caller_nid,
|
|
2353
|
-
"callee": callee,
|
|
2354
|
-
"source_file": str_path,
|
|
2355
|
-
"source_location": f"L{node.start_point[0] + 1}",
|
|
2356
|
-
})
|
|
2357
|
-
for child in node.children:
|
|
2358
|
-
walk_calls(child, caller_nid)
|
|
2359
|
-
|
|
2360
|
-
for caller_nid, body_node in function_bodies:
|
|
2361
|
-
walk_calls(body_node, caller_nid)
|
|
2362
|
-
|
|
2363
|
-
clean_edges = [e for e in edges if e["source"] in seen_ids and
|
|
2364
|
-
(e["target"] in seen_ids or e["relation"] == "imports_from")]
|
|
2365
|
-
return {"nodes": nodes, "edges": clean_edges, "raw_calls": raw_calls}
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
# ── PowerShell ────────────────────────────────────────────────────────────────
|
|
2369
|
-
|
|
2370
|
-
def extract_powershell(path: Path) -> dict:
|
|
2371
|
-
"""Extract functions, classes, methods, and using statements from a .ps1 file."""
|
|
2372
|
-
try:
|
|
2373
|
-
import tree_sitter_powershell as tsps
|
|
2374
|
-
from tree_sitter import Language, Parser
|
|
2375
|
-
except ImportError:
|
|
2376
|
-
return {"nodes": [], "edges": [], "error": "tree_sitter_powershell not installed"}
|
|
2377
|
-
|
|
2378
|
-
try:
|
|
2379
|
-
language = Language(tsps.language())
|
|
2380
|
-
parser = Parser(language)
|
|
2381
|
-
source = path.read_bytes()
|
|
2382
|
-
tree = parser.parse(source)
|
|
2383
|
-
root = tree.root_node
|
|
2384
|
-
except Exception as e:
|
|
2385
|
-
return {"nodes": [], "edges": [], "error": str(e)}
|
|
2386
|
-
|
|
2387
|
-
stem = path.stem
|
|
2388
|
-
str_path = str(path)
|
|
2389
|
-
nodes: list[dict] = []
|
|
2390
|
-
edges: list[dict] = []
|
|
2391
|
-
seen_ids: set[str] = set()
|
|
2392
|
-
function_bodies: list[tuple[str, Any]] = []
|
|
2393
|
-
|
|
2394
|
-
def add_node(nid: str, label: str, line: int) -> None:
|
|
2395
|
-
if nid not in seen_ids:
|
|
2396
|
-
seen_ids.add(nid)
|
|
2397
|
-
nodes.append({"id": nid, "label": label, "file_type": "code",
|
|
2398
|
-
"source_file": str_path, "source_location": f"L{line}"})
|
|
2399
|
-
|
|
2400
|
-
def add_edge(src: str, tgt: str, relation: str, line: int,
|
|
2401
|
-
confidence: str = "EXTRACTED", weight: float = 1.0) -> None:
|
|
2402
|
-
edges.append({"source": src, "target": tgt, "relation": relation,
|
|
2403
|
-
"confidence": confidence, "source_file": str_path,
|
|
2404
|
-
"source_location": f"L{line}", "weight": weight})
|
|
2405
|
-
|
|
2406
|
-
file_nid = _make_id(str(path))
|
|
2407
|
-
add_node(file_nid, path.name, 1)
|
|
2408
|
-
|
|
2409
|
-
_PS_SKIP = frozenset({
|
|
2410
|
-
"using", "return", "if", "else", "elseif", "foreach", "for",
|
|
2411
|
-
"while", "do", "switch", "try", "catch", "finally", "throw",
|
|
2412
|
-
"break", "continue", "exit", "param", "begin", "process", "end",
|
|
2413
|
-
})
|
|
2414
|
-
|
|
2415
|
-
def _find_script_block_body(node):
|
|
2416
|
-
for child in node.children:
|
|
2417
|
-
if child.type == "script_block":
|
|
2418
|
-
for sc in child.children:
|
|
2419
|
-
if sc.type == "script_block_body":
|
|
2420
|
-
return sc
|
|
2421
|
-
return child
|
|
2422
|
-
return None
|
|
2423
|
-
|
|
2424
|
-
def walk(node, parent_class_nid: str | None = None) -> None:
|
|
2425
|
-
t = node.type
|
|
2426
|
-
|
|
2427
|
-
if t == "function_statement":
|
|
2428
|
-
name_node = next((c for c in node.children if c.type == "function_name"), None)
|
|
2429
|
-
if name_node:
|
|
2430
|
-
func_name = _read_text(name_node, source)
|
|
2431
|
-
line = node.start_point[0] + 1
|
|
2432
|
-
func_nid = _make_id(stem, func_name)
|
|
2433
|
-
add_node(func_nid, f"{func_name}()", line)
|
|
2434
|
-
add_edge(file_nid, func_nid, "contains", line)
|
|
2435
|
-
body = _find_script_block_body(node)
|
|
2436
|
-
if body:
|
|
2437
|
-
function_bodies.append((func_nid, body))
|
|
2438
|
-
return
|
|
2439
|
-
|
|
2440
|
-
if t == "class_statement":
|
|
2441
|
-
name_node = next((c for c in node.children if c.type == "simple_name"), None)
|
|
2442
|
-
if name_node:
|
|
2443
|
-
class_name = _read_text(name_node, source)
|
|
2444
|
-
line = node.start_point[0] + 1
|
|
2445
|
-
class_nid = _make_id(stem, class_name)
|
|
2446
|
-
add_node(class_nid, class_name, line)
|
|
2447
|
-
add_edge(file_nid, class_nid, "contains", line)
|
|
2448
|
-
for child in node.children:
|
|
2449
|
-
walk(child, parent_class_nid=class_nid)
|
|
2450
|
-
return
|
|
2451
|
-
|
|
2452
|
-
if t == "class_method_definition":
|
|
2453
|
-
name_node = next((c for c in node.children if c.type == "simple_name"), None)
|
|
2454
|
-
if name_node:
|
|
2455
|
-
method_name = _read_text(name_node, source)
|
|
2456
|
-
line = node.start_point[0] + 1
|
|
2457
|
-
if parent_class_nid:
|
|
2458
|
-
method_nid = _make_id(parent_class_nid, method_name)
|
|
2459
|
-
add_node(method_nid, f".{method_name}()", line)
|
|
2460
|
-
add_edge(parent_class_nid, method_nid, "method", line)
|
|
2461
|
-
else:
|
|
2462
|
-
method_nid = _make_id(stem, method_name)
|
|
2463
|
-
add_node(method_nid, f"{method_name}()", line)
|
|
2464
|
-
add_edge(file_nid, method_nid, "contains", line)
|
|
2465
|
-
body = _find_script_block_body(node)
|
|
2466
|
-
if body:
|
|
2467
|
-
function_bodies.append((method_nid, body))
|
|
2468
|
-
return
|
|
2469
|
-
|
|
2470
|
-
if t == "command":
|
|
2471
|
-
cmd_name_node = next((c for c in node.children if c.type == "command_name"), None)
|
|
2472
|
-
if cmd_name_node:
|
|
2473
|
-
cmd_text = _read_text(cmd_name_node, source).lower()
|
|
2474
|
-
if cmd_text == "using":
|
|
2475
|
-
tokens = []
|
|
2476
|
-
for child in node.children:
|
|
2477
|
-
if child.type == "command_elements":
|
|
2478
|
-
for el in child.children:
|
|
2479
|
-
if el.type == "generic_token":
|
|
2480
|
-
tokens.append(_read_text(el, source))
|
|
2481
|
-
module_tokens = [t for t in tokens
|
|
2482
|
-
if t.lower() not in ("namespace", "module", "assembly")]
|
|
2483
|
-
if module_tokens:
|
|
2484
|
-
module_name = module_tokens[-1].split(".")[-1]
|
|
2485
|
-
add_edge(file_nid, _make_id(module_name), "imports_from",
|
|
2486
|
-
node.start_point[0] + 1)
|
|
2487
|
-
return
|
|
2488
|
-
|
|
2489
|
-
for child in node.children:
|
|
2490
|
-
walk(child, parent_class_nid)
|
|
2491
|
-
|
|
2492
|
-
walk(root)
|
|
2493
|
-
|
|
2494
|
-
label_to_nid = {n["label"].strip("()").lstrip(".").lower(): n["id"] for n in nodes}
|
|
2495
|
-
seen_call_pairs: set[tuple[str, str]] = set()
|
|
2496
|
-
raw_calls: list[dict] = []
|
|
2497
|
-
|
|
2498
|
-
def walk_calls(node, caller_nid: str) -> None:
|
|
2499
|
-
if node.type in ("function_statement", "class_statement"):
|
|
2500
|
-
return
|
|
2501
|
-
if node.type == "command":
|
|
2502
|
-
cmd_name_node = next((c for c in node.children if c.type == "command_name"), None)
|
|
2503
|
-
if cmd_name_node:
|
|
2504
|
-
cmd_text = _read_text(cmd_name_node, source)
|
|
2505
|
-
if cmd_text.lower() not in _PS_SKIP:
|
|
2506
|
-
tgt_nid = label_to_nid.get(cmd_text.lower())
|
|
2507
|
-
if tgt_nid and tgt_nid != caller_nid:
|
|
2508
|
-
pair = (caller_nid, tgt_nid)
|
|
2509
|
-
if pair not in seen_call_pairs:
|
|
2510
|
-
seen_call_pairs.add(pair)
|
|
2511
|
-
add_edge(caller_nid, tgt_nid, "calls",
|
|
2512
|
-
node.start_point[0] + 1,
|
|
2513
|
-
confidence="EXTRACTED", weight=1.0)
|
|
2514
|
-
elif cmd_text:
|
|
2515
|
-
raw_calls.append({
|
|
2516
|
-
"caller_nid": caller_nid,
|
|
2517
|
-
"callee": cmd_text,
|
|
2518
|
-
"source_file": str_path,
|
|
2519
|
-
"source_location": f"L{node.start_point[0] + 1}",
|
|
2520
|
-
})
|
|
2521
|
-
for child in node.children:
|
|
2522
|
-
walk_calls(child, caller_nid)
|
|
2523
|
-
|
|
2524
|
-
for caller_nid, body_node in function_bodies:
|
|
2525
|
-
walk_calls(body_node, caller_nid)
|
|
2526
|
-
|
|
2527
|
-
clean_edges = [e for e in edges if e["source"] in seen_ids and
|
|
2528
|
-
(e["target"] in seen_ids or e["relation"] == "imports_from")]
|
|
2529
|
-
return {"nodes": nodes, "edges": clean_edges, "raw_calls": raw_calls}
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
# ── Cross-file import resolution ──────────────────────────────────────────────
|
|
2533
|
-
|
|
2534
|
-
def _resolve_cross_file_imports(
|
|
2535
|
-
per_file: list[dict],
|
|
2536
|
-
paths: list[Path],
|
|
2537
|
-
) -> list[dict]:
|
|
2538
|
-
"""
|
|
2539
|
-
Two-pass import resolution: turn file-level imports into class-level edges.
|
|
2540
|
-
|
|
2541
|
-
Pass 1 - build a global map: class/function name → node_id, per stem.
|
|
2542
|
-
Pass 2 - for each `from .module import Name`, look up Name in the global
|
|
2543
|
-
map and add a direct INFERRED edge from each class in the
|
|
2544
|
-
importing file to the imported entity.
|
|
2545
|
-
|
|
2546
|
-
This turns:
|
|
2547
|
-
auth.py --imports_from--> models.py (obvious, filtered out)
|
|
2548
|
-
Into:
|
|
2549
|
-
DigestAuth --uses--> Response [INFERRED] (cross-file, interesting!)
|
|
2550
|
-
BasicAuth --uses--> Request [INFERRED]
|
|
2551
|
-
"""
|
|
2552
|
-
try:
|
|
2553
|
-
import tree_sitter_python as tspython
|
|
2554
|
-
from tree_sitter import Language, Parser
|
|
2555
|
-
except ImportError:
|
|
2556
|
-
return []
|
|
2557
|
-
|
|
2558
|
-
language = Language(tspython.language())
|
|
2559
|
-
parser = Parser(language)
|
|
2560
|
-
|
|
2561
|
-
# Pass 1: name → node_id across all files
|
|
2562
|
-
# Map: stem → {ClassName: node_id}
|
|
2563
|
-
stem_to_entities: dict[str, dict[str, str]] = {}
|
|
2564
|
-
for file_result in per_file:
|
|
2565
|
-
for node in file_result.get("nodes", []):
|
|
2566
|
-
src = node.get("source_file", "")
|
|
2567
|
-
if not src:
|
|
2568
|
-
continue
|
|
2569
|
-
stem = Path(src).stem
|
|
2570
|
-
label = node.get("label", "")
|
|
2571
|
-
nid = node.get("id", "")
|
|
2572
|
-
# Only index real classes/functions (not file nodes, not method stubs)
|
|
2573
|
-
if label and not label.endswith((")", ".py")) and "_" not in label[:1]:
|
|
2574
|
-
stem_to_entities.setdefault(stem, {})[label] = nid
|
|
2575
|
-
|
|
2576
|
-
# Pass 2: for each file, find `from .X import A, B, C` and resolve
|
|
2577
|
-
new_edges: list[dict] = []
|
|
2578
|
-
stem_to_path: dict[str, Path] = {p.stem: p for p in paths}
|
|
2579
|
-
|
|
2580
|
-
for file_result, path in zip(per_file, paths):
|
|
2581
|
-
stem = path.stem
|
|
2582
|
-
str_path = str(path)
|
|
2583
|
-
|
|
2584
|
-
# Find all classes defined in this file (the importers)
|
|
2585
|
-
local_classes = [
|
|
2586
|
-
n["id"] for n in file_result.get("nodes", [])
|
|
2587
|
-
if n.get("source_file") == str_path
|
|
2588
|
-
and not n["label"].endswith((")", ".py"))
|
|
2589
|
-
and n["id"] != _make_id(stem) # exclude file-level node
|
|
2590
|
-
]
|
|
2591
|
-
if not local_classes:
|
|
2592
|
-
continue
|
|
2593
|
-
|
|
2594
|
-
# Parse imports from this file
|
|
2595
|
-
try:
|
|
2596
|
-
source = path.read_bytes()
|
|
2597
|
-
tree = parser.parse(source)
|
|
2598
|
-
except Exception:
|
|
2599
|
-
continue
|
|
2600
|
-
|
|
2601
|
-
def walk_imports(node) -> None:
|
|
2602
|
-
if node.type == "import_from_statement":
|
|
2603
|
-
# Find the module name - handles both absolute and relative imports.
|
|
2604
|
-
# Relative: `from .models import X` → relative_import → dotted_name
|
|
2605
|
-
# Absolute: `from models import X` → module_name field
|
|
2606
|
-
target_stem: str | None = None
|
|
2607
|
-
for child in node.children:
|
|
2608
|
-
if child.type == "relative_import":
|
|
2609
|
-
# Dig into relative_import → dotted_name → identifier
|
|
2610
|
-
for sub in child.children:
|
|
2611
|
-
if sub.type == "dotted_name":
|
|
2612
|
-
raw = source[sub.start_byte:sub.end_byte].decode("utf-8", errors="replace")
|
|
2613
|
-
target_stem = raw.split(".")[-1]
|
|
2614
|
-
break
|
|
2615
|
-
break
|
|
2616
|
-
if child.type == "dotted_name" and target_stem is None:
|
|
2617
|
-
raw = source[child.start_byte:child.end_byte].decode("utf-8", errors="replace")
|
|
2618
|
-
target_stem = raw.split(".")[-1]
|
|
2619
|
-
|
|
2620
|
-
if not target_stem or target_stem not in stem_to_entities:
|
|
2621
|
-
return
|
|
2622
|
-
|
|
2623
|
-
# Collect imported names: dotted_name children of import_from_statement
|
|
2624
|
-
# that come AFTER the 'import' keyword token.
|
|
2625
|
-
imported_names: list[str] = []
|
|
2626
|
-
past_import_kw = False
|
|
2627
|
-
for child in node.children:
|
|
2628
|
-
if child.type == "import":
|
|
2629
|
-
past_import_kw = True
|
|
2630
|
-
continue
|
|
2631
|
-
if not past_import_kw:
|
|
2632
|
-
continue
|
|
2633
|
-
if child.type == "dotted_name":
|
|
2634
|
-
imported_names.append(
|
|
2635
|
-
source[child.start_byte:child.end_byte].decode("utf-8", errors="replace")
|
|
2636
|
-
)
|
|
2637
|
-
elif child.type == "aliased_import":
|
|
2638
|
-
# `import X as Y` - take the original name
|
|
2639
|
-
name_node = child.child_by_field_name("name")
|
|
2640
|
-
if name_node:
|
|
2641
|
-
imported_names.append(
|
|
2642
|
-
source[name_node.start_byte:name_node.end_byte].decode("utf-8", errors="replace")
|
|
2643
|
-
)
|
|
2644
|
-
|
|
2645
|
-
line = node.start_point[0] + 1
|
|
2646
|
-
for name in imported_names:
|
|
2647
|
-
tgt_nid = stem_to_entities[target_stem].get(name)
|
|
2648
|
-
if tgt_nid:
|
|
2649
|
-
for src_class_nid in local_classes:
|
|
2650
|
-
new_edges.append({
|
|
2651
|
-
"source": src_class_nid,
|
|
2652
|
-
"target": tgt_nid,
|
|
2653
|
-
"relation": "uses",
|
|
2654
|
-
"confidence": "INFERRED",
|
|
2655
|
-
"source_file": str_path,
|
|
2656
|
-
"source_location": f"L{line}",
|
|
2657
|
-
"weight": 0.8,
|
|
2658
|
-
})
|
|
2659
|
-
for child in node.children:
|
|
2660
|
-
walk_imports(child)
|
|
2661
|
-
|
|
2662
|
-
walk_imports(tree.root_node)
|
|
2663
|
-
|
|
2664
|
-
return new_edges
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
def extract_objc(path: Path) -> dict:
|
|
2668
|
-
"""Extract interfaces, implementations, protocols, methods, and imports from .m/.mm/.h files."""
|
|
2669
|
-
try:
|
|
2670
|
-
import tree_sitter_objc as tsobjc
|
|
2671
|
-
from tree_sitter import Language, Parser
|
|
2672
|
-
except ImportError:
|
|
2673
|
-
return {"nodes": [], "edges": [], "error": "tree_sitter_objc not installed"}
|
|
2674
|
-
|
|
2675
|
-
try:
|
|
2676
|
-
language = Language(tsobjc.language())
|
|
2677
|
-
parser = Parser(language)
|
|
2678
|
-
source = path.read_bytes()
|
|
2679
|
-
tree = parser.parse(source)
|
|
2680
|
-
root = tree.root_node
|
|
2681
|
-
except Exception as e:
|
|
2682
|
-
return {"nodes": [], "edges": [], "error": str(e)}
|
|
2683
|
-
|
|
2684
|
-
stem = path.stem
|
|
2685
|
-
str_path = str(path)
|
|
2686
|
-
nodes: list[dict] = []
|
|
2687
|
-
edges: list[dict] = []
|
|
2688
|
-
seen_ids: set[str] = set()
|
|
2689
|
-
method_bodies: list[tuple[str, Any]] = []
|
|
2690
|
-
|
|
2691
|
-
def add_node(nid: str, label: str, line: int) -> None:
|
|
2692
|
-
if nid not in seen_ids:
|
|
2693
|
-
seen_ids.add(nid)
|
|
2694
|
-
nodes.append({"id": nid, "label": label, "file_type": "code",
|
|
2695
|
-
"source_file": str_path, "source_location": f"L{line}"})
|
|
2696
|
-
|
|
2697
|
-
def add_edge(src: str, tgt: str, relation: str, line: int,
|
|
2698
|
-
confidence: str = "EXTRACTED", weight: float = 1.0) -> None:
|
|
2699
|
-
edges.append({"source": src, "target": tgt, "relation": relation,
|
|
2700
|
-
"confidence": confidence, "source_file": str_path,
|
|
2701
|
-
"source_location": f"L{line}", "weight": weight})
|
|
2702
|
-
|
|
2703
|
-
file_nid = _make_id(str(path))
|
|
2704
|
-
add_node(file_nid, path.name, 1)
|
|
2705
|
-
|
|
2706
|
-
def _read(node) -> str:
|
|
2707
|
-
return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
|
|
2708
|
-
|
|
2709
|
-
def _get_name(node, field: str) -> str | None:
|
|
2710
|
-
n = node.child_by_field_name(field)
|
|
2711
|
-
return _read(n) if n else None
|
|
2712
|
-
|
|
2713
|
-
def walk(node, parent_nid: str | None = None) -> None:
|
|
2714
|
-
t = node.type
|
|
2715
|
-
line = node.start_point[0] + 1
|
|
2716
|
-
|
|
2717
|
-
if t == "preproc_include":
|
|
2718
|
-
# #import <Foundation/Foundation.h> or #import "MyClass.h"
|
|
2719
|
-
for child in node.children:
|
|
2720
|
-
if child.type == "system_lib_string":
|
|
2721
|
-
raw = _read(child).strip("<>")
|
|
2722
|
-
module = raw.split("/")[-1].replace(".h", "")
|
|
2723
|
-
if module:
|
|
2724
|
-
tgt_nid = _make_id(module)
|
|
2725
|
-
add_edge(file_nid, tgt_nid, "imports", line)
|
|
2726
|
-
elif child.type == "string_literal":
|
|
2727
|
-
# recurse into string_literal to find string_content
|
|
2728
|
-
for sub in child.children:
|
|
2729
|
-
if sub.type == "string_content":
|
|
2730
|
-
raw = _read(sub)
|
|
2731
|
-
module = raw.split("/")[-1].replace(".h", "")
|
|
2732
|
-
if module:
|
|
2733
|
-
tgt_nid = _make_id(module)
|
|
2734
|
-
add_edge(file_nid, tgt_nid, "imports", line)
|
|
2735
|
-
return
|
|
2736
|
-
|
|
2737
|
-
if t == "class_interface":
|
|
2738
|
-
# @interface ClassName : SuperClass <Protocols>
|
|
2739
|
-
# children: @interface, identifier(name), ':', identifier(super), parameterized_arguments, ...
|
|
2740
|
-
identifiers = [c for c in node.children if c.type == "identifier"]
|
|
2741
|
-
if not identifiers:
|
|
2742
|
-
for child in node.children:
|
|
2743
|
-
walk(child, parent_nid)
|
|
2744
|
-
return
|
|
2745
|
-
name = _read(identifiers[0])
|
|
2746
|
-
cls_nid = _make_id(stem, name)
|
|
2747
|
-
add_node(cls_nid, name, line)
|
|
2748
|
-
add_edge(file_nid, cls_nid, "contains", line)
|
|
2749
|
-
# superclass is second identifier after ':'
|
|
2750
|
-
colon_seen = False
|
|
2751
|
-
for child in node.children:
|
|
2752
|
-
if child.type == ":":
|
|
2753
|
-
colon_seen = True
|
|
2754
|
-
elif colon_seen and child.type == "identifier":
|
|
2755
|
-
super_nid = _make_id(_read(child))
|
|
2756
|
-
add_edge(cls_nid, super_nid, "inherits", line)
|
|
2757
|
-
colon_seen = False
|
|
2758
|
-
elif child.type == "parameterized_arguments":
|
|
2759
|
-
# protocols adopted
|
|
2760
|
-
for sub in child.children:
|
|
2761
|
-
if sub.type == "type_name":
|
|
2762
|
-
for s in sub.children:
|
|
2763
|
-
if s.type == "type_identifier":
|
|
2764
|
-
proto_nid = _make_id(_read(s))
|
|
2765
|
-
add_edge(cls_nid, proto_nid, "imports", line)
|
|
2766
|
-
elif child.type == "method_declaration":
|
|
2767
|
-
walk(child, cls_nid)
|
|
2768
|
-
return
|
|
2769
|
-
|
|
2770
|
-
if t == "class_implementation":
|
|
2771
|
-
# @implementation ClassName
|
|
2772
|
-
name = None
|
|
2773
|
-
for child in node.children:
|
|
2774
|
-
if child.type == "identifier":
|
|
2775
|
-
name = _read(child)
|
|
2776
|
-
break
|
|
2777
|
-
if not name:
|
|
2778
|
-
for child in node.children:
|
|
2779
|
-
walk(child, parent_nid)
|
|
2780
|
-
return
|
|
2781
|
-
impl_nid = _make_id(stem, name)
|
|
2782
|
-
if impl_nid not in seen_ids:
|
|
2783
|
-
add_node(impl_nid, name, line)
|
|
2784
|
-
add_edge(file_nid, impl_nid, "contains", line)
|
|
2785
|
-
for child in node.children:
|
|
2786
|
-
if child.type == "implementation_definition":
|
|
2787
|
-
for sub in child.children:
|
|
2788
|
-
walk(sub, impl_nid)
|
|
2789
|
-
return
|
|
2790
|
-
|
|
2791
|
-
if t == "protocol_declaration":
|
|
2792
|
-
name = None
|
|
2793
|
-
for child in node.children:
|
|
2794
|
-
if child.type == "identifier":
|
|
2795
|
-
name = _read(child)
|
|
2796
|
-
break
|
|
2797
|
-
if name:
|
|
2798
|
-
proto_nid = _make_id(stem, name)
|
|
2799
|
-
add_node(proto_nid, f"<{name}>", line)
|
|
2800
|
-
add_edge(file_nid, proto_nid, "contains", line)
|
|
2801
|
-
for child in node.children:
|
|
2802
|
-
walk(child, proto_nid)
|
|
2803
|
-
return
|
|
2804
|
-
|
|
2805
|
-
if t in ("method_declaration", "method_definition"):
|
|
2806
|
-
container = parent_nid or file_nid
|
|
2807
|
-
# method name is the first identifier child (simple selector)
|
|
2808
|
-
# for compound selectors: identifier + method_parameter pairs
|
|
2809
|
-
parts = []
|
|
2810
|
-
for child in node.children:
|
|
2811
|
-
if child.type == "identifier":
|
|
2812
|
-
parts.append(_read(child))
|
|
2813
|
-
elif child.type == "method_parameter":
|
|
2814
|
-
for sub in child.children:
|
|
2815
|
-
if sub.type == "identifier":
|
|
2816
|
-
# selector keyword before ':'
|
|
2817
|
-
pass
|
|
2818
|
-
method_name = "".join(parts) if parts else None
|
|
2819
|
-
if method_name:
|
|
2820
|
-
method_nid = _make_id(container, method_name)
|
|
2821
|
-
add_node(method_nid, f"-{method_name}", line)
|
|
2822
|
-
add_edge(container, method_nid, "method", line)
|
|
2823
|
-
if t == "method_definition":
|
|
2824
|
-
method_bodies.append((method_nid, node))
|
|
2825
|
-
return
|
|
2826
|
-
|
|
2827
|
-
for child in node.children:
|
|
2828
|
-
walk(child, parent_nid)
|
|
2829
|
-
|
|
2830
|
-
walk(root)
|
|
2831
|
-
|
|
2832
|
-
# Second pass: resolve calls inside method bodies
|
|
2833
|
-
all_method_nids = {n["id"] for n in nodes if n["id"] != file_nid}
|
|
2834
|
-
seen_calls: set[tuple[str, str]] = set()
|
|
2835
|
-
for caller_nid, body_node in method_bodies:
|
|
2836
|
-
def walk_calls(n) -> None:
|
|
2837
|
-
if n.type == "message_expression":
|
|
2838
|
-
# [receiver selector]
|
|
2839
|
-
for child in n.children:
|
|
2840
|
-
if child.type in ("selector", "keyword_argument_list"):
|
|
2841
|
-
sel = []
|
|
2842
|
-
if child.type == "selector":
|
|
2843
|
-
sel.append(_read(child))
|
|
2844
|
-
else:
|
|
2845
|
-
for sub in child.children:
|
|
2846
|
-
if sub.type == "keyword_argument":
|
|
2847
|
-
for s in sub.children:
|
|
2848
|
-
if s.type == "selector":
|
|
2849
|
-
sel.append(_read(s))
|
|
2850
|
-
method_name = "".join(sel)
|
|
2851
|
-
for candidate in all_method_nids:
|
|
2852
|
-
if candidate.endswith(_make_id("", method_name).lstrip("_")):
|
|
2853
|
-
pair = (caller_nid, candidate)
|
|
2854
|
-
if pair not in seen_calls and caller_nid != candidate:
|
|
2855
|
-
seen_calls.add(pair)
|
|
2856
|
-
add_edge(caller_nid, candidate, "calls", body_node.start_point[0] + 1,
|
|
2857
|
-
confidence="EXTRACTED", weight=1.0)
|
|
2858
|
-
for child in n.children:
|
|
2859
|
-
walk_calls(child)
|
|
2860
|
-
walk_calls(body_node)
|
|
2861
|
-
|
|
2862
|
-
return {"nodes": nodes, "edges": edges, "input_tokens": 0, "output_tokens": 0}
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
def extract_elixir(path: Path) -> dict:
|
|
2866
|
-
"""Extract modules, functions, imports, and calls from a .ex/.exs file."""
|
|
2867
|
-
try:
|
|
2868
|
-
import tree_sitter_elixir as tselixir
|
|
2869
|
-
from tree_sitter import Language, Parser
|
|
2870
|
-
except ImportError:
|
|
2871
|
-
return {"nodes": [], "edges": [], "error": "tree_sitter_elixir not installed"}
|
|
2872
|
-
|
|
2873
|
-
try:
|
|
2874
|
-
language = Language(tselixir.language())
|
|
2875
|
-
parser = Parser(language)
|
|
2876
|
-
source = path.read_bytes()
|
|
2877
|
-
tree = parser.parse(source)
|
|
2878
|
-
root = tree.root_node
|
|
2879
|
-
except Exception as e:
|
|
2880
|
-
return {"nodes": [], "edges": [], "error": str(e)}
|
|
2881
|
-
|
|
2882
|
-
stem = path.stem
|
|
2883
|
-
str_path = str(path)
|
|
2884
|
-
nodes: list[dict] = []
|
|
2885
|
-
edges: list[dict] = []
|
|
2886
|
-
seen_ids: set[str] = set()
|
|
2887
|
-
function_bodies: list[tuple[str, Any]] = []
|
|
2888
|
-
|
|
2889
|
-
def add_node(nid: str, label: str, line: int) -> None:
|
|
2890
|
-
if nid not in seen_ids:
|
|
2891
|
-
seen_ids.add(nid)
|
|
2892
|
-
nodes.append({"id": nid, "label": label, "file_type": "code",
|
|
2893
|
-
"source_file": str_path, "source_location": f"L{line}"})
|
|
2894
|
-
|
|
2895
|
-
def add_edge(src: str, tgt: str, relation: str, line: int,
|
|
2896
|
-
confidence: str = "EXTRACTED", weight: float = 1.0) -> None:
|
|
2897
|
-
edges.append({"source": src, "target": tgt, "relation": relation,
|
|
2898
|
-
"confidence": confidence, "source_file": str_path,
|
|
2899
|
-
"source_location": f"L{line}", "weight": weight})
|
|
2900
|
-
|
|
2901
|
-
file_nid = _make_id(str(path))
|
|
2902
|
-
add_node(file_nid, path.name, 1)
|
|
2903
|
-
|
|
2904
|
-
_IMPORT_KEYWORDS = frozenset({"alias", "import", "require", "use"})
|
|
2905
|
-
|
|
2906
|
-
def _get_alias_text(node) -> str | None:
|
|
2907
|
-
for child in node.children:
|
|
2908
|
-
if child.type == "alias":
|
|
2909
|
-
return source[child.start_byte:child.end_byte].decode("utf-8", errors="replace")
|
|
2910
|
-
return None
|
|
2911
|
-
|
|
2912
|
-
def walk(node, parent_module_nid: str | None = None) -> None:
|
|
2913
|
-
if node.type != "call":
|
|
2914
|
-
for child in node.children:
|
|
2915
|
-
walk(child, parent_module_nid)
|
|
2916
|
-
return
|
|
2917
|
-
|
|
2918
|
-
identifier_node = None
|
|
2919
|
-
arguments_node = None
|
|
2920
|
-
do_block_node = None
|
|
2921
|
-
for child in node.children:
|
|
2922
|
-
if child.type == "identifier":
|
|
2923
|
-
identifier_node = child
|
|
2924
|
-
elif child.type == "arguments":
|
|
2925
|
-
arguments_node = child
|
|
2926
|
-
elif child.type == "do_block":
|
|
2927
|
-
do_block_node = child
|
|
2928
|
-
|
|
2929
|
-
if identifier_node is None:
|
|
2930
|
-
for child in node.children:
|
|
2931
|
-
walk(child, parent_module_nid)
|
|
2932
|
-
return
|
|
2933
|
-
|
|
2934
|
-
keyword = source[identifier_node.start_byte:identifier_node.end_byte].decode("utf-8", errors="replace")
|
|
2935
|
-
line = node.start_point[0] + 1
|
|
2936
|
-
|
|
2937
|
-
if keyword == "defmodule":
|
|
2938
|
-
module_name = _get_alias_text(arguments_node) if arguments_node else None
|
|
2939
|
-
if not module_name:
|
|
2940
|
-
return
|
|
2941
|
-
module_nid = _make_id(stem, module_name)
|
|
2942
|
-
add_node(module_nid, module_name, line)
|
|
2943
|
-
add_edge(file_nid, module_nid, "contains", line)
|
|
2944
|
-
if do_block_node:
|
|
2945
|
-
for child in do_block_node.children:
|
|
2946
|
-
walk(child, parent_module_nid=module_nid)
|
|
2947
|
-
return
|
|
2948
|
-
|
|
2949
|
-
if keyword in ("def", "defp"):
|
|
2950
|
-
func_name = None
|
|
2951
|
-
if arguments_node:
|
|
2952
|
-
for child in arguments_node.children:
|
|
2953
|
-
if child.type == "call":
|
|
2954
|
-
for sub in child.children:
|
|
2955
|
-
if sub.type == "identifier":
|
|
2956
|
-
func_name = source[sub.start_byte:sub.end_byte].decode("utf-8", errors="replace")
|
|
2957
|
-
break
|
|
2958
|
-
elif child.type == "identifier":
|
|
2959
|
-
func_name = source[child.start_byte:child.end_byte].decode("utf-8", errors="replace")
|
|
2960
|
-
break
|
|
2961
|
-
if not func_name:
|
|
2962
|
-
return
|
|
2963
|
-
container = parent_module_nid or file_nid
|
|
2964
|
-
func_nid = _make_id(container, func_name)
|
|
2965
|
-
add_node(func_nid, f"{func_name}()", line)
|
|
2966
|
-
if parent_module_nid:
|
|
2967
|
-
add_edge(parent_module_nid, func_nid, "method", line)
|
|
2968
|
-
else:
|
|
2969
|
-
add_edge(file_nid, func_nid, "contains", line)
|
|
2970
|
-
if do_block_node:
|
|
2971
|
-
function_bodies.append((func_nid, do_block_node))
|
|
2972
|
-
return
|
|
2973
|
-
|
|
2974
|
-
if keyword in _IMPORT_KEYWORDS and arguments_node:
|
|
2975
|
-
module_name = _get_alias_text(arguments_node)
|
|
2976
|
-
if module_name:
|
|
2977
|
-
tgt_nid = _make_id(module_name)
|
|
2978
|
-
add_edge(file_nid, tgt_nid, "imports", line)
|
|
2979
|
-
return
|
|
2980
|
-
|
|
2981
|
-
for child in node.children:
|
|
2982
|
-
walk(child, parent_module_nid)
|
|
2983
|
-
|
|
2984
|
-
walk(root)
|
|
2985
|
-
|
|
2986
|
-
label_to_nid: dict[str, str] = {}
|
|
2987
|
-
for n in nodes:
|
|
2988
|
-
normalised = n["label"].strip("()").lstrip(".")
|
|
2989
|
-
label_to_nid[normalised.lower()] = n["id"]
|
|
2990
|
-
|
|
2991
|
-
seen_call_pairs: set[tuple[str, str]] = set()
|
|
2992
|
-
raw_calls: list[dict] = []
|
|
2993
|
-
_SKIP_KEYWORDS = frozenset({
|
|
2994
|
-
"def", "defp", "defmodule", "defmacro", "defmacrop",
|
|
2995
|
-
"defstruct", "defprotocol", "defimpl", "defguard",
|
|
2996
|
-
"alias", "import", "require", "use",
|
|
2997
|
-
"if", "unless", "case", "cond", "with", "for",
|
|
2998
|
-
})
|
|
2999
|
-
|
|
3000
|
-
def walk_calls(node, caller_nid: str) -> None:
|
|
3001
|
-
if node.type != "call":
|
|
3002
|
-
for child in node.children:
|
|
3003
|
-
walk_calls(child, caller_nid)
|
|
3004
|
-
return
|
|
3005
|
-
for child in node.children:
|
|
3006
|
-
if child.type == "identifier":
|
|
3007
|
-
kw = source[child.start_byte:child.end_byte].decode("utf-8", errors="replace")
|
|
3008
|
-
if kw in _SKIP_KEYWORDS:
|
|
3009
|
-
for c in node.children:
|
|
3010
|
-
walk_calls(c, caller_nid)
|
|
3011
|
-
return
|
|
3012
|
-
break
|
|
3013
|
-
callee_name: str | None = None
|
|
3014
|
-
for child in node.children:
|
|
3015
|
-
if child.type == "dot":
|
|
3016
|
-
dot_text = source[child.start_byte:child.end_byte].decode("utf-8", errors="replace")
|
|
3017
|
-
parts = dot_text.rstrip(".").split(".")
|
|
3018
|
-
if parts:
|
|
3019
|
-
callee_name = parts[-1]
|
|
3020
|
-
break
|
|
3021
|
-
if child.type == "identifier":
|
|
3022
|
-
callee_name = source[child.start_byte:child.end_byte].decode("utf-8", errors="replace")
|
|
3023
|
-
break
|
|
3024
|
-
if callee_name:
|
|
3025
|
-
tgt_nid = label_to_nid.get(callee_name.lower())
|
|
3026
|
-
if tgt_nid and tgt_nid != caller_nid:
|
|
3027
|
-
pair = (caller_nid, tgt_nid)
|
|
3028
|
-
if pair not in seen_call_pairs:
|
|
3029
|
-
seen_call_pairs.add(pair)
|
|
3030
|
-
add_edge(caller_nid, tgt_nid, "calls",
|
|
3031
|
-
node.start_point[0] + 1, confidence="EXTRACTED", weight=1.0)
|
|
3032
|
-
else:
|
|
3033
|
-
raw_calls.append({
|
|
3034
|
-
"caller_nid": caller_nid,
|
|
3035
|
-
"callee": callee_name,
|
|
3036
|
-
"source_file": str_path,
|
|
3037
|
-
"source_location": f"L{node.start_point[0] + 1}",
|
|
3038
|
-
})
|
|
3039
|
-
for child in node.children:
|
|
3040
|
-
walk_calls(child, caller_nid)
|
|
3041
|
-
|
|
3042
|
-
for caller_nid, body in function_bodies:
|
|
3043
|
-
walk_calls(body, caller_nid)
|
|
3044
|
-
|
|
3045
|
-
clean_edges = [e for e in edges if e["source"] in seen_ids and
|
|
3046
|
-
(e["target"] in seen_ids or e["relation"] == "imports")]
|
|
3047
|
-
return {"nodes": nodes, "edges": clean_edges, "raw_calls": raw_calls, "input_tokens": 0, "output_tokens": 0}
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
# ── Main extract and collect_files ────────────────────────────────────────────
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
def _check_tree_sitter_version() -> None:
|
|
3054
|
-
"""Raise a clear error if tree-sitter is too old for the new Language API."""
|
|
3055
|
-
try:
|
|
3056
|
-
from tree_sitter import LANGUAGE_VERSION
|
|
3057
|
-
except ImportError:
|
|
3058
|
-
raise ImportError(
|
|
3059
|
-
"tree-sitter is not installed. Run: pip install 'tree-sitter>=0.23.0'"
|
|
3060
|
-
)
|
|
3061
|
-
# Language API v2 starts at LANGUAGE_VERSION 14
|
|
3062
|
-
if LANGUAGE_VERSION < 14:
|
|
3063
|
-
import tree_sitter as _ts
|
|
3064
|
-
raise RuntimeError(
|
|
3065
|
-
f"tree-sitter {getattr(_ts, '__version__', 'unknown')} is too old. "
|
|
3066
|
-
f"graphify requires tree-sitter >= 0.23.0 (Language API v2). "
|
|
3067
|
-
f"Run: pip install --upgrade tree-sitter"
|
|
3068
|
-
)
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
def extract(paths: list[Path], cache_root: Path | None = None) -> dict:
|
|
3072
|
-
"""Extract AST nodes and edges from a list of code files.
|
|
3073
|
-
|
|
3074
|
-
Two-pass process:
|
|
3075
|
-
1. Per-file structural extraction (classes, functions, imports)
|
|
3076
|
-
2. Cross-file import resolution: turns file-level imports into
|
|
3077
|
-
class-level INFERRED edges (DigestAuth --uses--> Response)
|
|
3078
|
-
|
|
3079
|
-
Args:
|
|
3080
|
-
paths: files to extract from
|
|
3081
|
-
cache_root: explicit root for graphify-out/cache/ (overrides the
|
|
3082
|
-
inferred common path prefix). Pass Path('.') when running on a
|
|
3083
|
-
subdirectory so the cache stays at ./graphify-out/cache/.
|
|
3084
|
-
"""
|
|
3085
|
-
_check_tree_sitter_version()
|
|
3086
|
-
per_file: list[dict] = []
|
|
3087
|
-
|
|
3088
|
-
# Infer a common root for cache keys
|
|
3089
|
-
try:
|
|
3090
|
-
if not paths:
|
|
3091
|
-
root = Path(".")
|
|
3092
|
-
elif len(paths) == 1:
|
|
3093
|
-
root = paths[0].parent
|
|
3094
|
-
else:
|
|
3095
|
-
common_len = sum(
|
|
3096
|
-
1 for i in range(min(len(p.parts) for p in paths))
|
|
3097
|
-
if len({p.parts[i] for p in paths}) == 1
|
|
3098
|
-
)
|
|
3099
|
-
root = Path(*paths[0].parts[:common_len]) if common_len else Path(".")
|
|
3100
|
-
except Exception:
|
|
3101
|
-
root = Path(".")
|
|
3102
|
-
|
|
3103
|
-
_DISPATCH: dict[str, Any] = {
|
|
3104
|
-
".py": extract_python,
|
|
3105
|
-
".js": extract_js,
|
|
3106
|
-
".jsx": extract_js,
|
|
3107
|
-
".mjs": extract_js,
|
|
3108
|
-
".ts": extract_js,
|
|
3109
|
-
".tsx": extract_js,
|
|
3110
|
-
".go": extract_go,
|
|
3111
|
-
".rs": extract_rust,
|
|
3112
|
-
".java": extract_java,
|
|
3113
|
-
".c": extract_c,
|
|
3114
|
-
".h": extract_c,
|
|
3115
|
-
".cpp": extract_cpp,
|
|
3116
|
-
".cc": extract_cpp,
|
|
3117
|
-
".cxx": extract_cpp,
|
|
3118
|
-
".hpp": extract_cpp,
|
|
3119
|
-
".rb": extract_ruby,
|
|
3120
|
-
".cs": extract_csharp,
|
|
3121
|
-
".kt": extract_kotlin,
|
|
3122
|
-
".kts": extract_kotlin,
|
|
3123
|
-
".scala": extract_scala,
|
|
3124
|
-
".php": extract_php,
|
|
3125
|
-
".swift": extract_swift,
|
|
3126
|
-
".lua": extract_lua,
|
|
3127
|
-
".toc": extract_lua,
|
|
3128
|
-
".zig": extract_zig,
|
|
3129
|
-
".ps1": extract_powershell,
|
|
3130
|
-
".ex": extract_elixir,
|
|
3131
|
-
".exs": extract_elixir,
|
|
3132
|
-
".m": extract_objc,
|
|
3133
|
-
".mm": extract_objc,
|
|
3134
|
-
".jl": extract_julia,
|
|
3135
|
-
".vue": extract_js,
|
|
3136
|
-
".svelte": extract_js,
|
|
3137
|
-
".dart": extract_dart,
|
|
3138
|
-
".v": extract_verilog,
|
|
3139
|
-
".sv": extract_verilog,
|
|
3140
|
-
}
|
|
3141
|
-
|
|
3142
|
-
total = len(paths)
|
|
3143
|
-
_PROGRESS_INTERVAL = 100
|
|
3144
|
-
for i, path in enumerate(paths):
|
|
3145
|
-
if total >= _PROGRESS_INTERVAL and i % _PROGRESS_INTERVAL == 0 and i > 0:
|
|
3146
|
-
print(f" AST extraction: {i}/{total} files ({i * 100 // total}%)", flush=True)
|
|
3147
|
-
# .blade.php must be checked before suffix lookup since Path.suffix returns .php
|
|
3148
|
-
if path.name.endswith(".blade.php"):
|
|
3149
|
-
extractor = extract_blade
|
|
3150
|
-
else:
|
|
3151
|
-
extractor = _DISPATCH.get(path.suffix)
|
|
3152
|
-
if extractor is None:
|
|
3153
|
-
continue
|
|
3154
|
-
cached = load_cached(path, cache_root or root)
|
|
3155
|
-
if cached is not None:
|
|
3156
|
-
per_file.append(cached)
|
|
3157
|
-
continue
|
|
3158
|
-
result = extractor(path)
|
|
3159
|
-
if "error" not in result:
|
|
3160
|
-
save_cached(path, result, cache_root or root)
|
|
3161
|
-
per_file.append(result)
|
|
3162
|
-
if total >= _PROGRESS_INTERVAL:
|
|
3163
|
-
print(f" AST extraction: {total}/{total} files (100%)", flush=True)
|
|
3164
|
-
|
|
3165
|
-
all_nodes: list[dict] = []
|
|
3166
|
-
all_edges: list[dict] = []
|
|
3167
|
-
for result in per_file:
|
|
3168
|
-
all_nodes.extend(result.get("nodes", []))
|
|
3169
|
-
all_edges.extend(result.get("edges", []))
|
|
3170
|
-
|
|
3171
|
-
# Add cross-file class-level edges (Python only - uses Python parser internally)
|
|
3172
|
-
py_paths = [p for p in paths if p.suffix == ".py"]
|
|
3173
|
-
if py_paths:
|
|
3174
|
-
py_results = [r for r, p in zip(per_file, paths) if p.suffix == ".py"]
|
|
3175
|
-
try:
|
|
3176
|
-
cross_file_edges = _resolve_cross_file_imports(py_results, py_paths)
|
|
3177
|
-
all_edges.extend(cross_file_edges)
|
|
3178
|
-
except Exception as exc:
|
|
3179
|
-
import logging
|
|
3180
|
-
logging.getLogger(__name__).warning("Cross-file import resolution failed, skipping: %s", exc)
|
|
3181
|
-
|
|
3182
|
-
# Cross-file call resolution for all languages
|
|
3183
|
-
# Each extractor saved unresolved calls in raw_calls. Now that we have all
|
|
3184
|
-
# nodes from all files, resolve any callee that exists in another file.
|
|
3185
|
-
global_label_to_nid: dict[str, str] = {}
|
|
3186
|
-
for n in all_nodes:
|
|
3187
|
-
raw = n.get("label", "")
|
|
3188
|
-
normalised = raw.strip("()").lstrip(".")
|
|
3189
|
-
if normalised:
|
|
3190
|
-
global_label_to_nid[normalised.lower()] = n["id"]
|
|
3191
|
-
|
|
3192
|
-
existing_pairs = {(e["source"], e["target"]) for e in all_edges}
|
|
3193
|
-
for result in per_file:
|
|
3194
|
-
for rc in result.get("raw_calls", []):
|
|
3195
|
-
callee = rc.get("callee", "")
|
|
3196
|
-
if not callee:
|
|
3197
|
-
continue
|
|
3198
|
-
tgt = global_label_to_nid.get(callee.lower())
|
|
3199
|
-
caller = rc["caller_nid"]
|
|
3200
|
-
if tgt and tgt != caller and (caller, tgt) not in existing_pairs:
|
|
3201
|
-
existing_pairs.add((caller, tgt))
|
|
3202
|
-
all_edges.append({
|
|
3203
|
-
"source": caller,
|
|
3204
|
-
"target": tgt,
|
|
3205
|
-
"relation": "calls",
|
|
3206
|
-
"confidence": "INFERRED",
|
|
3207
|
-
"confidence_score": 0.8,
|
|
3208
|
-
"source_file": rc.get("source_file", ""),
|
|
3209
|
-
"source_location": rc.get("source_location"),
|
|
3210
|
-
"weight": 1.0,
|
|
3211
|
-
})
|
|
3212
|
-
|
|
3213
|
-
return {
|
|
3214
|
-
"nodes": all_nodes,
|
|
3215
|
-
"edges": all_edges,
|
|
3216
|
-
"input_tokens": 0,
|
|
3217
|
-
"output_tokens": 0,
|
|
3218
|
-
}
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
def collect_files(target: Path, *, follow_symlinks: bool = False, root: Path | None = None) -> list[Path]:
|
|
3222
|
-
if target.is_file():
|
|
3223
|
-
return [target]
|
|
3224
|
-
_EXTENSIONS = {
|
|
3225
|
-
".py", ".js", ".ts", ".tsx", ".go", ".rs",
|
|
3226
|
-
".java", ".c", ".h", ".cpp", ".cc", ".cxx", ".hpp",
|
|
3227
|
-
".rb", ".cs", ".kt", ".kts", ".scala", ".php", ".swift",
|
|
3228
|
-
".lua", ".toc", ".zig", ".ps1",
|
|
3229
|
-
".m", ".mm",
|
|
3230
|
-
}
|
|
3231
|
-
from graphify.detect import _load_graphifyignore, _is_ignored
|
|
3232
|
-
ignore_root = root if root is not None else target
|
|
3233
|
-
patterns = _load_graphifyignore(ignore_root)
|
|
3234
|
-
|
|
3235
|
-
def _ignored(p: Path) -> bool:
|
|
3236
|
-
return bool(patterns and _is_ignored(p, ignore_root, patterns))
|
|
3237
|
-
|
|
3238
|
-
if not follow_symlinks:
|
|
3239
|
-
results: list[Path] = []
|
|
3240
|
-
for ext in sorted(_EXTENSIONS):
|
|
3241
|
-
results.extend(
|
|
3242
|
-
p for p in target.rglob(f"*{ext}")
|
|
3243
|
-
if not any(part.startswith(".") for part in p.parts)
|
|
3244
|
-
and not _ignored(p)
|
|
3245
|
-
)
|
|
3246
|
-
return sorted(results)
|
|
3247
|
-
# Walk with symlink following + cycle detection
|
|
3248
|
-
results = []
|
|
3249
|
-
for dirpath, dirnames, filenames in os.walk(target, followlinks=True):
|
|
3250
|
-
if os.path.islink(dirpath):
|
|
3251
|
-
real = os.path.realpath(dirpath)
|
|
3252
|
-
parent_real = os.path.realpath(os.path.dirname(dirpath))
|
|
3253
|
-
if parent_real == real or parent_real.startswith(real + os.sep):
|
|
3254
|
-
dirnames.clear()
|
|
3255
|
-
continue
|
|
3256
|
-
dp = Path(dirpath)
|
|
3257
|
-
if any(part.startswith(".") for part in dp.parts):
|
|
3258
|
-
dirnames.clear()
|
|
3259
|
-
continue
|
|
3260
|
-
for fname in filenames:
|
|
3261
|
-
p = dp / fname
|
|
3262
|
-
if p.suffix in _EXTENSIONS and not fname.startswith(".") and not _ignored(p):
|
|
3263
|
-
results.append(p)
|
|
3264
|
-
return sorted(results)
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
if __name__ == "__main__":
|
|
3268
|
-
if len(sys.argv) < 2:
|
|
3269
|
-
print("Usage: python -m graphify.extract <file_or_dir> ...", file=sys.stderr)
|
|
3270
|
-
sys.exit(1)
|
|
3271
|
-
|
|
3272
|
-
paths: list[Path] = []
|
|
3273
|
-
for arg in sys.argv[1:]:
|
|
3274
|
-
paths.extend(collect_files(Path(arg)))
|
|
3275
|
-
|
|
3276
|
-
result = extract(paths)
|
|
3277
|
-
print(json.dumps(result, indent=2))
|