codingest 0.1.5__tar.gz → 0.1.6__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.
- {codingest-0.1.5 → codingest-0.1.6}/Cargo.lock +4 -4
- {codingest-0.1.5 → codingest-0.1.6}/Cargo.toml +1 -1
- {codingest-0.1.5 → codingest-0.1.6}/PKG-INFO +1 -1
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/bin/codingest_bench.rs +5 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/bin/codingest_stats.rs +166 -24
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/builder/call_edges.rs +194 -19
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/builder/load/entity_frames.rs +34 -1
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/builder/other_edges.rs +140 -15
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/builder/routes/django.rs +136 -11
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/docs/mod.rs +255 -29
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/parsers/python.rs +726 -2
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/parsers/shared.rs +88 -1
- codingest-0.1.6/crates/codingest/src/parsers/typescript.rs +2764 -0
- codingest-0.1.6/crates/codingest/tests/goldens/README.md +146 -0
- codingest-0.1.6/crates/codingest/tests/goldens/docs_mdx.sha256 +1 -0
- codingest-0.1.6/crates/codingest/tests/goldens/py_nested_defs.sha256 +1 -0
- codingest-0.1.6/crates/codingest/tests/goldens/ts_closure_scope.sha256 +1 -0
- codingest-0.1.6/crates/codingest/tests/goldens/ts_hof_binding.sha256 +1 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/tests/parity.rs +42 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest-cli/Cargo.toml +1 -1
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest-mcp/Cargo.toml +1 -1
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest-py/Cargo.toml +3 -3
- codingest-0.1.5/crates/codingest/src/parsers/typescript.rs +0 -1281
- codingest-0.1.5/crates/codingest/tests/goldens/README.md +0 -80
- {codingest-0.1.5 → codingest-0.1.6}/LICENSE +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/README.md +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/codingest/__init__.py +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/codingest/__init__.pyi +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/codingest/cli.py +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/codingest/mcp_server.py +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/Cargo.toml +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/builder/js_workspace.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/builder/load/edge_frames.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/builder/load.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/builder/mod.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/builder/routes/fastapi.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/builder/routes/flask.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/builder/routes/mod.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/builder/semantic_edges.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/builder/type_edges.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/cross_lang.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/docs/rst.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/lib.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/manifest/mod.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/models.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/parsers/agc.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/parsers/cpp.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/parsers/csharp.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/parsers/css.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/parsers/dart.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/parsers/go.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/parsers/html.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/parsers/java.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/parsers/mod.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/parsers/php.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/parsers/registry.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/parsers/rust_lang.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/parsers/swift.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/repo.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/src/rev.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/tests/agc_apollo.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/tests/goldens/agc_basic.sha256 +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/tests/goldens/cross_ts_py.sha256 +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/tests/goldens/dup_minified_assets.sha256 +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/tests/goldens/py_basic.sha256 +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/tests/goldens/py_inheritance.sha256 +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/tests/goldens/rust_xfile.sha256 +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/tests/goldens/ts_callback.sha256 +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest/tests/goldens/ts_monorepo.sha256 +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest-cli/skills/codingest-code-review/SKILL.md +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest-cli/skills/codingest-code-review/references/mcp-upgrade.md +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest-cli/skills/codingest-code-review/references/public-repositories.md +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest-cli/skills/codingest-code-review/references/queries.md +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest-cli/src/code_tree_cli.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest-cli/src/lib.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest-cli/src/main.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest-cli/src/query.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest-cli/src/skill.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest-cli/src/skill_assets.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest-cli/tests/exit_codes.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest-mcp/src/lib.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest-mcp/src/main.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/crates/codingest-py/src/lib.rs +0 -0
- {codingest-0.1.5 → codingest-0.1.6}/pyproject.toml +0 -0
|
@@ -313,7 +313,7 @@ dependencies = [
|
|
|
313
313
|
|
|
314
314
|
[[package]]
|
|
315
315
|
name = "codingest"
|
|
316
|
-
version = "0.1.
|
|
316
|
+
version = "0.1.6"
|
|
317
317
|
dependencies = [
|
|
318
318
|
"aho-corasick",
|
|
319
319
|
"base64",
|
|
@@ -345,7 +345,7 @@ dependencies = [
|
|
|
345
345
|
|
|
346
346
|
[[package]]
|
|
347
347
|
name = "codingest-cli"
|
|
348
|
-
version = "0.1.
|
|
348
|
+
version = "0.1.6"
|
|
349
349
|
dependencies = [
|
|
350
350
|
"anyhow",
|
|
351
351
|
"clap",
|
|
@@ -357,7 +357,7 @@ dependencies = [
|
|
|
357
357
|
|
|
358
358
|
[[package]]
|
|
359
359
|
name = "codingest-mcp"
|
|
360
|
-
version = "0.1.
|
|
360
|
+
version = "0.1.6"
|
|
361
361
|
dependencies = [
|
|
362
362
|
"anyhow",
|
|
363
363
|
"codingest",
|
|
@@ -366,7 +366,7 @@ dependencies = [
|
|
|
366
366
|
|
|
367
367
|
[[package]]
|
|
368
368
|
name = "codingest-py"
|
|
369
|
-
version = "0.1.
|
|
369
|
+
version = "0.1.6"
|
|
370
370
|
dependencies = [
|
|
371
371
|
"codingest",
|
|
372
372
|
"codingest-cli",
|
|
@@ -486,6 +486,11 @@ fn main() {
|
|
|
486
486
|
"corpus_files": corpus.sha256.as_ref().map(|_| corpus.files),
|
|
487
487
|
"corpus_bytes": corpus.sha256.as_ref().map(|_| corpus.bytes),
|
|
488
488
|
"corpus_sha256": corpus.sha256,
|
|
489
|
+
// Fixed build configuration, echoed so a recorded row states it.
|
|
490
|
+
// Unlike codingest_stats, this harness has always built docs-ON,
|
|
491
|
+
// so the docs pass IS in its build times and node counts.
|
|
492
|
+
"include_tests": false,
|
|
493
|
+
"include_docs": true,
|
|
489
494
|
"nodes": nodes,
|
|
490
495
|
"edges": edges,
|
|
491
496
|
"build_a_secs": (build_a_secs * 1000.0).round() / 1000.0,
|
|
@@ -15,10 +15,21 @@
|
|
|
15
15
|
//! Usage:
|
|
16
16
|
//! cargo run -p codingest --bin codingest_stats --release -- <path>
|
|
17
17
|
//! cargo run -p codingest --bin codingest_stats --release -- <path> --include-tests
|
|
18
|
+
//! cargo run -p codingest --bin codingest_stats --release -- <path> --include-docs
|
|
18
19
|
//! cargo run -p codingest --bin codingest_stats --release -- <path> --function-metrics
|
|
19
20
|
//! cargo run -p codingest --bin codingest_stats --release -- <path> --edge-breakdown
|
|
20
21
|
//! cargo run -p codingest --bin codingest_stats --release -- <path> --dump-calls a,b,c
|
|
21
22
|
//!
|
|
23
|
+
//! `--include-docs` turns the docs pass on. It is **off by default**, which is
|
|
24
|
+
//! the configuration every historical row in `dev-docs/bench/results/` was
|
|
25
|
+
//! measured at, so the default is the comparable one; without the flag `:Doc`,
|
|
26
|
+
//! `:MENTIONS` and `:DOCUMENTS` are structurally absent and no docs-pass
|
|
27
|
+
//! regression — in node counts or in build time — can be seen here. Both the
|
|
28
|
+
//! tests and the docs configuration are echoed into the emitted JSON so a
|
|
29
|
+
//! recorded row states the configuration it was taken under, and an unknown
|
|
30
|
+
//! flag is a usage error rather than a silently-ignored typo that would report
|
|
31
|
+
//! a measurement under a mode it never ran in.
|
|
32
|
+
//!
|
|
22
33
|
//! `--edge-breakdown` appends a per-connection-type edge histogram to the JSON
|
|
23
34
|
//! object, with `IMPORTS` split by endpoint node type (`IMPORTS(File->File)`
|
|
24
35
|
//! vs `IMPORTS(File->Module)`) — the two are the same connection type but
|
|
@@ -231,37 +242,76 @@ fn dump_calls(graph: &DirGraph, names: &BTreeSet<String>) -> Vec<CallRow> {
|
|
|
231
242
|
rows
|
|
232
243
|
}
|
|
233
244
|
|
|
245
|
+
const USAGE: &str = "usage: codingest_stats <path> [--include-tests] [--include-docs] \
|
|
246
|
+
[--function-metrics] [--edge-breakdown] [--dump-calls name1,name2,...]";
|
|
247
|
+
|
|
248
|
+
/// Everything the command line can say, parsed once so the configuration a run
|
|
249
|
+
/// was taken under is a value that can be echoed and tested rather than a
|
|
250
|
+
/// scatter of `any(|a| a == …)` predicates.
|
|
251
|
+
#[derive(Debug, Default, PartialEq, Eq)]
|
|
252
|
+
struct Options {
|
|
253
|
+
include_tests: bool,
|
|
254
|
+
include_docs: bool,
|
|
255
|
+
function_metrics: bool,
|
|
256
|
+
edge_breakdown: bool,
|
|
257
|
+
dump_calls: Option<BTreeSet<String>>,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/// Parse the arguments after `<path>`. Rejects anything unrecognised: a
|
|
261
|
+
/// typo'd `--include-dcos` must not report a docs-off measurement as a docs-on
|
|
262
|
+
/// one, which is the same reason `codingest_bench` rejects unknown flags.
|
|
263
|
+
fn parse_options(args: &[String]) -> Result<Options, String> {
|
|
264
|
+
let mut out = Options::default();
|
|
265
|
+
let mut i = 0;
|
|
266
|
+
while i < args.len() {
|
|
267
|
+
match args[i].as_str() {
|
|
268
|
+
"--include-tests" => out.include_tests = true,
|
|
269
|
+
"--include-docs" => out.include_docs = true,
|
|
270
|
+
"--function-metrics" => out.function_metrics = true,
|
|
271
|
+
"--edge-breakdown" => out.edge_breakdown = true,
|
|
272
|
+
"--dump-calls" => {
|
|
273
|
+
let list = args.get(i + 1).ok_or_else(|| {
|
|
274
|
+
"--dump-calls requires a comma-separated name list".to_string()
|
|
275
|
+
})?;
|
|
276
|
+
out.dump_calls = Some(
|
|
277
|
+
list.split(',')
|
|
278
|
+
.map(str::trim)
|
|
279
|
+
.filter(|s| !s.is_empty())
|
|
280
|
+
.map(str::to_string)
|
|
281
|
+
.collect(),
|
|
282
|
+
);
|
|
283
|
+
i += 1;
|
|
284
|
+
}
|
|
285
|
+
other => return Err(format!("unknown argument `{other}`")),
|
|
286
|
+
}
|
|
287
|
+
i += 1;
|
|
288
|
+
}
|
|
289
|
+
Ok(out)
|
|
290
|
+
}
|
|
291
|
+
|
|
234
292
|
fn main() {
|
|
235
293
|
let mut args = std::env::args().skip(1);
|
|
236
294
|
let path = match args.next() {
|
|
237
295
|
Some(p) => p,
|
|
238
296
|
None => {
|
|
239
|
-
eprintln!(
|
|
240
|
-
"usage: codingest_stats <path> [--include-tests] [--function-metrics] \
|
|
241
|
-
[--edge-breakdown] [--dump-calls name1,name2,...]"
|
|
242
|
-
);
|
|
297
|
+
eprintln!("{USAGE}");
|
|
243
298
|
std::process::exit(2);
|
|
244
299
|
}
|
|
245
300
|
};
|
|
246
|
-
let
|
|
247
|
-
let
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
None => {
|
|
261
|
-
eprintln!("--dump-calls requires a comma-separated name list");
|
|
262
|
-
std::process::exit(2);
|
|
263
|
-
}
|
|
264
|
-
});
|
|
301
|
+
let rest: Vec<String> = args.collect();
|
|
302
|
+
let options = match parse_options(&rest) {
|
|
303
|
+
Ok(options) => options,
|
|
304
|
+
Err(e) => {
|
|
305
|
+
eprintln!("{e}");
|
|
306
|
+
eprintln!("{USAGE}");
|
|
307
|
+
std::process::exit(2);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
let include_tests = options.include_tests;
|
|
311
|
+
let include_docs = options.include_docs;
|
|
312
|
+
let emit_function_metrics = options.function_metrics;
|
|
313
|
+
let emit_edge_breakdown = options.edge_breakdown;
|
|
314
|
+
let dump_call_names = options.dump_calls.clone();
|
|
265
315
|
|
|
266
316
|
let t = Instant::now();
|
|
267
317
|
let (graph, stats) = match codingest::builder::run_with_options_stats(
|
|
@@ -270,7 +320,7 @@ fn main() {
|
|
|
270
320
|
include_tests,
|
|
271
321
|
None,
|
|
272
322
|
None,
|
|
273
|
-
|
|
323
|
+
include_docs,
|
|
274
324
|
) {
|
|
275
325
|
Ok(pair) => pair,
|
|
276
326
|
Err(e) => {
|
|
@@ -306,6 +356,7 @@ fn main() {
|
|
|
306
356
|
let mut out = serde_json::json!({
|
|
307
357
|
"path": path,
|
|
308
358
|
"include_tests": include_tests,
|
|
359
|
+
"include_docs": include_docs,
|
|
309
360
|
"build_secs": (build_secs * 1000.0).round() / 1000.0,
|
|
310
361
|
"nodes": graph.graph.node_count(),
|
|
311
362
|
"edges": graph.graph.edge_count(),
|
|
@@ -343,6 +394,97 @@ mod tests {
|
|
|
343
394
|
.expect("corpus build")
|
|
344
395
|
}
|
|
345
396
|
|
|
397
|
+
fn opts(args: &[&str]) -> Result<Options, String> {
|
|
398
|
+
parse_options(&args.iter().map(|a| a.to_string()).collect::<Vec<_>>())
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
#[test]
|
|
402
|
+
fn parse_options_defaults_to_the_historical_configuration() {
|
|
403
|
+
// Every row in dev-docs/bench/results/ predating --include-docs was
|
|
404
|
+
// measured tests-off/docs-off; the default must stay that way or old
|
|
405
|
+
// and new rows silently stop being comparable.
|
|
406
|
+
let parsed = opts(&[]).expect("empty argv parses");
|
|
407
|
+
assert_eq!(parsed, Options::default());
|
|
408
|
+
assert!(!parsed.include_tests);
|
|
409
|
+
assert!(!parsed.include_docs);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
#[test]
|
|
413
|
+
fn parse_options_reads_every_flag_including_docs() {
|
|
414
|
+
let parsed = opts(&[
|
|
415
|
+
"--include-tests",
|
|
416
|
+
"--include-docs",
|
|
417
|
+
"--edge-breakdown",
|
|
418
|
+
"--function-metrics",
|
|
419
|
+
"--dump-calls",
|
|
420
|
+
"run, helper ,,other",
|
|
421
|
+
])
|
|
422
|
+
.expect("all flags parse");
|
|
423
|
+
assert!(parsed.include_tests);
|
|
424
|
+
assert!(parsed.include_docs);
|
|
425
|
+
assert!(parsed.edge_breakdown);
|
|
426
|
+
assert!(parsed.function_metrics);
|
|
427
|
+
assert_eq!(
|
|
428
|
+
parsed.dump_calls,
|
|
429
|
+
Some(BTreeSet::from([
|
|
430
|
+
"run".to_string(),
|
|
431
|
+
"helper".to_string(),
|
|
432
|
+
"other".to_string()
|
|
433
|
+
])),
|
|
434
|
+
"the name list is split, trimmed and empties dropped"
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
#[test]
|
|
439
|
+
fn parse_options_rejects_an_unknown_flag_but_not_a_dump_calls_value() {
|
|
440
|
+
// A typo must be a usage error: silently ignoring it would report a
|
|
441
|
+
// measurement under a configuration the run never used.
|
|
442
|
+
assert_eq!(
|
|
443
|
+
opts(&["--include-dcos"]),
|
|
444
|
+
Err("unknown argument `--include-dcos`".to_string())
|
|
445
|
+
);
|
|
446
|
+
assert_eq!(
|
|
447
|
+
opts(&["--dump-calls"]),
|
|
448
|
+
Err("--dump-calls requires a comma-separated name list".to_string())
|
|
449
|
+
);
|
|
450
|
+
// The value that follows --dump-calls is consumed, not re-inspected as
|
|
451
|
+
// a flag — even when it looks like one.
|
|
452
|
+
let parsed = opts(&["--dump-calls", "--include-docs", "--include-tests"])
|
|
453
|
+
.expect("the value after --dump-calls is a value");
|
|
454
|
+
assert!(!parsed.include_docs, "the value was not read as a flag");
|
|
455
|
+
assert!(parsed.include_tests);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
#[test]
|
|
459
|
+
fn include_docs_is_what_makes_doc_nodes_visible() {
|
|
460
|
+
// The gap this flag closes: with the docs pass off — the hard-coded
|
|
461
|
+
// behaviour before it existed — a :Doc node cannot appear in any
|
|
462
|
+
// codingest_stats number, so no docs-pass regression can fail a gate.
|
|
463
|
+
fn doc_nodes(include_docs: bool) -> usize {
|
|
464
|
+
let graph = codingest::builder::run_with_options(
|
|
465
|
+
&corpus("docs_mdx"),
|
|
466
|
+
false,
|
|
467
|
+
true,
|
|
468
|
+
None,
|
|
469
|
+
None,
|
|
470
|
+
include_docs,
|
|
471
|
+
)
|
|
472
|
+
.expect("corpus build");
|
|
473
|
+
let params = HashMap::new();
|
|
474
|
+
let options = session::ExecuteOptions::eager(¶ms);
|
|
475
|
+
session::execute_read(&graph, "MATCH (d:Doc) RETURN d.name", &options)
|
|
476
|
+
.expect("query docs")
|
|
477
|
+
.result
|
|
478
|
+
.rows
|
|
479
|
+
.len()
|
|
480
|
+
}
|
|
481
|
+
assert_eq!(doc_nodes(false), 0, "docs off means no Doc node exists");
|
|
482
|
+
assert!(
|
|
483
|
+
doc_nodes(true) > 0,
|
|
484
|
+
"docs on must surface the docs_mdx corpus's Doc nodes"
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
346
488
|
#[test]
|
|
347
489
|
fn short_name_takes_the_rightmost_separator_of_any_flavour() {
|
|
348
490
|
assert_eq!(short_name("pkg.util.helper"), "helper");
|
|
@@ -229,6 +229,41 @@ fn qname_starts_with_any(qname: &str, scopes: &[String]) -> bool {
|
|
|
229
229
|
false
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
/// D3: a closure-scoped definition — one the parser tagged with a non-zero
|
|
233
|
+
/// `nesting_depth` — is lexically reachable only inside its enclosing scope
|
|
234
|
+
/// unless it escapes. Every bare-name index over `functions` has to keep it
|
|
235
|
+
/// out of the global tier and offer it to its own file only.
|
|
236
|
+
pub(super) fn is_nested_function(function: &FunctionInfo) -> bool {
|
|
237
|
+
function
|
|
238
|
+
.metadata
|
|
239
|
+
.get("nesting_depth")
|
|
240
|
+
.and_then(|value| value.as_u64())
|
|
241
|
+
.is_some_and(|depth| depth > 0)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/// D3 candidate assembly: the globally visible targets for a call name plus
|
|
245
|
+
/// the nested (closure-scoped) targets declared in the *caller's own file*.
|
|
246
|
+
///
|
|
247
|
+
/// Borrows both sides untouched in the common case — `storage` is filled only
|
|
248
|
+
/// when a name has candidates on both sides, which is rare.
|
|
249
|
+
pub(super) fn merge_candidates<'a, 'b>(
|
|
250
|
+
global: Option<&'b Vec<&'a str>>,
|
|
251
|
+
local: Option<&'b Vec<&'a str>>,
|
|
252
|
+
storage: &'b mut Vec<&'a str>,
|
|
253
|
+
) -> &'b [&'a str] {
|
|
254
|
+
match (global, local) {
|
|
255
|
+
(Some(g), None) => g.as_slice(),
|
|
256
|
+
(None, Some(l)) => l.as_slice(),
|
|
257
|
+
(None, None) => &[],
|
|
258
|
+
(Some(g), Some(l)) => {
|
|
259
|
+
storage.reserve(g.len() + l.len());
|
|
260
|
+
storage.extend_from_slice(g);
|
|
261
|
+
storage.extend_from_slice(l);
|
|
262
|
+
storage.as_slice()
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
232
267
|
fn infer_lang_group(qname: &str) -> &'static str {
|
|
233
268
|
if qname.contains("::") {
|
|
234
269
|
"rust_cpp"
|
|
@@ -262,29 +297,62 @@ pub fn build_call_edges(
|
|
|
262
297
|
) -> (Vec<CallEdge>, CallResolutionStats) {
|
|
263
298
|
let verbose = std::env::var_os("KGLITE_CODE_TREE_VERBOSE").is_some();
|
|
264
299
|
let t0 = std::time::Instant::now();
|
|
265
|
-
//
|
|
300
|
+
// D3 — a closure-scoped definition (`nesting_depth > 0`) is lexically
|
|
301
|
+
// callable only inside its enclosing scope unless it escapes. It
|
|
302
|
+
// therefore never joins the global lookups; it is offered only to
|
|
303
|
+
// callers in its own file. Without this the ~2 270 nested names opencode
|
|
304
|
+
// gains would flood the bare-name index: the Phase 1 spike measured
|
|
305
|
+
// multi-candidate names going 664 → 1 562 and 293 currently-unique names
|
|
306
|
+
// turning ambiguous, which is exactly the false-positive class v0.1.5's
|
|
307
|
+
// resolution metadata was built to keep out. Escape analysis (which
|
|
308
|
+
// nested functions really do leak cross-file) is the deliberate
|
|
309
|
+
// non-goal; same-file is the conservative approximation. The predicate
|
|
310
|
+
// itself is `is_nested_function` above — `build_references_fn_edges` and
|
|
311
|
+
// `build_decorates_edges` are bare-name indexes over the same population
|
|
312
|
+
// and gate on it too.
|
|
313
|
+
let is_nested = is_nested_function;
|
|
314
|
+
|
|
315
|
+
// Bare name → every qualified_name that matches. Top-level definitions
|
|
316
|
+
// only; nested ones go to `nested_names` below, keyed by file.
|
|
266
317
|
let mut name_lookup: HashMap<&str, Vec<&str>> = HashMap::new();
|
|
318
|
+
let mut nested_names: HashMap<&str, HashMap<&str, Vec<&str>>> = HashMap::new();
|
|
267
319
|
for fn_info in functions {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
320
|
+
let target = if is_nested(fn_info) {
|
|
321
|
+
nested_names
|
|
322
|
+
.entry(fn_info.file_path.as_str())
|
|
323
|
+
.or_default()
|
|
324
|
+
.entry(fn_info.name.as_str())
|
|
325
|
+
.or_default()
|
|
326
|
+
} else {
|
|
327
|
+
name_lookup.entry(fn_info.name.as_str()).or_default()
|
|
328
|
+
};
|
|
329
|
+
target.push(fn_info.qualified_name.as_str());
|
|
272
330
|
}
|
|
273
331
|
// Exact qualified call text → one ordinary function or every overload in
|
|
274
332
|
// that same-scope group. Parser call sites keep the source-level base ID,
|
|
275
333
|
// while graph identities carry a signature discriminator.
|
|
276
334
|
let mut qualified_lookup: HashMap<&str, Vec<&str>> = HashMap::new();
|
|
335
|
+
let mut nested_qualified: HashMap<&str, HashMap<&str, Vec<&str>>> = HashMap::new();
|
|
277
336
|
for function in functions {
|
|
278
337
|
let qualified_name = function.qualified_name.as_str();
|
|
279
|
-
|
|
280
|
-
.entry(qualified_name)
|
|
281
|
-
.or_default()
|
|
282
|
-
.push(qualified_name);
|
|
338
|
+
let mut keys: Vec<&str> = vec![qualified_name];
|
|
283
339
|
if let Some(base) = super::overload_base_qualified_name(qualified_name) {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
340
|
+
keys.push(base);
|
|
341
|
+
}
|
|
342
|
+
for key in keys {
|
|
343
|
+
if is_nested(function) {
|
|
344
|
+
nested_qualified
|
|
345
|
+
.entry(function.file_path.as_str())
|
|
346
|
+
.or_default()
|
|
347
|
+
.entry(key)
|
|
348
|
+
.or_default()
|
|
349
|
+
.push(qualified_name);
|
|
350
|
+
} else {
|
|
351
|
+
qualified_lookup
|
|
352
|
+
.entry(key)
|
|
353
|
+
.or_default()
|
|
354
|
+
.push(qualified_name);
|
|
355
|
+
}
|
|
288
356
|
}
|
|
289
357
|
}
|
|
290
358
|
|
|
@@ -348,6 +416,10 @@ pub fn build_call_edges(
|
|
|
348
416
|
let caller_prefix = qname_to_prefix.get(caller_qn).copied();
|
|
349
417
|
let caller_owner = qname_to_owner.get(caller_qn).copied();
|
|
350
418
|
let caller_file = fn_info.file_path.as_str();
|
|
419
|
+
// D3: the nested definitions this caller may see — its own file's,
|
|
420
|
+
// and nothing else's.
|
|
421
|
+
let local_names = nested_names.get(caller_file);
|
|
422
|
+
let local_qualified = nested_qualified.get(caller_file);
|
|
351
423
|
|
|
352
424
|
let mut out: Vec<(&str, &str, u32, Resolution, usize)> = Vec::new();
|
|
353
425
|
let mut counts = Counts::default();
|
|
@@ -359,16 +431,22 @@ pub fn build_call_edges(
|
|
|
359
431
|
counts.excluded += 1;
|
|
360
432
|
continue;
|
|
361
433
|
}
|
|
362
|
-
|
|
434
|
+
let mut exact_storage: Vec<&str> = Vec::new();
|
|
435
|
+
let exact: &[&str] = merge_candidates(
|
|
436
|
+
qualified_lookup.get(called_name.as_str()),
|
|
437
|
+
local_qualified.and_then(|m| m.get(called_name.as_str())),
|
|
438
|
+
&mut exact_storage,
|
|
439
|
+
);
|
|
440
|
+
if !exact.is_empty() {
|
|
363
441
|
counts.resolved += 1;
|
|
364
|
-
for &target in
|
|
442
|
+
for &target in exact {
|
|
365
443
|
if target != caller_qn {
|
|
366
444
|
out.push((
|
|
367
445
|
caller_qn,
|
|
368
446
|
target,
|
|
369
447
|
*line,
|
|
370
448
|
Resolution::ExactQualified,
|
|
371
|
-
|
|
449
|
+
exact.len(),
|
|
372
450
|
));
|
|
373
451
|
}
|
|
374
452
|
}
|
|
@@ -379,10 +457,16 @@ pub fn build_call_edges(
|
|
|
379
457
|
None => (None, called_name.as_str()),
|
|
380
458
|
};
|
|
381
459
|
|
|
382
|
-
let
|
|
460
|
+
let mut name_storage: Vec<&str> = Vec::new();
|
|
461
|
+
let candidates: &[&str] = merge_candidates(
|
|
462
|
+
name_lookup.get(method_name),
|
|
463
|
+
local_names.and_then(|m| m.get(method_name)),
|
|
464
|
+
&mut name_storage,
|
|
465
|
+
);
|
|
466
|
+
if candidates.is_empty() {
|
|
383
467
|
counts.no_candidate += 1;
|
|
384
468
|
continue;
|
|
385
|
-
}
|
|
469
|
+
}
|
|
386
470
|
|
|
387
471
|
if candidates.len() == 1 {
|
|
388
472
|
counts.resolved += 1;
|
|
@@ -393,7 +477,7 @@ pub fn build_call_edges(
|
|
|
393
477
|
continue;
|
|
394
478
|
}
|
|
395
479
|
|
|
396
|
-
let mut targets: &[&str] = candidates
|
|
480
|
+
let mut targets: &[&str] = candidates;
|
|
397
481
|
let mut filtered: Vec<&str>;
|
|
398
482
|
|
|
399
483
|
// Tier 0: receiver-type filter. Two sources of hints —
|
|
@@ -682,6 +766,97 @@ mod stats_tests {
|
|
|
682
766
|
rows
|
|
683
767
|
}
|
|
684
768
|
|
|
769
|
+
/// D3: mark a function as closure-scoped, the way the TS scope walk does.
|
|
770
|
+
fn nested(mut f: FunctionInfo, depth: u64) -> FunctionInfo {
|
|
771
|
+
f.metadata
|
|
772
|
+
.insert("nesting_depth".into(), serde_json::json!(depth));
|
|
773
|
+
f
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/// D3 — a `nesting_depth > 0` definition never joins the global bare-name
|
|
777
|
+
/// lookup. Naively merging opencode's ~2 270 nested names into it took
|
|
778
|
+
/// multi-candidate names from 664 to 1 562 and flipped 293 unique names
|
|
779
|
+
/// ambiguous (Phase 1 spike), which is exactly the false-positive class
|
|
780
|
+
/// v0.1.5's resolution metadata exists to keep out.
|
|
781
|
+
#[test]
|
|
782
|
+
fn a_nested_definition_is_invisible_to_a_caller_in_another_file() {
|
|
783
|
+
let functions = vec![
|
|
784
|
+
func("b.consume", "b.ts", &[("helper", 3)]),
|
|
785
|
+
nested(func("a.outer.helper", "a.ts", &[]), 1),
|
|
786
|
+
];
|
|
787
|
+
let (edges, _) = build_call_edges(
|
|
788
|
+
&functions,
|
|
789
|
+
&[],
|
|
790
|
+
&std::collections::HashSet::new(),
|
|
791
|
+
5,
|
|
792
|
+
&[],
|
|
793
|
+
&no_imports(),
|
|
794
|
+
);
|
|
795
|
+
assert!(
|
|
796
|
+
edges.is_empty(),
|
|
797
|
+
"leaked cross-file edges: {:?}",
|
|
798
|
+
edges
|
|
799
|
+
.iter()
|
|
800
|
+
.map(|e| (&e.caller, &e.callee))
|
|
801
|
+
.collect::<Vec<_>>()
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/// …but a caller in its own file resolves it, top-level or nested.
|
|
806
|
+
#[test]
|
|
807
|
+
fn a_nested_definition_resolves_for_callers_in_its_own_file() {
|
|
808
|
+
let functions = vec![
|
|
809
|
+
func("a.outer", "a.ts", &[("helper", 3)]),
|
|
810
|
+
nested(func("a.outer.helper", "a.ts", &[("deeper", 5)]), 1),
|
|
811
|
+
nested(func("a.outer.helper.deeper", "a.ts", &[]), 2),
|
|
812
|
+
];
|
|
813
|
+
let (edges, _) = build_call_edges(
|
|
814
|
+
&functions,
|
|
815
|
+
&[],
|
|
816
|
+
&std::collections::HashSet::new(),
|
|
817
|
+
5,
|
|
818
|
+
&[],
|
|
819
|
+
&no_imports(),
|
|
820
|
+
);
|
|
821
|
+
let mut pairs: Vec<_> = edges
|
|
822
|
+
.iter()
|
|
823
|
+
.map(|e| (e.caller.as_str(), e.callee.as_str()))
|
|
824
|
+
.collect();
|
|
825
|
+
pairs.sort();
|
|
826
|
+
assert_eq!(
|
|
827
|
+
pairs,
|
|
828
|
+
vec![
|
|
829
|
+
("a.outer", "a.outer.helper"),
|
|
830
|
+
("a.outer.helper", "a.outer.helper.deeper"),
|
|
831
|
+
]
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/// The gating is by *name visibility*, not by dropping the node: a
|
|
836
|
+
/// nested definition in one file must not shadow or suppress an
|
|
837
|
+
/// identically named top-level export in another.
|
|
838
|
+
#[test]
|
|
839
|
+
fn a_nested_name_does_not_disturb_the_global_lookup() {
|
|
840
|
+
let functions = vec![
|
|
841
|
+
func("b.consume", "b.ts", &[("helper", 3)]),
|
|
842
|
+
func("c.helper", "c.ts", &[]),
|
|
843
|
+
nested(func("a.outer.helper", "a.ts", &[]), 1),
|
|
844
|
+
];
|
|
845
|
+
let (edges, _) = build_call_edges(
|
|
846
|
+
&functions,
|
|
847
|
+
&[],
|
|
848
|
+
&std::collections::HashSet::new(),
|
|
849
|
+
5,
|
|
850
|
+
&[],
|
|
851
|
+
&no_imports(),
|
|
852
|
+
);
|
|
853
|
+
assert_eq!(
|
|
854
|
+
meta(&edges),
|
|
855
|
+
vec![("c.helper", "unique_name", 1, false)],
|
|
856
|
+
"the cross-file caller must see exactly the top-level `helper`"
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
|
|
685
860
|
#[test]
|
|
686
861
|
fn unique_name_tier_is_labelled_and_counted() {
|
|
687
862
|
let functions = vec![
|
|
@@ -463,7 +463,40 @@ pub(super) fn functions_df(
|
|
|
463
463
|
),
|
|
464
464
|
),
|
|
465
465
|
];
|
|
466
|
-
|
|
466
|
+
// `nesting_depth` — 0 == top level. Written by the parsers only when it
|
|
467
|
+
// is non-zero, so this column appears only in a build that actually
|
|
468
|
+
// contains closure-scoped definitions, and NULL reads as "top level".
|
|
469
|
+
// Together with `parent_scope` below it replaces a Function→Function
|
|
470
|
+
// containment edge, which would have lied about node types: the
|
|
471
|
+
// enclosing scope is very often a `Constant` (an Effect-TS `layer`) or
|
|
472
|
+
// an anonymous literal, not a Function.
|
|
473
|
+
if fns
|
|
474
|
+
.iter()
|
|
475
|
+
.any(|function| function.metadata.contains_key("nesting_depth"))
|
|
476
|
+
{
|
|
477
|
+
columns.push((
|
|
478
|
+
"nesting_depth",
|
|
479
|
+
ColumnType::Int64,
|
|
480
|
+
int_col(
|
|
481
|
+
fns.iter()
|
|
482
|
+
.map(|function| {
|
|
483
|
+
function
|
|
484
|
+
.metadata
|
|
485
|
+
.get("nesting_depth")
|
|
486
|
+
.and_then(|value| value.as_i64())
|
|
487
|
+
})
|
|
488
|
+
.collect(),
|
|
489
|
+
),
|
|
490
|
+
));
|
|
491
|
+
}
|
|
492
|
+
// Sparse string metadata, promoted only when some function in the build
|
|
493
|
+
// actually carries the key — so a corpus without it keeps its exact
|
|
494
|
+
// column set (and therefore its golden digest). `wrapped_by` is the
|
|
495
|
+
// TS/JS factory-unwrap label (`Effect.fn`, `Layer.effect`, `memoize`);
|
|
496
|
+
// `parent_scope` is the qualified name of the nearest named enclosing
|
|
497
|
+
// binding, so containment queries read `WHERE n.parent_scope = '…'`.
|
|
498
|
+
// Without promotion the parser would be writing metadata nothing reads.
|
|
499
|
+
for key in ["symbol_kind", "role_hint", "wrapped_by", "parent_scope"] {
|
|
467
500
|
if fns.iter().any(|function| {
|
|
468
501
|
function
|
|
469
502
|
.metadata
|