serpentine-parser 0.3.1__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/PKG-INFO +1 -1
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/pyproject.toml +1 -1
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/rust/Cargo.lock +22 -22
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/rust/Cargo.toml +4 -3
- serpentine_parser-0.3.2/rust/src/graph/builder.rs +441 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/rust/src/graph/loaders.rs +279 -396
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/rust/src/graph/mod.rs +4 -61
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/rust/src/graph/resolvers.rs +73 -69
- serpentine_parser-0.3.2/rust/src/lib.rs +704 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/rust/src/subscribers/mod.rs +7 -0
- serpentine_parser-0.3.2/rust/src/subscribers/types.rs +139 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/src/serpentine/__init__.py +1 -1
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/src/serpentine/_analyzer.pyi +4 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/src/serpentine/cache.py +83 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/src/serpentine/cli.py +2 -2
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/src/serpentine/server/routes.py +7 -0
- serpentine_parser-0.3.2/src/serpentine/skills/__init__.py +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/src/serpentine/state.py +65 -20
- serpentine_parser-0.3.2/src/serpentine/static/assets/elk-worker.min-BqREE3LP.js +6268 -0
- serpentine_parser-0.3.2/src/serpentine/static/assets/index-CqW3OZBs.css +1 -0
- serpentine_parser-0.3.2/src/serpentine/static/assets/index-DGBMc5dK.js +32 -0
- serpentine_parser-0.3.2/src/serpentine/static/assets/layoutWorker-DEFCbISa.js +24 -0
- serpentine_parser-0.3.2/src/serpentine/static/assets/mermaidExport-DphjeTrR.js +3 -0
- serpentine_parser-0.3.2/src/serpentine/static/assets/svgToPng-C-twkIuO.js +2 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/src/serpentine/static/index.html +2 -2
- serpentine_parser-0.3.1/rust/src/graph/builder.rs +0 -185
- serpentine_parser-0.3.1/rust/src/graph/pdg.rs +0 -94
- serpentine_parser-0.3.1/rust/src/lib.rs +0 -413
- serpentine_parser-0.3.1/src/serpentine/static/assets/elk-worker.min-BdOC9sib.js +0 -6263
- serpentine_parser-0.3.1/src/serpentine/static/assets/index-BPIIaXQj.js +0 -219
- serpentine_parser-0.3.1/src/serpentine/static/assets/index-DDwdNVWo.css +0 -1
- serpentine_parser-0.3.1/src/serpentine/static/assets/layoutWorker-BkzQvq4g.js +0 -24
- serpentine_parser-0.3.1/src/serpentine/static/assets/mermaidExport-B2JREVsY.js +0 -3
- serpentine_parser-0.3.1/src/serpentine/static/assets/svgToPng-CCU1Oj7k.js +0 -2
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/LICENSE +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/README.md +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/rust/CLAUDE.md +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/rust/src/events.rs +0 -0
- /serpentine_parser-0.3.1/src/serpentine/skills/__init__.py → /serpentine_parser-0.3.2/rust/src/graph/pdg.rs +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/rust/src/javascript/config.rs +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/rust/src/javascript/mod.rs +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/rust/src/message_bus.rs +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/rust/src/python/config.rs +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/rust/src/python/mod.rs +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/rust/src/rust_lang/config.rs +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/rust/src/rust_lang/mod.rs +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/rust/src/subscribers/README.md +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/rust/src/subscribers/code_snippet.rs +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/rust/src/subscribers/decorators.rs +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/rust/src/subscribers/definitions.rs +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/rust/src/subscribers/event_counter.rs +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/rust/src/subscribers/imports.rs +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/rust/src/subscribers/pdg.rs +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/rust/src/subscribers/raw_bindings.rs +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/rust/src/subscribers/scope_tree.rs +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/rust/src/subscribers/uses.rs +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/rust/src/terraform/config.rs +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/rust/src/terraform/mod.rs +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/src/serpentine/config.py +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/src/serpentine/selector.py +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/src/serpentine/server/__init__.py +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/src/serpentine/server/app.py +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/src/serpentine/server/websocket.py +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/src/serpentine/skills/agents-md.md +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/src/serpentine/skills/code-analysis.md +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/src/serpentine/skills/copilot-instructions.md +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/src/serpentine/skills/cursor-rules.md +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/src/serpentine/static/logo.svg +0 -0
- {serpentine_parser-0.3.1 → serpentine_parser-0.3.2}/src/serpentine/watcher.py +0 -0
|
@@ -13,9 +13,9 @@ dependencies = [
|
|
|
13
13
|
|
|
14
14
|
[[package]]
|
|
15
15
|
name = "cc"
|
|
16
|
-
version = "1.2.
|
|
16
|
+
version = "1.2.65"
|
|
17
17
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
18
|
-
checksum = "
|
|
18
|
+
checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
|
|
19
19
|
dependencies = [
|
|
20
20
|
"find-msvc-tools",
|
|
21
21
|
"shlex",
|
|
@@ -100,9 +100,9 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
|
|
100
100
|
|
|
101
101
|
[[package]]
|
|
102
102
|
name = "memchr"
|
|
103
|
-
version = "2.8.
|
|
103
|
+
version = "2.8.2"
|
|
104
104
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
105
|
-
checksum = "
|
|
105
|
+
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
|
|
106
106
|
|
|
107
107
|
[[package]]
|
|
108
108
|
name = "once_cell"
|
|
@@ -127,9 +127,9 @@ dependencies = [
|
|
|
127
127
|
|
|
128
128
|
[[package]]
|
|
129
129
|
name = "pyo3"
|
|
130
|
-
version = "0.
|
|
130
|
+
version = "0.29.0"
|
|
131
131
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
132
|
-
checksum = "
|
|
132
|
+
checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c"
|
|
133
133
|
dependencies = [
|
|
134
134
|
"libc",
|
|
135
135
|
"once_cell",
|
|
@@ -141,18 +141,18 @@ dependencies = [
|
|
|
141
141
|
|
|
142
142
|
[[package]]
|
|
143
143
|
name = "pyo3-build-config"
|
|
144
|
-
version = "0.
|
|
144
|
+
version = "0.29.0"
|
|
145
145
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
146
|
-
checksum = "
|
|
146
|
+
checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078"
|
|
147
147
|
dependencies = [
|
|
148
148
|
"target-lexicon",
|
|
149
149
|
]
|
|
150
150
|
|
|
151
151
|
[[package]]
|
|
152
152
|
name = "pyo3-ffi"
|
|
153
|
-
version = "0.
|
|
153
|
+
version = "0.29.0"
|
|
154
154
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
155
|
-
checksum = "
|
|
155
|
+
checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b"
|
|
156
156
|
dependencies = [
|
|
157
157
|
"libc",
|
|
158
158
|
"pyo3-build-config",
|
|
@@ -160,9 +160,9 @@ dependencies = [
|
|
|
160
160
|
|
|
161
161
|
[[package]]
|
|
162
162
|
name = "pyo3-macros"
|
|
163
|
-
version = "0.
|
|
163
|
+
version = "0.29.0"
|
|
164
164
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
165
|
-
checksum = "
|
|
165
|
+
checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771"
|
|
166
166
|
dependencies = [
|
|
167
167
|
"proc-macro2",
|
|
168
168
|
"pyo3-macros-backend",
|
|
@@ -172,13 +172,12 @@ dependencies = [
|
|
|
172
172
|
|
|
173
173
|
[[package]]
|
|
174
174
|
name = "pyo3-macros-backend"
|
|
175
|
-
version = "0.
|
|
175
|
+
version = "0.29.0"
|
|
176
176
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
177
|
-
checksum = "
|
|
177
|
+
checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362"
|
|
178
178
|
dependencies = [
|
|
179
179
|
"heck",
|
|
180
180
|
"proc-macro2",
|
|
181
|
-
"pyo3-build-config",
|
|
182
181
|
"quote",
|
|
183
182
|
"syn",
|
|
184
183
|
]
|
|
@@ -214,9 +213,9 @@ dependencies = [
|
|
|
214
213
|
|
|
215
214
|
[[package]]
|
|
216
215
|
name = "regex"
|
|
217
|
-
version = "1.12.
|
|
216
|
+
version = "1.12.4"
|
|
218
217
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
219
|
-
checksum = "
|
|
218
|
+
checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
|
|
220
219
|
dependencies = [
|
|
221
220
|
"aho-corasick",
|
|
222
221
|
"memchr",
|
|
@@ -237,9 +236,9 @@ dependencies = [
|
|
|
237
236
|
|
|
238
237
|
[[package]]
|
|
239
238
|
name = "regex-syntax"
|
|
240
|
-
version = "0.8.
|
|
239
|
+
version = "0.8.11"
|
|
241
240
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
242
|
-
checksum = "
|
|
241
|
+
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
|
243
242
|
|
|
244
243
|
[[package]]
|
|
245
244
|
name = "serde"
|
|
@@ -287,8 +286,9 @@ dependencies = [
|
|
|
287
286
|
|
|
288
287
|
[[package]]
|
|
289
288
|
name = "serpentine"
|
|
290
|
-
version = "0.3.
|
|
289
|
+
version = "0.3.2"
|
|
291
290
|
dependencies = [
|
|
291
|
+
"indexmap",
|
|
292
292
|
"pyo3",
|
|
293
293
|
"rayon",
|
|
294
294
|
"serde",
|
|
@@ -315,9 +315,9 @@ checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520"
|
|
|
315
315
|
|
|
316
316
|
[[package]]
|
|
317
317
|
name = "syn"
|
|
318
|
-
version = "2.0.
|
|
318
|
+
version = "2.0.118"
|
|
319
319
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
320
|
-
checksum = "
|
|
320
|
+
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
|
321
321
|
dependencies = [
|
|
322
322
|
"proc-macro2",
|
|
323
323
|
"quote",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "serpentine"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.2"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
license = "Apache-2.0"
|
|
6
6
|
|
|
@@ -15,7 +15,8 @@ tree-sitter-javascript = { version = "0.25", default-features = false }
|
|
|
15
15
|
tree-sitter-typescript = { version = "0.23", default-features = false }
|
|
16
16
|
tree-sitter-rust = { version = "0.24", default-features = false }
|
|
17
17
|
tree-sitter-hcl = "1.1.0"
|
|
18
|
-
pyo3 = { version = "0.
|
|
18
|
+
pyo3 = { version = "0.29", features = ["extension-module", "abi3-py310"] }
|
|
19
19
|
rayon = "1.10"
|
|
20
|
-
|
|
20
|
+
indexmap = "2"
|
|
21
|
+
serde = { version = "1.0", features = ["derive", "rc"] }
|
|
21
22
|
serde_json = "1.0"
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
use std::collections::{HashMap, HashSet};
|
|
2
|
+
use std::path::{Path, PathBuf};
|
|
3
|
+
|
|
4
|
+
use indexmap::{IndexMap, IndexSet};
|
|
5
|
+
|
|
6
|
+
use super::{EdgeData, GraphMetadata, LanguageConfig, NodeData};
|
|
7
|
+
use crate::subscribers::RawBinding;
|
|
8
|
+
|
|
9
|
+
/// Per-file ownership record for incremental graph updates.
|
|
10
|
+
#[derive(Default)]
|
|
11
|
+
pub(crate) struct FileContributions {
|
|
12
|
+
/// Qualnames that this file's load passes added to `definitions`.
|
|
13
|
+
pub(crate) node_ids: HashSet<String>,
|
|
14
|
+
/// Edges from per-file load passes (uses, decorators, imports, inheritance).
|
|
15
|
+
/// Does NOT include raw_binding edges (tracked separately on GraphBuilder).
|
|
16
|
+
pub(crate) edge_keys: HashSet<EdgeData>,
|
|
17
|
+
/// Phantom keys this file added to `reexport_map`.
|
|
18
|
+
pub(crate) reexport_keys: Vec<String>,
|
|
19
|
+
/// Keys this file added to `import_bindings`.
|
|
20
|
+
pub(crate) import_binding_keys: Vec<String>,
|
|
21
|
+
/// Typed raw bindings from this file's subscriber output, stored for
|
|
22
|
+
/// global re-merge when any file is dirtied.
|
|
23
|
+
pub(crate) raw_bindings: Vec<RawBinding>,
|
|
24
|
+
/// FNV-like hash of this file's raw_bindings used to detect unchanged
|
|
25
|
+
/// bindings on incremental builds and skip the global re-run.
|
|
26
|
+
pub(crate) raw_bindings_hash: u64,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Builder that combines subscriber outputs into a DependencyGraph
|
|
30
|
+
pub struct GraphBuilder {
|
|
31
|
+
/// Local module top-level prefixes (e.g., {"src", "frontend"})
|
|
32
|
+
pub(crate) local_prefixes: HashSet<String>,
|
|
33
|
+
/// Language configs used for stdlib classification
|
|
34
|
+
pub(crate) lang_configs: Vec<Box<dyn LanguageConfig>>,
|
|
35
|
+
/// All known definitions indexed by qualname (only real definitions!)
|
|
36
|
+
pub(crate) definitions: HashMap<String, NodeData>,
|
|
37
|
+
/// Dependency edges from per-file load passes (uses, imports, decorators, inheritance).
|
|
38
|
+
pub(crate) edges: HashSet<EdgeData>,
|
|
39
|
+
/// Edges produced by the global load_raw_bindings pass, tracked separately
|
|
40
|
+
/// so they can be cleared and re-run incrementally without disturbing per-file edges.
|
|
41
|
+
pub(crate) raw_binding_edges: HashSet<EdgeData>,
|
|
42
|
+
/// Re-export map: phantom qualname → actual definition qualname.
|
|
43
|
+
/// Built from __init__.py imports so that `pkg.name` resolves to
|
|
44
|
+
/// `pkg.submodule.name` when the symbol is re-exported through __init__.
|
|
45
|
+
pub(crate) reexport_map: HashMap<String, String>,
|
|
46
|
+
/// Import bindings: maps "module.local_name" → "resolved_qualname"
|
|
47
|
+
/// e.g., "serpentine.state.Config" → "serpentine.config.Config"
|
|
48
|
+
///
|
|
49
|
+
/// Built from scope_tree + definitions + imports subscriber outputs
|
|
50
|
+
/// BEFORE any edge resolution. Used by resolve_name_legb() to follow
|
|
51
|
+
/// what each name means in each module's namespace, just like Python's
|
|
52
|
+
/// LEGB rule at the G (global/module) level.
|
|
53
|
+
pub(crate) import_bindings: HashMap<String, String>,
|
|
54
|
+
/// Edge index keyed by caller — built before CALLS pass of load_raw_bindings.
|
|
55
|
+
/// Enables O(1) lookup in resolve_variable_type instead of O(E) scan.
|
|
56
|
+
pub(crate) edge_caller_index: HashMap<String, Vec<EdgeData>>,
|
|
57
|
+
/// Function return types: fn_qualname → class_qualname.
|
|
58
|
+
/// Populated by the RETURNS pass in load_raw_bindings.
|
|
59
|
+
/// Used by resolve_variable_type to resolve factory-function return types.
|
|
60
|
+
pub(crate) function_return_types: HashMap<String, String>,
|
|
61
|
+
/// All module qualnames for O(1) lookup in build_reexport_map,
|
|
62
|
+
/// replacing the O(n) `.any(|k| k.starts_with(...))` scan.
|
|
63
|
+
pub(crate) module_qualnames: HashSet<String>,
|
|
64
|
+
/// Per-file ownership records for incremental updates.
|
|
65
|
+
pub(crate) file_contributions: HashMap<PathBuf, FileContributions>,
|
|
66
|
+
/// Files modified since the last graph build. Processed on next call.
|
|
67
|
+
pub(crate) dirty_files: HashSet<PathBuf>,
|
|
68
|
+
/// File currently being loaded. Set before each load call, cleared after.
|
|
69
|
+
/// Used by load_* methods to attribute contributions without extra parameters.
|
|
70
|
+
pub(crate) current_file: Option<PathBuf>,
|
|
71
|
+
/// Cached JSON snapshot from the last completed build. Cleared when any
|
|
72
|
+
/// file is retracted (i.e., on the first dirty-file build after a change).
|
|
73
|
+
pub(crate) cached_snapshot: Option<String>,
|
|
74
|
+
/// Incremental hierarchy: parent qualname → ordered child qualnames.
|
|
75
|
+
/// Updated on every definitions insert and cleared on retract_file.
|
|
76
|
+
pub(crate) hierarchy_children: IndexMap<String, Vec<String>>,
|
|
77
|
+
/// Root qualnames (no parent in definitions), in insertion order.
|
|
78
|
+
pub(crate) hierarchy_roots: IndexSet<String>,
|
|
79
|
+
/// Per-node JSON fragment cache (structural fields only, excluding children).
|
|
80
|
+
/// Entries are invalidated in retract_file when a node is removed.
|
|
81
|
+
/// snapshot() takes &mut self so no Mutex is needed.
|
|
82
|
+
pub(crate) node_json_cache: HashMap<String, String>,
|
|
83
|
+
/// Per-subtree JSON cache: top-level module qualname → full subtree JSON string.
|
|
84
|
+
/// A subtree is dirty (and absent here) when any node within it was retracted
|
|
85
|
+
/// or re-added since the last snapshot. Clean subtrees are emitted directly
|
|
86
|
+
/// without re-traversing the node hierarchy.
|
|
87
|
+
pub(crate) subtree_json_cache: HashMap<String, String>,
|
|
88
|
+
/// Top-level module qualnames whose subtree JSON must be rebuilt on next snapshot.
|
|
89
|
+
/// Populated by retract_file and register_node_in_hierarchy; cleared after snapshot.
|
|
90
|
+
pub(crate) dirty_subtrees: HashSet<String>,
|
|
91
|
+
/// Cached serialized edges JSON (the "edges" section of the snapshot).
|
|
92
|
+
/// Invalidated when raw_binding_edges change (i.e., when clear_raw_binding_edges
|
|
93
|
+
/// is called). Reused when raw_bindings haven't changed on an incremental build.
|
|
94
|
+
pub(crate) cached_edges_json: Option<String>,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
impl Default for GraphBuilder {
|
|
98
|
+
fn default() -> Self {
|
|
99
|
+
Self::new()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
impl GraphBuilder {
|
|
104
|
+
pub fn new() -> Self {
|
|
105
|
+
GraphBuilder {
|
|
106
|
+
local_prefixes: HashSet::new(),
|
|
107
|
+
lang_configs: Vec::new(),
|
|
108
|
+
definitions: HashMap::new(),
|
|
109
|
+
edges: HashSet::new(),
|
|
110
|
+
raw_binding_edges: HashSet::new(),
|
|
111
|
+
reexport_map: HashMap::new(),
|
|
112
|
+
import_bindings: HashMap::new(),
|
|
113
|
+
edge_caller_index: HashMap::new(),
|
|
114
|
+
function_return_types: HashMap::new(),
|
|
115
|
+
module_qualnames: HashSet::new(),
|
|
116
|
+
file_contributions: HashMap::new(),
|
|
117
|
+
dirty_files: HashSet::new(),
|
|
118
|
+
current_file: None,
|
|
119
|
+
cached_snapshot: None,
|
|
120
|
+
hierarchy_children: IndexMap::new(),
|
|
121
|
+
hierarchy_roots: IndexSet::new(),
|
|
122
|
+
node_json_cache: HashMap::new(),
|
|
123
|
+
subtree_json_cache: HashMap::new(),
|
|
124
|
+
dirty_subtrees: HashSet::new(),
|
|
125
|
+
cached_edges_json: None,
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// Record a node qualname as contributed by `current_file`.
|
|
130
|
+
pub(crate) fn record_node_contribution(&mut self, qualname: &str) {
|
|
131
|
+
if let Some(path) = self.current_file.clone() {
|
|
132
|
+
self.file_contributions
|
|
133
|
+
.entry(path)
|
|
134
|
+
.or_default()
|
|
135
|
+
.node_ids
|
|
136
|
+
.insert(qualname.to_string());
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// Record an edge as contributed by `current_file`.
|
|
141
|
+
pub(crate) fn record_edge_contribution(&mut self, edge: EdgeData) {
|
|
142
|
+
if let Some(path) = self.current_file.clone() {
|
|
143
|
+
self.file_contributions
|
|
144
|
+
.entry(path)
|
|
145
|
+
.or_default()
|
|
146
|
+
.edge_keys
|
|
147
|
+
.insert(edge);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// Record a reexport_map key as contributed by `current_file`.
|
|
152
|
+
pub(crate) fn record_reexport_contribution(&mut self, key: &str) {
|
|
153
|
+
if let Some(path) = self.current_file.clone() {
|
|
154
|
+
self.file_contributions
|
|
155
|
+
.entry(path)
|
|
156
|
+
.or_default()
|
|
157
|
+
.reexport_keys
|
|
158
|
+
.push(key.to_string());
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// Record an import_bindings key as contributed by `current_file`.
|
|
163
|
+
pub(crate) fn record_import_binding_contribution(&mut self, key: &str) {
|
|
164
|
+
if let Some(path) = self.current_file.clone() {
|
|
165
|
+
self.file_contributions
|
|
166
|
+
.entry(path)
|
|
167
|
+
.or_default()
|
|
168
|
+
.import_binding_keys
|
|
169
|
+
.push(key.to_string());
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// Register a newly-inserted node in the incremental hierarchy.
|
|
174
|
+
/// Must be called AFTER the node's ancestors exist in `self.definitions`
|
|
175
|
+
/// (i.e., after `ensure_parent_nodes` returns).
|
|
176
|
+
pub(crate) fn register_node_in_hierarchy(&mut self, qualname: &str) {
|
|
177
|
+
// Mark the top-level subtree as dirty so snapshot rebuilds it
|
|
178
|
+
let root = qualname.split('.').next().unwrap_or(qualname);
|
|
179
|
+
self.dirty_subtrees.insert(root.to_string());
|
|
180
|
+
self.subtree_json_cache.remove(root);
|
|
181
|
+
|
|
182
|
+
if let Some((parent, _)) = qualname.rsplit_once('.') {
|
|
183
|
+
if self.definitions.contains_key(parent) {
|
|
184
|
+
let children = self.hierarchy_children.entry(parent.to_string()).or_default();
|
|
185
|
+
if !children.iter().any(|c| c == qualname) {
|
|
186
|
+
children.push(qualname.to_string());
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
self.hierarchy_roots.insert(qualname.to_string());
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/// Build the edge_caller_index from current edges. Call before the CALLS
|
|
195
|
+
/// pass of load_raw_bindings so resolve_variable_type gets O(1) lookups.
|
|
196
|
+
pub(crate) fn build_edge_caller_index(&mut self) {
|
|
197
|
+
self.edge_caller_index.clear();
|
|
198
|
+
// Only has-a edges are ever queried from this index (resolve_variable_type,
|
|
199
|
+
// constructor-arg, param-type passes all filter for edge_type == "has-a").
|
|
200
|
+
// Indexing only has-a reduces the index by ~10-20× vs all edge types.
|
|
201
|
+
for edge in self.edges.iter().chain(self.raw_binding_edges.iter()) {
|
|
202
|
+
if edge.edge_type == "has-a" {
|
|
203
|
+
self.edge_caller_index
|
|
204
|
+
.entry(edge.caller.clone())
|
|
205
|
+
.or_default()
|
|
206
|
+
.push(edge.clone());
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/// Remove all raw_binding-derived edges from `self.edges` and clear the
|
|
212
|
+
/// tracking set. Call before re-running `load_raw_bindings` globally.
|
|
213
|
+
pub fn clear_raw_binding_edges(&mut self) {
|
|
214
|
+
for edge in self.raw_binding_edges.drain() {
|
|
215
|
+
self.edges.remove(&edge);
|
|
216
|
+
}
|
|
217
|
+
self.cached_edges_json = None;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/// Remove all contributions from `path` and retract their effects on graph state.
|
|
221
|
+
pub fn retract_file(&mut self, path: &Path) {
|
|
222
|
+
self.cached_snapshot = None;
|
|
223
|
+
let Some(contrib) = self.file_contributions.remove(path) else {
|
|
224
|
+
return;
|
|
225
|
+
};
|
|
226
|
+
for qualname in &contrib.node_ids {
|
|
227
|
+
self.definitions.remove(qualname);
|
|
228
|
+
self.module_qualnames.remove(qualname);
|
|
229
|
+
// Remove from incremental hierarchy
|
|
230
|
+
self.hierarchy_roots.shift_remove(qualname.as_str());
|
|
231
|
+
if let Some((parent, _)) = qualname.rsplit_once('.') {
|
|
232
|
+
if let Some(children) = self.hierarchy_children.get_mut(parent) {
|
|
233
|
+
children.retain(|c| c != qualname.as_str());
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
self.hierarchy_children.shift_remove(qualname.as_str());
|
|
237
|
+
// Invalidate the per-node JSON fragment cache entry
|
|
238
|
+
self.node_json_cache.remove(qualname.as_str());
|
|
239
|
+
// Mark the top-level subtree dirty so snapshot rebuilds it
|
|
240
|
+
let root = qualname.split('.').next().unwrap_or(qualname);
|
|
241
|
+
self.dirty_subtrees.insert(root.to_string());
|
|
242
|
+
self.subtree_json_cache.remove(root);
|
|
243
|
+
}
|
|
244
|
+
for edge in &contrib.edge_keys {
|
|
245
|
+
self.edges.remove(edge);
|
|
246
|
+
}
|
|
247
|
+
for key in &contrib.reexport_keys {
|
|
248
|
+
self.reexport_map.remove(key);
|
|
249
|
+
}
|
|
250
|
+
for key in &contrib.import_binding_keys {
|
|
251
|
+
self.import_bindings.remove(key);
|
|
252
|
+
}
|
|
253
|
+
// raw_bindings removed with the FileContributions entry above.
|
|
254
|
+
// Caller is responsible for calling clear_raw_binding_edges() and
|
|
255
|
+
// re-running load_raw_bindings() globally after all retractions.
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/// True if one endpoint is a direct ancestor of the other in the dotted hierarchy.
|
|
259
|
+
/// Replaces `starts_with(&format!("{}.", x))` with a zero-allocation check.
|
|
260
|
+
#[inline]
|
|
261
|
+
fn is_ancestor_edge(e: &EdgeData) -> bool {
|
|
262
|
+
fn one_is_ancestor(parent: &str, child: &str) -> bool {
|
|
263
|
+
child.len() > parent.len()
|
|
264
|
+
&& child.as_bytes().get(parent.len()) == Some(&b'.')
|
|
265
|
+
&& child.starts_with(parent)
|
|
266
|
+
}
|
|
267
|
+
one_is_ancestor(&e.caller, &e.callee) || one_is_ancestor(&e.callee, &e.caller)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/// Return the set of has-a edges that are redundant because a more-specific
|
|
271
|
+
/// edge from a descendant caller already covers the same top-level module.
|
|
272
|
+
///
|
|
273
|
+
/// O(N log N): builds sorted caller lists per top-level module, then uses
|
|
274
|
+
/// binary search to check for prefix matches — avoids the O(N×M) inner
|
|
275
|
+
/// `any(starts_with)` scan of the previous implementation.
|
|
276
|
+
///
|
|
277
|
+
/// Operates entirely on borrowed references; no EdgeData is cloned.
|
|
278
|
+
fn find_redundant_edges<'a>(all_edges: &[&'a EdgeData]) -> HashSet<&'a EdgeData> {
|
|
279
|
+
// Build sorted caller list per top-level callee module
|
|
280
|
+
let mut module_callers: HashMap<&str, Vec<&str>> = HashMap::new();
|
|
281
|
+
for edge in all_edges {
|
|
282
|
+
let top = edge.callee.split('.').next().unwrap_or(edge.callee.as_str());
|
|
283
|
+
module_callers.entry(top).or_default().push(edge.caller.as_str());
|
|
284
|
+
}
|
|
285
|
+
for callers in module_callers.values_mut() {
|
|
286
|
+
callers.sort_unstable();
|
|
287
|
+
callers.dedup();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let mut redundant: HashSet<&EdgeData> = HashSet::new();
|
|
291
|
+
for &edge in all_edges {
|
|
292
|
+
if edge.edge_type != "has-a" {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
let top = edge.callee.split('.').next().unwrap_or(edge.callee.as_str());
|
|
296
|
+
if let Some(callers) = module_callers.get(top) {
|
|
297
|
+
let caller = edge.caller.as_str();
|
|
298
|
+
// Binary search: find first entry that could start with "caller."
|
|
299
|
+
// Strings starting with "caller." sort immediately after "caller" and
|
|
300
|
+
// before any string that is lexicographically >= "caller" + char > '.'.
|
|
301
|
+
let idx = callers.partition_point(|&c| c <= caller);
|
|
302
|
+
// Check if the entry at idx starts with "caller." without allocating
|
|
303
|
+
if callers.get(idx).is_some_and(|&c| {
|
|
304
|
+
c.len() > caller.len()
|
|
305
|
+
&& c.as_bytes().get(caller.len()) == Some(&b'.')
|
|
306
|
+
&& c.starts_with(caller)
|
|
307
|
+
}) {
|
|
308
|
+
redundant.insert(edge);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
redundant
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/// Produce a graph snapshot JSON string from current state without consuming the builder.
|
|
316
|
+
/// Uses the incremental hierarchy and per-node JSON fragment cache for fast incremental
|
|
317
|
+
/// serialization: only nodes retracted since the last snapshot need re-serialization.
|
|
318
|
+
pub fn snapshot(&mut self) -> String {
|
|
319
|
+
// Build edge JSON (cached separately from nodes so it survives node-only changes)
|
|
320
|
+
let edges_json = if let Some(ref cached) = self.cached_edges_json {
|
|
321
|
+
cached.clone()
|
|
322
|
+
} else {
|
|
323
|
+
// Collect edge refs without cloning any EdgeData. Deduplicate by content
|
|
324
|
+
// since self.edges and raw_binding_edges can contain value-equivalent items.
|
|
325
|
+
let mut seen: HashSet<(&str, &str, &str)> = HashSet::new();
|
|
326
|
+
let all_edges: Vec<&EdgeData> = self.edges.iter()
|
|
327
|
+
.chain(self.raw_binding_edges.iter())
|
|
328
|
+
.filter(|e| seen.insert((e.caller.as_str(), e.callee.as_str(), e.edge_type.as_str())))
|
|
329
|
+
.collect();
|
|
330
|
+
|
|
331
|
+
let redundant = Self::find_redundant_edges(&all_edges);
|
|
332
|
+
let edges: Vec<&EdgeData> = all_edges.into_iter()
|
|
333
|
+
.filter(|&e| !redundant.contains(e))
|
|
334
|
+
.filter(|e| !Self::is_ancestor_edge(e))
|
|
335
|
+
.collect();
|
|
336
|
+
|
|
337
|
+
let j = serde_json::to_string(&edges).unwrap_or_default();
|
|
338
|
+
self.cached_edges_json = Some(j.clone());
|
|
339
|
+
j
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
let edge_count = {
|
|
343
|
+
// Count from the JSON cheaply by counting top-level objects
|
|
344
|
+
// (serde already produced it; for metadata just recount the filtered edges)
|
|
345
|
+
// Re-derive from edges_json length would be wrong; just recompute edge count.
|
|
346
|
+
// We already know the edges: use the cached_edges_json length is wrong.
|
|
347
|
+
// Parse is too expensive. Store edge_count alongside cached_edges_json instead.
|
|
348
|
+
// For now, count commas at top level — edges_json is `[{...},{...},...]`
|
|
349
|
+
if edges_json == "[]" || edges_json == "null" { 0usize }
|
|
350
|
+
else { edges_json.bytes().filter(|&b| b == b'{').count() }
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
let mut out = String::with_capacity(8 * 1024 * 1024);
|
|
354
|
+
out.push_str("{\"nodes\":[");
|
|
355
|
+
|
|
356
|
+
// Emit nodes using per-subtree cache. Dirty subtrees are rebuilt and re-cached;
|
|
357
|
+
// clean subtrees are emitted directly from cache without any node traversal.
|
|
358
|
+
let mut first_root = true;
|
|
359
|
+
let roots: Vec<String> = self.hierarchy_roots.iter().cloned().collect();
|
|
360
|
+
for root in &roots {
|
|
361
|
+
if !self.definitions.contains_key(root.as_str()) {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if !first_root { out.push(','); }
|
|
365
|
+
first_root = false;
|
|
366
|
+
|
|
367
|
+
if let Some(cached_subtree) = self.subtree_json_cache.get(root.as_str()) {
|
|
368
|
+
// Clean subtree: emit cached JSON directly
|
|
369
|
+
out.push_str(cached_subtree);
|
|
370
|
+
} else {
|
|
371
|
+
// Dirty subtree: rebuild and cache
|
|
372
|
+
let subtree_start = out.len();
|
|
373
|
+
self.write_node_json(&mut out, root);
|
|
374
|
+
let subtree = out[subtree_start..].to_string();
|
|
375
|
+
self.subtree_json_cache.insert(root.clone(), subtree);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
self.dirty_subtrees.clear();
|
|
379
|
+
|
|
380
|
+
out.push_str("],\"edges\":");
|
|
381
|
+
out.push_str(&edges_json);
|
|
382
|
+
let metadata = self.compute_metadata_inline(edge_count);
|
|
383
|
+
out.push_str(",\"metadata\":");
|
|
384
|
+
out.push_str(&serde_json::to_string(&metadata).unwrap_or_default());
|
|
385
|
+
out.push('}');
|
|
386
|
+
out
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/// Recursively write the JSON for a single node and its children into `out`.
|
|
390
|
+
/// Uses `node_json_cache` to avoid re-serializing unchanged node fragments.
|
|
391
|
+
fn write_node_json(&mut self, out: &mut String, qualname: &str) {
|
|
392
|
+
// We need node data but can't hold a borrow of self.definitions while also
|
|
393
|
+
// borrowing self.node_json_cache mutably. Serialize fresh if cache miss,
|
|
394
|
+
// using a temporary clone of the node to avoid the borrow conflict.
|
|
395
|
+
let fragment = if let Some(frag) = self.node_json_cache.get(qualname) {
|
|
396
|
+
frag.clone()
|
|
397
|
+
} else {
|
|
398
|
+
let frag = self.definitions.get(qualname)
|
|
399
|
+
.map(|n| serde_json::to_string(n).unwrap_or_default())
|
|
400
|
+
.unwrap_or_default();
|
|
401
|
+
if !frag.is_empty() {
|
|
402
|
+
self.node_json_cache.insert(qualname.to_string(), frag.clone());
|
|
403
|
+
}
|
|
404
|
+
frag
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
if fragment.is_empty() { return; }
|
|
408
|
+
|
|
409
|
+
// Strip the closing `}` and inject `"children":[...]`
|
|
410
|
+
let base = fragment.trim_end_matches('}');
|
|
411
|
+
out.push_str(base);
|
|
412
|
+
out.push_str(",\"children\":[");
|
|
413
|
+
let kids: Vec<String> = self.hierarchy_children
|
|
414
|
+
.get(qualname).cloned()
|
|
415
|
+
.unwrap_or_default();
|
|
416
|
+
let mut first = true;
|
|
417
|
+
for child in &kids {
|
|
418
|
+
if self.definitions.contains_key(child.as_str()) {
|
|
419
|
+
if !first { out.push(','); }
|
|
420
|
+
first = false;
|
|
421
|
+
self.write_node_json(out, child);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
out.push_str("]}");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/// Compute graph metadata directly from definitions (no DependencyGraph traversal).
|
|
428
|
+
fn compute_metadata_inline(&self, edge_count: usize) -> GraphMetadata {
|
|
429
|
+
let mut node_types: HashMap<String, usize> = HashMap::new();
|
|
430
|
+
for node in self.definitions.values() {
|
|
431
|
+
let type_str = format!("{:?}", node.object_type).to_lowercase();
|
|
432
|
+
*node_types.entry(type_str).or_insert(0) += 1;
|
|
433
|
+
}
|
|
434
|
+
GraphMetadata {
|
|
435
|
+
node_count: self.definitions.len(),
|
|
436
|
+
edge_count,
|
|
437
|
+
node_types,
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
}
|