diff --git a/AGENTS.md b/AGENTS.md index 1092a786..a9be9cd8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,11 +15,18 @@ Protocol) stdio server. No AI / vector / LLM anywhere in the binary — output i (`.tscn` `uid=`) — byte-for-byte) and `reference/golden/ruby/` (corpus `crates/codegraph-bench/fixtures/ruby/`; guards #1110 Ruby `receiver.method` extraction — instance/class-method Calls, `Const.new` Instantiates, - bare `include` Implements — byte-for-byte) and `reference/golden/cpp/` + bare `include` Implements — byte-for-byte), `reference/golden/python/` (corpus + `crates/codegraph-bench/fixtures/python/`; guards six bare class-as-value + `References` edges — return, assignment RHS, registry pair, call argument, + list literal, and one cross-file unique match — plus tuple-return and bare-method + negatives, byte-for-byte), and `reference/golden/cpp/` (corpus `crates/codegraph-bench/fixtures/cpp/`; guards #1043 C++ class/struct inheritance incl. templated-base stripping byte-for-byte, and retroactively the earlier C++ extraction work). - Regen recipe: `docs/equivalence.md` "Godot fixture" / "Ruby fixture" / "C++ fixture" sections. + The Python cross-file edge is resolved by Gate 3a's unique-name fallback; + import Gate 3b is deliberately unreachable for real Python nodes because the + extractor does not mark them exported. Regen recipe: `docs/equivalence.md` + "Godot fixture" / "Ruby fixture" / "Python fixture" / "C++ fixture" sections. - **node-id formula**: `{kind}:{sha256("{filePath}:{kind}:{name}:{line}").hex[:32]}`; file nodes are the literal `file:{relpath}`; lines are 1-based; paths relative with `/`. - **No AI / vector / LLM crates** — enforced by `scripts/guardrail.sh` (CI gate): diff --git a/crates/codegraph-bench/fixtures/python/consumers.py b/crates/codegraph-bench/fixtures/python/consumers.py new file mode 100644 index 00000000..7ae71631 --- /dev/null +++ b/crates/codegraph-bench/fixtures/python/consumers.py @@ -0,0 +1,5 @@ +from imported_types import ImportedClass + + +def choose_imported(): + return ImportedClass diff --git a/crates/codegraph-bench/fixtures/python/imported_types.py b/crates/codegraph-bench/fixtures/python/imported_types.py new file mode 100644 index 00000000..e411af74 --- /dev/null +++ b/crates/codegraph-bench/fixtures/python/imported_types.py @@ -0,0 +1,2 @@ +class ImportedClass: + pass diff --git a/crates/codegraph-bench/fixtures/python/same_file.py b/crates/codegraph-bench/fixtures/python/same_file.py new file mode 100644 index 00000000..ae55859b --- /dev/null +++ b/crates/codegraph-bench/fixtures/python/same_file.py @@ -0,0 +1,59 @@ +class ReturnClass: + pass + + +class AliasClass: + pass + + +class RegistryClass: + pass + + +class ArgumentClass: + pass + + +class ListClass: + pass + + +class TupleA: + pass + + +class TupleB: + pass + + +class HandlerOwner: + def handler(self): + return None + + +def choose_return(): + return ReturnClass + + +def choose_alias(): + alias = AliasClass + + +def choose_registry(): + registry = {"registry": RegistryClass} + + +def choose_argument(): + register(ArgumentClass) + + +def choose_list(): + values = [ListClass] + + +def choose_tuple(): + return TupleA, TupleB + + +def wire(handler): + register(handler) diff --git a/crates/codegraph-bench/tests/equivalence.rs b/crates/codegraph-bench/tests/equivalence.rs index 74cf2a95..693e7b2b 100644 --- a/crates/codegraph-bench/tests/equivalence.rs +++ b/crates/codegraph-bench/tests/equivalence.rs @@ -110,6 +110,22 @@ fn cpp_db_is_self_equivalent_to_cpp_golden() { assert_equivalent(&cpp_db(), &cpp_golden_dir()).unwrap(); } +#[test] +fn generated_golden_matches_committed_python_fixture() { + let tempdir = TestDir::new("generated-golden-python"); + write_golden(&python_db(), tempdir.path()).unwrap(); + + let expected = load_golden(&python_golden_dir()).unwrap(); + let actual = load_golden(tempdir.path()).unwrap(); + + diff_canonical(&expected, &actual, None).unwrap(); +} + +#[test] +fn python_db_is_self_equivalent_to_python_golden() { + assert_equivalent(&python_db(), &python_golden_dir()).unwrap(); +} + #[test] fn generated_golden_matches_committed_metal_fixture() { // Guards Metal (#1121): `.metal`→cpp mapping + the `[[attribute]]` blank that @@ -364,6 +380,14 @@ fn cpp_golden_dir() -> PathBuf { workspace_root().join("reference/golden/cpp") } +fn python_db() -> PathBuf { + workspace_root().join("reference/golden/python/colby.db") +} + +fn python_golden_dir() -> PathBuf { + workspace_root().join("reference/golden/python") +} + fn metal_db() -> PathBuf { workspace_root().join("reference/golden/metal/colby.db") } diff --git a/crates/codegraph-extract/src/function_ref.rs b/crates/codegraph-extract/src/function_ref.rs index a3c26b4b..23bccd2d 100644 --- a/crates/codegraph-extract/src/function_ref.rs +++ b/crates/codegraph-extract/src/function_ref.rs @@ -161,6 +161,7 @@ const PYTHON_SPEC: FnRefSpec = FnRefSpec { ("keyword_argument", CaptureMode::Value, Some("value")), ("pair", CaptureMode::Value, Some("value")), ("list", CaptureMode::List, None), + ("return_statement", CaptureMode::List, None), ], layers: &[], unwrap: &[], @@ -1221,6 +1222,75 @@ class W: assert!(has_fn_ref(&refs, "handler"), "names={:?}", names(&refs)); } + #[test] + fn python_captures_single_return_value_but_not_tuple_members() { + let src = r#" +def handler(): + return 1 + +def first(): + return 2 + +def second(): + return 3 + +def choose_single(): + return handler + +def choose_tuple(): + return first, second +"#; + let refs = fn_refs("src/returns.py", src, Language::Python); + assert!(has_fn_ref(&refs, "handler"), "names={:?}", names(&refs)); + assert!( + !has_fn_ref(&refs, "first"), + "tuple members must not be captured: names={:?}", + names(&refs) + ); + assert!( + !has_fn_ref(&refs, "second"), + "tuple members must not be captured: names={:?}", + names(&refs) + ); + } + + #[test] + fn python_captures_same_file_classes_in_value_containers() { + let src = r#" +class AliasClass: + pass + +class RegistryClass: + pass + +class ArgumentClass: + pass + +class ListClass: + pass + +def choose_alias(): + alias = AliasClass + +def choose_registry(): + registry = {"registry": RegistryClass} + +def choose_argument(): + register(ArgumentClass) + +def choose_list(): + values = [ListClass] +"#; + let refs = fn_refs("src/classes.py", src, Language::Python); + for name in ["AliasClass", "RegistryClass", "ArgumentClass", "ListClass"] { + assert!( + has_fn_ref(&refs, name), + "missing {name}: names={:?}", + names(&refs) + ); + } + } + // --- Go ---------------------------------------------------------------- #[test] diff --git a/crates/codegraph-extract/src/walker.rs b/crates/codegraph-extract/src/walker.rs index 321e9dbd..e4f9ed92 100644 --- a/crates/codegraph-extract/src/walker.rs +++ b/crates/codegraph-extract/src/walker.rs @@ -3738,10 +3738,13 @@ impl<'a, 'tree> TreeSitterWalker<'a, 'tree> { } /// Gate captured function-value candidates and push survivors as - /// `function_ref` references. A bare-name candidate survives only if its - /// name is a function/method DEFINED in this file or an imported name; - /// `this.` candidates always flush (class-scoped at resolution). - /// Ports `flushFnRefCandidates` (tree-sitter.ts:429-521), TS/JS gate. + /// `function_ref` references. `this.` and `::` candidates always + /// flush. C-family file-scope candidates in an ungated mode bypass the name + /// gate, as do `skip_gate` candidates (PHP HOF strings). Every other bare + /// name must be a function/method defined in this file, a Python class + /// defined in this file, or an imported name. + /// Ports `flushFnRefCandidates` (tree-sitter.ts:429-521) from the upstream + /// TS/JS gate, adapted here across languages. fn flush_fn_ref_candidates(&mut self) { if self.fn_ref_candidates.is_empty() { return; @@ -3754,7 +3757,9 @@ impl<'a, 'tree> TreeSitterWalker<'a, 'tree> { let mut defined_here: std::collections::HashSet<&str> = std::collections::HashSet::new(); for n in &self.nodes { - if matches!(n.kind, NodeKind::Function | NodeKind::Method) { + if matches!(n.kind, NodeKind::Function | NodeKind::Method) + || (self.spec.language() == Language::Python && n.kind == NodeKind::Class) + { defined_here.insert(n.name.as_str()); } } @@ -3800,8 +3805,8 @@ impl<'a, 'tree> TreeSitterWalker<'a, 'tree> { } // Gate by candidate shape: `this.`/`::` always flush; C-family // file-scope initializers (ungated_modes) skip; PHP HOF strings - // (skip_gate) skip; everything else must be a same-file fn/method - // or an import (tree-sitter.ts:493-512). + // (skip_gate) skip; everything else must be a same-file fn/method, + // a same-file Python class, or an import (tree-sitter.ts:493-512). if !cand.name.starts_with("this.") && !cand.name.contains("::") { let skip_gate = (at_file_scope && crate::function_ref::mode_is_ungated(spec, cand.mode)) diff --git a/crates/codegraph-resolve/src/name_matcher.rs b/crates/codegraph-resolve/src/name_matcher.rs index 9e797a1e..d3adcf85 100644 --- a/crates/codegraph-resolve/src/name_matcher.rs +++ b/crates/codegraph-resolve/src/name_matcher.rs @@ -2020,11 +2020,15 @@ pub fn match_reference( match_fuzzy(reference, context) } +pub(crate) fn is_python_class_function_ref_target(language: Language, kind: NodeKind) -> bool { + language == Language::Python && kind == NodeKind::Class +} + /// Resolve a `function_ref` (callback-as-value) reference: exact name, -/// function/method targets only, same language family, same-file first, -/// cross-file only when unique. No fuzzy fallback. `this.` refs are -/// resolved elsewhere (resolve_this_member_fn_ref). Ports `matchFunctionRef` -/// (name-matcher.ts:179-310). +/// function/method targets plus Python class targets (Python bare methods remain +/// excluded), same language family, same-file first, cross-file only when unique. +/// No fuzzy fallback. `this.` refs are resolved elsewhere +/// (resolve_this_member_fn_ref). Ports `matchFunctionRef` (name-matcher.ts:179-310). pub fn match_function_ref( reference: &RefView, context: &dyn ResolutionContext, @@ -2033,8 +2037,9 @@ pub fn match_function_ref( return None; } - // A bare identifier can never be a method value in JS/TS/C++/Python/PHP - // (methods need a receiver), so those match FUNCTIONS only. + // A bare identifier cannot be a method value in JS/TS/C++/Python/PHP + // (methods need a receiver). Those match FUNCTIONS; Python additionally + // accepts CLASS targets through the shared predicate, but still not METHODS. let bare_fn_only = matches!( reference.language, Language::TypeScript @@ -2093,7 +2098,9 @@ pub fn match_function_ref( .get_nodes_by_name(&reference.reference_name) .into_iter() .filter(|n| { - (n.kind == NodeKind::Function || (!bare_fn_only && n.kind == NodeKind::Method)) + (n.kind == NodeKind::Function + || (!bare_fn_only && n.kind == NodeKind::Method) + || is_python_class_function_ref_target(reference.language, n.kind)) && same_language_family(n.language, reference.language) && n.id != reference.from_node_id }) @@ -3800,6 +3807,47 @@ mod tests { assert!(match_function_ref(&r, &ctx).is_none()); } + #[test] + fn function_ref_bare_class_target_matrix() { + let cases = [ + (Language::TypeScript, NodeKind::Class, false), + (Language::Tsx, NodeKind::Class, false), + (Language::JavaScript, NodeKind::Class, false), + (Language::Jsx, NodeKind::Class, false), + (Language::Cpp, NodeKind::Class, false), + (Language::Php, NodeKind::Class, false), + (Language::Python, NodeKind::Method, false), + (Language::Python, NodeKind::Class, true), + ]; + + for (language, kind, should_resolve) in cases { + let target = mk( + "target:id", + kind, + "Target", + "Target", + "src/target", + language, + ); + let ctx = Ctx::default().name("Target", vec![target]); + let r = refv("Target", EdgeKind::References, "src/consumer", language, 1); + let resolved = match_function_ref(&r, &ctx); + + if should_resolve { + let resolved = resolved.unwrap_or_else(|| { + panic!("{language:?} {kind:?} must be a function-ref target") + }); + assert_eq!(resolved.target_node_id, "target:id"); + assert_eq!(resolved.resolved_by, ResolvedBy::FunctionRef); + } else { + assert!( + resolved.is_none(), + "{language:?} {kind:?} must not be a function-ref target" + ); + } + } + } + #[test] fn function_ref_qualified_member_pointer_unique() { // C++ `&Widget::on_click` — resolve the member on that scope. diff --git a/crates/codegraph-resolve/src/resolver.rs b/crates/codegraph-resolve/src/resolver.rs index 969a96f4..1ffbdfd4 100644 --- a/crates/codegraph-resolve/src/resolver.rs +++ b/crates/codegraph-resolve/src/resolver.rs @@ -10,9 +10,9 @@ use crate::framework::FrameworkResolver; use crate::import_resolver::{is_php_include_path_ref, resolve_jvm_import, resolve_via_import}; use crate::name_matcher::{ - crosses_known_family, is_php_property_receiver_shape, match_dotted_call_chain, - match_function_ref, match_method_call, match_reference, match_scoped_call_chain, - same_language_family, + crosses_known_family, is_php_property_receiver_shape, is_python_class_function_ref_target, + match_dotted_call_chain, match_function_ref, match_method_call, match_reference, + match_scoped_call_chain, same_language_family, }; use crate::snapshot_context::{SnapshotResolutionContext, build_edge_adjacency}; use crate::types::{ @@ -980,7 +980,9 @@ impl ReferenceResolver { self.gate_language(resolve_via_import(reference, context), reference, context) { if let Some(target) = context.get_node_by_id(&via_import.target_node_id) { - if matches!(target.kind, NodeKind::Function | NodeKind::Method) { + if matches!(target.kind, NodeKind::Function | NodeKind::Method) + || is_python_class_function_ref_target(reference.language, target.kind) + { return (Some(via_import), None); } } @@ -3087,6 +3089,85 @@ mod tests { assert_eq!(resolved.target_node_id, onblur.id); } + // Synthetic kind-gate coverage only: this test hand-sets `is_exported = true` + // on Python class nodes, a state the extractor never produces because Python + // inherits `LanguageSpec::is_exported`'s `false` (`spec.rs:106`). Real Python + // indexing cannot reach this import path: `find_exported_symbol` requires + // `is_exported` at `import_resolver.rs:1476`, `:1489`, and `:1496`. This proves + // only that the upstream kind gate and shared B1 helper are wired; it must not + // be cited as golden, hands-on QA, or `ResolvedBy::Import` acceptance evidence + // for a real Python import. + #[test] + fn resolve_one_function_ref_imported_python_class_synthetic_kind_gate_accepts_python_class() { + let root = temp_db("fnref-imported-python-class").with_extension("project"); + std::fs::create_dir_all(&root).expect("create project"); + std::fs::write(root.join("models.py"), "class ImportedClass:\n pass\n") + .expect("write imported class"); + std::fs::write(root.join("decoy.py"), "class ImportedClass:\n pass\n") + .expect("write decoy class"); + std::fs::write( + root.join("consumer.py"), + "from models import ImportedClass\n\ndef choose():\n return ImportedClass\n", + ) + .expect("write consumer"); + + let mut store = Store::open(&root.join("codegraph.db")).expect("open"); + let mut imported = mk_node2( + "class:imported", + NodeKind::Class, + "ImportedClass", + "models.py", + Language::Python, + ); + imported.is_exported = true; + let mut decoy = mk_node2( + "class:decoy", + NodeKind::Class, + "ImportedClass", + "decoy.py", + Language::Python, + ); + decoy.is_exported = true; + let caller = mk_node2( + "function:choose", + NodeKind::Function, + "choose", + "consumer.py", + Language::Python, + ); + store + .upsert_nodes(&[imported.clone(), decoy, caller.clone()]) + .expect("nodes"); + + let project_root = root.to_string_lossy().into_owned(); + let mut resolver = ReferenceResolver::new(project_root.clone()); + resolver.warm_caches(&crate::context::StoreResolutionContext::new( + &store, + project_root.clone(), + )); + let ctx = crate::context::StoreResolutionContext::new(&store, project_root); + let reference = RefView { + row_id: None, + from_node_id: caller.id, + reference_name: "ImportedClass".to_string(), + reference_kind: EdgeKind::References, + line: 4, + column: 11, + file_path: "consumer.py".to_string(), + language: Language::Python, + is_function_ref: true, + reference_subkind: None, + }; + let resolved = resolver.resolve_one(&reference, &ctx); + drop(ctx); + drop(store); + std::fs::remove_dir_all(&root).expect("remove project"); + + let resolved = resolved.expect("imported Python class resolves"); + assert_eq!(resolved.target_node_id, imported.id); + assert_eq!(resolved.resolved_by, ResolvedBy::Import); + } + #[test] fn helper_stubs_are_exercised() { // Drives the test-only ReactLike / Universal / MinimalCtx stub methods so diff --git a/crates/codegraph-store/src/index_state.rs b/crates/codegraph-store/src/index_state.rs index cf35083a..b29cbbc4 100644 --- a/crates/codegraph-store/src/index_state.rs +++ b/crates/codegraph-store/src/index_state.rs @@ -26,7 +26,7 @@ //! //! This classifier boundary is narrower than the commit series containing it: //! existing CLI indexing still writes `project_metadata`, but now takes that -//! metadata key and extraction version (`2`) from this store-owned module. That +//! metadata key and extraction version (`3`) from this store-owned module. That //! pre-existing DB stamping is not state-slot publication and is not performed by //! this classifier. //! @@ -44,7 +44,7 @@ //! { //! "sequence": 7, //! "storageProtocol": 2, -//! "extractionVersion": 2, +//! "extractionVersion": 3, //! "phase": "current", //! "projectIdentity": "<64 lowercase hex>", //! "checksum": "<64 lowercase hex>" @@ -115,7 +115,7 @@ pub const CURRENT_STORAGE_PROTOCOL: u64 = 2; /// The extraction-pipeline version this binary produces. This is the single /// source of truth: no other crate may define its own copy. -pub const CURRENT_EXTRACTION_VERSION: u64 = 2; +pub const CURRENT_EXTRACTION_VERSION: u64 = 3; /// The `project_metadata` key under which a built index records the extraction /// version it was produced with. Single source of truth for the key spelling. diff --git a/crates/codegraph-store/tests/index_state.rs b/crates/codegraph-store/tests/index_state.rs index 909502e2..0b48cd9a 100644 --- a/crates/codegraph-store/tests/index_state.rs +++ b/crates/codegraph-store/tests/index_state.rs @@ -118,16 +118,16 @@ fn assert_corrupt( #[test] fn index_state_constants_and_canonical_payload_are_exact() { assert_eq!(CURRENT_STORAGE_PROTOCOL, 2); - assert_eq!(CURRENT_EXTRACTION_VERSION, 2); + assert_eq!(CURRENT_EXTRACTION_VERSION, 3); assert_eq!(EXTRACTION_VERSION_KEY, "indexed_with_extraction_version"); assert_eq!( - canonical_checksum_payload(7, 2, 2, "future-phase", OWNER), + canonical_checksum_payload(7, 2, 3, "future-phase", OWNER), format!( "codegraph-index-state-v1\nsequence=7\nstorageProtocol=2\n\ - extractionVersion=2\nphase=future-phase\nprojectIdentity={OWNER}\n" + extractionVersion=3\nphase=future-phase\nprojectIdentity={OWNER}\n" ) ); - let digest = checksum_hex(7, 2, 2, "future-phase", OWNER); + let digest = checksum_hex(7, 2, 3, "future-phase", OWNER); assert_eq!(digest.len(), 64); assert!( digest @@ -176,6 +176,7 @@ fn index_state_current_protocol_maps_each_phase() { #[test] fn index_state_extraction_version_maps_outdated_and_future_for_every_phase() { + let future_extraction = CURRENT_EXTRACTION_VERSION + 1; for phase in ["building", "current", "uninitialized"] { let old = TempTree::new("outdated"); write_wire( @@ -191,18 +192,34 @@ fn index_state_extraction_version_maps_outdated_and_future_for_every_phase() { &ExtractionStatus::Outdated { built: 1 } ); + let previous = TempTree::new("previous-extraction"); + write_wire( + &previous.slots()[0], + 1, + CURRENT_STORAGE_PROTOCOL, + 2, + phase, + OWNER, + ); + assert_eq!( + classify_slots(&previous.slots(), OWNER).status(), + &ExtractionStatus::Outdated { built: 2 } + ); + let future = TempTree::new("future-extraction"); write_wire( &future.slots()[0], 1, CURRENT_STORAGE_PROTOCOL, - 3, + future_extraction, phase, OWNER, ); assert_eq!( classify_slots(&future.slots(), OWNER).status(), - &ExtractionStatus::Future { built: 3 }, + &ExtractionStatus::Future { + built: future_extraction + }, "future extraction must dominate phase {phase}" ); } @@ -211,12 +228,18 @@ fn index_state_extraction_version_maps_outdated_and_future_for_every_phase() { #[test] fn index_state_unknown_fields_key_order_and_whitespace_are_ignored() { let tree = TempTree::new("formatting"); - let checksum = checksum_hex(9, 2, 2, "current", OWNER); + let checksum = checksum_hex( + 9, + CURRENT_STORAGE_PROTOCOL, + CURRENT_EXTRACTION_VERSION, + "current", + OWNER, + ); let formatted = format!( "{{\n \"unknownFutureField\": {{\"nested\": true}},\n \ \"checksum\": \"{checksum}\", \"projectIdentity\": \"{OWNER}\",\n \ - \"phase\": \"current\", \"extractionVersion\": 2,\n \ - \"storageProtocol\": 2, \"sequence\": 9\n}}\n" + \"phase\": \"current\", \"extractionVersion\": {CURRENT_EXTRACTION_VERSION},\n \ + \"storageProtocol\": {CURRENT_STORAGE_PROTOCOL}, \"sequence\": 9\n}}\n" ); write_bytes(&tree.slots()[1], formatted.as_bytes()); let result = classify_slots(&tree.slots(), OWNER); @@ -228,16 +251,16 @@ fn index_state_unknown_fields_key_order_and_whitespace_are_ignored() { fn index_state_missing_and_wrong_typed_required_fields_are_malformed() { for value in [ json!({ - "storageProtocol": 2, - "extractionVersion": 2, + "storageProtocol": CURRENT_STORAGE_PROTOCOL, + "extractionVersion": CURRENT_EXTRACTION_VERSION, "phase": "current", "projectIdentity": OWNER, "checksum": "0".repeat(64), }), json!({ "sequence": "1", - "storageProtocol": 2, - "extractionVersion": 2, + "storageProtocol": CURRENT_STORAGE_PROTOCOL, + "extractionVersion": CURRENT_EXTRACTION_VERSION, "phase": "current", "projectIdentity": OWNER, "checksum": "0".repeat(64), @@ -261,7 +284,14 @@ fn index_state_bad_owner_and_checksum_shapes_are_typed_corruption() { ]; for owner in bad_owners { let tree = TempTree::new("bad-owner-shape"); - write_wire(&tree.slots()[0], 1, 3, 2, "future-phase", owner); + write_wire( + &tree.slots()[0], + 1, + 3, + CURRENT_EXTRACTION_VERSION, + "future-phase", + owner, + ); let result = classify_unchanged(tree.path(), || classify_slots(&tree.slots(), OWNER)); assert_corrupt(&result, |reason| { matches!(reason, CorruptReason::InvalidOwnerEncoding { .. }) @@ -270,7 +300,7 @@ fn index_state_bad_owner_and_checksum_shapes_are_typed_corruption() { for checksum in ["abc".to_string(), "A".repeat(64), "g".repeat(64)] { let tree = TempTree::new("bad-checksum-shape"); - let mut value = wire_value(1, 3, 2, "future-phase", OWNER); + let mut value = wire_value(1, 3, CURRENT_EXTRACTION_VERSION, "future-phase", OWNER); value["checksum"] = Value::String(checksum); write_bytes(&tree.slots()[0], &serde_json::to_vec(&value).unwrap()); let result = classify_slots(&tree.slots(), OWNER); @@ -283,7 +313,7 @@ fn index_state_bad_owner_and_checksum_shapes_are_typed_corruption() { #[test] fn index_state_checksum_and_owner_mismatch_are_corrupt_even_for_future_protocol() { let checksum_tree = TempTree::new("checksum-mismatch"); - let mut value = wire_value(1, 3, 2, "future-phase", OWNER); + let mut value = wire_value(1, 3, CURRENT_EXTRACTION_VERSION, "future-phase", OWNER); value["checksum"] = Value::String("0".repeat(64)); write_bytes( &checksum_tree.slots()[0], @@ -297,7 +327,14 @@ fn index_state_checksum_and_owner_mismatch_are_corrupt_even_for_future_protocol( }); let owner_tree = TempTree::new("owner-mismatch"); - write_wire(&owner_tree.slots()[0], 1, 3, 2, "future-phase", OTHER_OWNER); + write_wire( + &owner_tree.slots()[0], + 1, + 3, + CURRENT_EXTRACTION_VERSION, + "future-phase", + OTHER_OWNER, + ); let result = classify_unchanged(owner_tree.path(), || { classify_slots(&owner_tree.slots(), OWNER) }); @@ -309,7 +346,14 @@ fn index_state_checksum_and_owner_mismatch_are_corrupt_even_for_future_protocol( #[test] fn index_state_current_unknown_phase_is_corrupt_but_future_unknown_phase_is_valid() { let current = TempTree::new("unknown-current-phase"); - write_wire(¤t.slots()[0], 1, 2, 2, "future-phase", OWNER); + write_wire( + ¤t.slots()[0], + 1, + CURRENT_STORAGE_PROTOCOL, + CURRENT_EXTRACTION_VERSION, + "future-phase", + OWNER, + ); let result = classify_slots(¤t.slots(), OWNER); assert_corrupt( &result, @@ -330,7 +374,14 @@ fn index_state_current_unknown_phase_is_corrupt_but_future_unknown_phase_is_vali fn index_state_lower_and_zero_storage_protocol_are_corrupt() { for protocol in [0, 1] { let tree = TempTree::new("lower-protocol"); - write_wire(&tree.slots()[0], 1, protocol, 2, "current", OWNER); + write_wire( + &tree.slots()[0], + 1, + protocol, + CURRENT_EXTRACTION_VERSION, + "current", + OWNER, + ); let result = classify_slots(&tree.slots(), OWNER); assert_corrupt( &result, @@ -343,7 +394,14 @@ fn index_state_lower_and_zero_storage_protocol_are_corrupt() { fn index_state_any_malformed_present_slot_dominates_a_valid_companion() { let tree = TempTree::new("invalid-dominance"); write_bytes(&tree.slots()[0], b"not json"); - write_wire(&tree.slots()[1], 99, 2, 2, "current", OWNER); + write_wire( + &tree.slots()[1], + 99, + CURRENT_STORAGE_PROTOCOL, + CURRENT_EXTRACTION_VERSION, + "current", + OWNER, + ); let result = classify_unchanged(tree.path(), || classify_slots(&tree.slots(), OWNER)); assert_corrupt(&result, |reason| { matches!(reason, CorruptReason::MalformedJson { slot: 0, .. }) @@ -373,7 +431,14 @@ fn index_state_non_regular_and_unreadable_slots_are_corrupt() { use std::os::unix::fs::symlink; let link = TempTree::new("slot-symlink"); let target = link.path().join("target.json"); - write_wire(&target, 1, 2, 2, "current", OWNER); + write_wire( + &target, + 1, + CURRENT_STORAGE_PROTOCOL, + CURRENT_EXTRACTION_VERSION, + "current", + OWNER, + ); symlink(&target, &link.slots()[0]).unwrap(); let result = classify_unchanged(link.path(), || classify_slots(&link.slots(), OWNER)); assert_corrupt(&result, |reason| { @@ -399,7 +464,14 @@ fn index_state_non_regular_and_unreadable_slots_are_corrupt() { fn index_state_future_protocol_dominates_current_in_both_sequence_directions() { for (future_sequence, current_sequence) in [(1, 100), (100, 1)] { let tree = TempTree::new("future-dominance"); - write_wire(&tree.slots()[0], current_sequence, 2, 2, "current", OWNER); + write_wire( + &tree.slots()[0], + current_sequence, + CURRENT_STORAGE_PROTOCOL, + CURRENT_EXTRACTION_VERSION, + "current", + OWNER, + ); write_wire( &tree.slots()[1], future_sequence, @@ -417,7 +489,13 @@ fn index_state_future_protocol_dominates_current_in_both_sequence_directions() { #[test] fn index_state_equal_current_sequences_are_corrupt_for_identical_and_reformatted_json() { let identical = TempTree::new("equal-identical"); - let bytes = wire_bytes(7, 2, 2, "current", OWNER); + let bytes = wire_bytes( + 7, + CURRENT_STORAGE_PROTOCOL, + CURRENT_EXTRACTION_VERSION, + "current", + OWNER, + ); write_bytes(&identical.slots()[0], &bytes); write_bytes(&identical.slots()[1], &bytes); let result = classify_unchanged(identical.path(), || { @@ -435,11 +513,18 @@ fn index_state_equal_current_sequences_are_corrupt_for_identical_and_reformatted let reformatted = TempTree::new("equal-reformatted"); write_bytes(&reformatted.slots()[0], &bytes); - let checksum = checksum_hex(7, 2, 2, "current", OWNER); + let checksum = checksum_hex( + 7, + CURRENT_STORAGE_PROTOCOL, + CURRENT_EXTRACTION_VERSION, + "current", + OWNER, + ); let other = format!( "{{ \"checksum\":\"{checksum}\", \"phase\":\"current\", \ - \"projectIdentity\":\"{OWNER}\", \"extractionVersion\":2, \ - \"storageProtocol\":2, \"sequence\":7 }}" + \"projectIdentity\":\"{OWNER}\", \ + \"extractionVersion\":{CURRENT_EXTRACTION_VERSION}, \ + \"storageProtocol\":{CURRENT_STORAGE_PROTOCOL}, \"sequence\":7 }}" ); write_bytes(&reformatted.slots()[1], other.as_bytes()); let result = classify_unchanged(reformatted.path(), || { @@ -467,7 +552,14 @@ fn index_state_equal_future_and_mixed_sequences_are_corrupt_before_future_domina }); let mixed = TempTree::new("equal-mixed"); - write_wire(&mixed.slots()[0], 8, 2, 2, "current", OWNER); + write_wire( + &mixed.slots()[0], + 8, + CURRENT_STORAGE_PROTOCOL, + CURRENT_EXTRACTION_VERSION, + "current", + OWNER, + ); write_wire(&mixed.slots()[1], 8, 3, 10, "future-phase", OWNER); let result = classify_unchanged(mixed.path(), || classify_slots(&mixed.slots(), OWNER)); assert_corrupt(&result, |reason| { @@ -477,7 +569,14 @@ fn index_state_equal_future_and_mixed_sequences_are_corrupt_before_future_domina #[test] fn index_state_selected_max_sequence_is_corrupt_before_status_mapping() { - for (protocol, extraction, phase) in [(2, 2, "current"), (3, 99, "future-phase")] { + for (protocol, extraction, phase) in [ + ( + CURRENT_STORAGE_PROTOCOL, + CURRENT_EXTRACTION_VERSION, + "current", + ), + (3, 99, "future-phase"), + ] { let tree = TempTree::new("max-sequence"); write_wire( &tree.slots()[0], @@ -506,8 +605,22 @@ fn index_state_selected_max_sequence_is_corrupt_before_status_mapping() { #[test] fn index_state_highest_ordinary_current_slot_is_authoritative_with_full_metadata() { let tree = TempTree::new("authority"); - write_wire(&tree.slots()[0], 4, 2, 1, "building", OWNER); - write_wire(&tree.slots()[1], 5, 2, 2, "current", OWNER); + write_wire( + &tree.slots()[0], + 4, + CURRENT_STORAGE_PROTOCOL, + 1, + "building", + OWNER, + ); + write_wire( + &tree.slots()[1], + 5, + CURRENT_STORAGE_PROTOCOL, + CURRENT_EXTRACTION_VERSION, + "current", + OWNER, + ); let result = classify_unchanged(tree.path(), || classify_slots(&tree.slots(), OWNER)); assert_eq!(result.status(), &ExtractionStatus::Current); let authority = result.authoritative().expect("authority"); @@ -515,13 +628,22 @@ fn index_state_highest_ordinary_current_slot_is_authoritative_with_full_metadata assert_eq!(authority.path, tree.slots()[1]); assert_eq!(authority.record.sequence, 5); assert_eq!(authority.record.storage_protocol, 2); - assert_eq!(authority.record.extraction_version, 2); + assert_eq!( + authority.record.extraction_version, + CURRENT_EXTRACTION_VERSION + ); assert_eq!(authority.record.phase, Some(StatePhase::Current)); assert_eq!(authority.record.phase_raw, "current"); assert_eq!(authority.record.project_identity, OWNER); assert_eq!( authority.record.checksum, - checksum_hex(5, 2, 2, "current", OWNER) + checksum_hex( + 5, + CURRENT_STORAGE_PROTOCOL, + CURRENT_EXTRACTION_VERSION, + "current", + OWNER + ) ); } diff --git a/docs/equivalence.md b/docs/equivalence.md index 316e030f..f4d8fdf6 100644 --- a/docs/equivalence.md +++ b/docs/equivalence.md @@ -258,6 +258,56 @@ Like the Godot fixture, both the index and the dump are byte-stable, and the `upstream_db_is_self_equivalent_to_ruby_golden` tests in `crates/codegraph-bench/tests/equivalence.rs` enforce it. +### Python fixture + +The dedicated `reference/golden/python/` fixture guards Python bare +class-as-value function references without changing the shared `mini` corpus. +Its three-file source corpus lives at `crates/codegraph-bench/fixtures/python/` +and pins six positive `References` edges: + +- same-file class values in a direct return, assignment RHS, registry-pair + value, call argument, and list literal; +- one cross-file `ImportedClass` value. The import syntax is present, but real + Python nodes are not marked exported, so import Gate 3b is unreachable. This + edge is intentionally resolved by Gate 3a's unique cross-file name match at + confidence 0.8. + +It also pins two negative boundaries: a tuple return does not recurse into +`TupleA`/`TupleB`, and a bare `handler` parameter remains an unresolved +`function_ref` rather than resolving to a same-named method. The undefined +`register(...)` calls used by the argument and method shapes legitimately remain +as two unresolved `calls` rows. + +Regenerate the committed database and canonical artifacts from a clean corpus: + +```bash +# 1. Copy the corpus to a clean directory (keeps the workspace index out of it). +rm -rf /tmp/cg-fixture-python +cp -r crates/codegraph-bench/fixtures/python /tmp/cg-fixture-python + +# 2. Index it with OUR release binary (never hand-write the golden). +cargo build --release -p codegraph-rs +CODEGRAPH_NO_DAEMON=1 CODEGRAPH_NO_WATCH=1 \ + ./target/release/codegraph init /tmp/cg-fixture-python + +# 3. Commit the produced database as the fixture's colby.db. +mkdir -p reference/golden/python +cp /tmp/cg-fixture-python/.codegraph-v2/codegraph.db reference/golden/python/colby.db + +# 4. Dump canonical JSON + schema from that exact database. +cargo run -p codegraph-bench --bin bench -- \ + --gen-golden reference/golden/python/colby.db reference/golden/python +``` + +As with every fixture, compare only `nodes.json`, `edges.json`, `refs.json`, +`files.json`, and `schema.sql` byte-for-byte. `colby.db` itself is not a +byte-reproducibility contract because SQLite updates its header change counter. +Regenerate `schema.sql` from the database being committed; statement ordering +may differ between binary versions, but its normalized statement set must not. +The tests `generated_golden_matches_committed_python_fixture` and +`python_db_is_self_equivalent_to_python_golden` enforce database/artifact +self-equivalence. + ### C++ fixture A fourth golden fixture, `reference/golden/cpp/`, guards C++ `base_class_clause` diff --git a/docs/languages.md b/docs/languages.md index f582dd5f..d548e7b9 100644 --- a/docs/languages.md +++ b/docs/languages.md @@ -27,7 +27,7 @@ level. | JavaScript | `.js` `.mjs` `.cjs` `.xsjs` `.xsjslib` | Full tree-sitter | | | JSX | `.jsx` | Full tree-sitter | JavaScript grammar, JSX syntax | | ArkTS | `.ets` | Full tree-sitter | HarmonyOS / OpenHarmony; `tree-sitter-arkts` grammar. `@Component struct` → struct symbol. ArkUI dynamic-dispatch bridges deferred. Plain `.ts` stays TypeScript | -| Python | `.py` `.pyw` | Full tree-sitter | | +| Python | `.py` `.pyw` | Full tree-sitter | Bare class names used as values resolve to `References` edges in return, assignment, pair, argument, and list positions. Tuple returns are not recursively traversed, and bare names never resolve to methods; methods require a receiver. | | Go | `.go` | Full tree-sitter | | | Rust | `.rs` | Full tree-sitter | | | Java | `.java` | Full tree-sitter | | diff --git a/reference/golden/python/colby.db b/reference/golden/python/colby.db new file mode 100644 index 00000000..a0826bdf Binary files /dev/null and b/reference/golden/python/colby.db differ diff --git a/reference/golden/python/edges.json b/reference/golden/python/edges.json new file mode 100644 index 00000000..e2c711b9 --- /dev/null +++ b/reference/golden/python/edges.json @@ -0,0 +1,275 @@ +[ + { + "col": 0, + "kind": "imports", + "line": 1, + "metadata": { + "confidence": 0.9, + "resolvedBy": "exact-match" + }, + "provenance": null, + "source": "file:consumers.py", + "target": "import:3e3fb8b0ff507a934daf33d767cfbcbe" + }, + { + "col": 11, + "kind": "references", + "line": 35, + "metadata": { + "confidence": 0.95, + "fnRef": true, + "resolvedBy": "function-ref" + }, + "provenance": null, + "source": "function:7fe38b2a101736abde38fef885862694", + "target": "class:4c9c90936185ce0ba79fff80c167872e" + }, + { + "col": 11, + "kind": "references", + "line": 5, + "metadata": { + "confidence": 0.8, + "fnRef": true, + "resolvedBy": "function-ref" + }, + "provenance": null, + "source": "function:230b5585efc925330d3cbf214e8be03b", + "target": "class:099ebd3f5c7b1a6ac5522e9fbfb00912" + }, + { + "col": 12, + "kind": "references", + "line": 39, + "metadata": { + "confidence": 0.95, + "fnRef": true, + "resolvedBy": "function-ref" + }, + "provenance": null, + "source": "function:f216f97a7c79cba22f0cf1bcb01d4ff4", + "target": "class:2ba663f8546d3fb02b843c54dd4ab265" + }, + { + "col": 13, + "kind": "references", + "line": 47, + "metadata": { + "confidence": 0.95, + "fnRef": true, + "resolvedBy": "function-ref" + }, + "provenance": null, + "source": "function:8ab91cb55b890abcf7c58c5a8c532661", + "target": "class:73d50236f18650b7d0a250d4ae90cf3c" + }, + { + "col": 14, + "kind": "references", + "line": 51, + "metadata": { + "confidence": 0.95, + "fnRef": true, + "resolvedBy": "function-ref" + }, + "provenance": null, + "source": "function:5e720163b2a45565e1d981792fd7ecb7", + "target": "class:02ea0f42e300fcc1605594c67cde3536" + }, + { + "col": 27, + "kind": "imports", + "line": 1, + "metadata": { + "confidence": 0.9, + "resolvedBy": "exact-match" + }, + "provenance": null, + "source": "file:consumers.py", + "target": "class:099ebd3f5c7b1a6ac5522e9fbfb00912" + }, + { + "col": 28, + "kind": "references", + "line": 43, + "metadata": { + "confidence": 0.95, + "fnRef": true, + "resolvedBy": "function-ref" + }, + "provenance": null, + "source": "function:1198b6fc92ac818287e950fa1a067eda", + "target": "class:f6b91632a5d8b5435b08ac99b12f0570" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "class:980443e74cab7da71208d2a972b5a8d9", + "target": "method:8ac33b9d670a290f5c7fc3d5e77b79be" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:consumers.py", + "target": "function:230b5585efc925330d3cbf214e8be03b" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:consumers.py", + "target": "import:3e3fb8b0ff507a934daf33d767cfbcbe" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:imported_types.py", + "target": "class:099ebd3f5c7b1a6ac5522e9fbfb00912" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:same_file.py", + "target": "class:02ea0f42e300fcc1605594c67cde3536" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:same_file.py", + "target": "class:2ba663f8546d3fb02b843c54dd4ab265" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:same_file.py", + "target": "class:4c9c90936185ce0ba79fff80c167872e" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:same_file.py", + "target": "class:60298d4e866b278c8cf73fc2a2ebceae" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:same_file.py", + "target": "class:73d50236f18650b7d0a250d4ae90cf3c" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:same_file.py", + "target": "class:980443e74cab7da71208d2a972b5a8d9" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:same_file.py", + "target": "class:ae598df922bbcd74e32d3bf7d7fa0828" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:same_file.py", + "target": "class:f6b91632a5d8b5435b08ac99b12f0570" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:same_file.py", + "target": "function:1198b6fc92ac818287e950fa1a067eda" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:same_file.py", + "target": "function:5e720163b2a45565e1d981792fd7ecb7" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:same_file.py", + "target": "function:7fe38b2a101736abde38fef885862694" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:same_file.py", + "target": "function:8ab91cb55b890abcf7c58c5a8c532661" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:same_file.py", + "target": "function:9b5e951f339c7fb15ae8717e06f1178e" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:same_file.py", + "target": "function:ed31b0a0b2aa330bfbd8d25cc2f04164" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:same_file.py", + "target": "function:f216f97a7c79cba22f0cf1bcb01d4ff4" + } +] diff --git a/reference/golden/python/files.json b/reference/golden/python/files.json new file mode 100644 index 00000000..dc1e461b --- /dev/null +++ b/reference/golden/python/files.json @@ -0,0 +1,26 @@ +[ + { + "content_hash": "ffcdab43044b451a4dd403d4d74460128523d9fcba30dfe75e9a595a0f2c46db", + "errors": null, + "language": "python", + "node_count": 3, + "path": "consumers.py", + "size": 91 + }, + { + "content_hash": "6cebb328503cb6d3a66c76ade1e6da7f16dbd066c1689c41765ea55255d9d030", + "errors": null, + "language": "python", + "node_count": 2, + "path": "imported_types.py", + "size": 30 + }, + { + "content_hash": "dcee64febdb07e86597a805d2ca48d93a6c69a4c547247399c39e316c21827d2", + "errors": null, + "language": "python", + "node_count": 17, + "path": "same_file.py", + "size": 613 + } +] diff --git a/reference/golden/python/nodes.json b/reference/golden/python/nodes.json new file mode 100644 index 00000000..55b7a7d9 --- /dev/null +++ b/reference/golden/python/nodes.json @@ -0,0 +1,486 @@ +[ + { + "decorators": null, + "docstring": null, + "end_column": 8, + "end_line": 18, + "file_path": "same_file.py", + "id": "class:02ea0f42e300fcc1605594c67cde3536", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "class", + "language": "python", + "name": "ListClass", + "qualified_name": "ListClass", + "return_type": null, + "signature": null, + "start_column": 0, + "start_line": 17, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 8, + "end_line": 2, + "file_path": "imported_types.py", + "id": "class:099ebd3f5c7b1a6ac5522e9fbfb00912", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "class", + "language": "python", + "name": "ImportedClass", + "qualified_name": "ImportedClass", + "return_type": null, + "signature": null, + "start_column": 0, + "start_line": 1, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 8, + "end_line": 6, + "file_path": "same_file.py", + "id": "class:2ba663f8546d3fb02b843c54dd4ab265", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "class", + "language": "python", + "name": "AliasClass", + "qualified_name": "AliasClass", + "return_type": null, + "signature": null, + "start_column": 0, + "start_line": 5, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 8, + "end_line": 2, + "file_path": "same_file.py", + "id": "class:4c9c90936185ce0ba79fff80c167872e", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "class", + "language": "python", + "name": "ReturnClass", + "qualified_name": "ReturnClass", + "return_type": null, + "signature": null, + "start_column": 0, + "start_line": 1, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 8, + "end_line": 26, + "file_path": "same_file.py", + "id": "class:60298d4e866b278c8cf73fc2a2ebceae", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "class", + "language": "python", + "name": "TupleB", + "qualified_name": "TupleB", + "return_type": null, + "signature": null, + "start_column": 0, + "start_line": 25, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 8, + "end_line": 14, + "file_path": "same_file.py", + "id": "class:73d50236f18650b7d0a250d4ae90cf3c", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "class", + "language": "python", + "name": "ArgumentClass", + "qualified_name": "ArgumentClass", + "return_type": null, + "signature": null, + "start_column": 0, + "start_line": 13, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 19, + "end_line": 31, + "file_path": "same_file.py", + "id": "class:980443e74cab7da71208d2a972b5a8d9", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "class", + "language": "python", + "name": "HandlerOwner", + "qualified_name": "HandlerOwner", + "return_type": null, + "signature": null, + "start_column": 0, + "start_line": 29, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 8, + "end_line": 22, + "file_path": "same_file.py", + "id": "class:ae598df922bbcd74e32d3bf7d7fa0828", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "class", + "language": "python", + "name": "TupleA", + "qualified_name": "TupleA", + "return_type": null, + "signature": null, + "start_column": 0, + "start_line": 21, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 8, + "end_line": 10, + "file_path": "same_file.py", + "id": "class:f6b91632a5d8b5435b08ac99b12f0570", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "class", + "language": "python", + "name": "RegistryClass", + "qualified_name": "RegistryClass", + "return_type": null, + "signature": null, + "start_column": 0, + "start_line": 9, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 0, + "end_line": 6, + "file_path": "consumers.py", + "id": "file:consumers.py", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "file", + "language": "python", + "name": "consumers.py", + "qualified_name": "consumers.py", + "return_type": null, + "signature": null, + "start_column": 0, + "start_line": 1, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 0, + "end_line": 3, + "file_path": "imported_types.py", + "id": "file:imported_types.py", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "file", + "language": "python", + "name": "imported_types.py", + "qualified_name": "imported_types.py", + "return_type": null, + "signature": null, + "start_column": 0, + "start_line": 1, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 0, + "end_line": 60, + "file_path": "same_file.py", + "id": "file:same_file.py", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "file", + "language": "python", + "name": "same_file.py", + "qualified_name": "same_file.py", + "return_type": null, + "signature": null, + "start_column": 0, + "start_line": 1, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 42, + "end_line": 43, + "file_path": "same_file.py", + "id": "function:1198b6fc92ac818287e950fa1a067eda", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "function", + "language": "python", + "name": "choose_registry", + "qualified_name": "choose_registry", + "return_type": null, + "signature": "()", + "start_column": 0, + "start_line": 42, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 24, + "end_line": 5, + "file_path": "consumers.py", + "id": "function:230b5585efc925330d3cbf214e8be03b", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "function", + "language": "python", + "name": "choose_imported", + "qualified_name": "choose_imported", + "return_type": null, + "signature": "()", + "start_column": 0, + "start_line": 4, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 24, + "end_line": 51, + "file_path": "same_file.py", + "id": "function:5e720163b2a45565e1d981792fd7ecb7", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "function", + "language": "python", + "name": "choose_list", + "qualified_name": "choose_list", + "return_type": null, + "signature": "()", + "start_column": 0, + "start_line": 50, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 22, + "end_line": 35, + "file_path": "same_file.py", + "id": "function:7fe38b2a101736abde38fef885862694", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "function", + "language": "python", + "name": "choose_return", + "qualified_name": "choose_return", + "return_type": null, + "signature": "()", + "start_column": 0, + "start_line": 34, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 27, + "end_line": 47, + "file_path": "same_file.py", + "id": "function:8ab91cb55b890abcf7c58c5a8c532661", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "function", + "language": "python", + "name": "choose_argument", + "qualified_name": "choose_argument", + "return_type": null, + "signature": "()", + "start_column": 0, + "start_line": 46, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 21, + "end_line": 59, + "file_path": "same_file.py", + "id": "function:9b5e951f339c7fb15ae8717e06f1178e", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "function", + "language": "python", + "name": "wire", + "qualified_name": "wire", + "return_type": null, + "signature": "(handler)", + "start_column": 0, + "start_line": 58, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 25, + "end_line": 55, + "file_path": "same_file.py", + "id": "function:ed31b0a0b2aa330bfbd8d25cc2f04164", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "function", + "language": "python", + "name": "choose_tuple", + "qualified_name": "choose_tuple", + "return_type": null, + "signature": "()", + "start_column": 0, + "start_line": 54, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 22, + "end_line": 39, + "file_path": "same_file.py", + "id": "function:f216f97a7c79cba22f0cf1bcb01d4ff4", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "function", + "language": "python", + "name": "choose_alias", + "qualified_name": "choose_alias", + "return_type": null, + "signature": "()", + "start_column": 0, + "start_line": 38, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 40, + "end_line": 1, + "file_path": "consumers.py", + "id": "import:3e3fb8b0ff507a934daf33d767cfbcbe", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "import", + "language": "python", + "name": "imported_types", + "qualified_name": "imported_types", + "return_type": null, + "signature": "from imported_types import ImportedClass", + "start_column": 0, + "start_line": 1, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 19, + "end_line": 31, + "file_path": "same_file.py", + "id": "method:8ac33b9d670a290f5c7fc3d5e77b79be", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "method", + "language": "python", + "name": "handler", + "qualified_name": "HandlerOwner::handler", + "return_type": null, + "signature": "(self)", + "start_column": 4, + "start_line": 30, + "type_parameters": null, + "visibility": null + } +] diff --git a/reference/golden/python/refs.json b/reference/golden/python/refs.json new file mode 100644 index 00000000..bd252450 --- /dev/null +++ b/reference/golden/python/refs.json @@ -0,0 +1,32 @@ +[ + { + "candidates": null, + "col": 13, + "file_path": "same_file.py", + "from_node_id": "function:9b5e951f339c7fb15ae8717e06f1178e", + "language": "python", + "line": 59, + "reference_kind": "function_ref", + "reference_name": "handler" + }, + { + "candidates": null, + "col": 4, + "file_path": "same_file.py", + "from_node_id": "function:8ab91cb55b890abcf7c58c5a8c532661", + "language": "python", + "line": 47, + "reference_kind": "calls", + "reference_name": "register" + }, + { + "candidates": null, + "col": 4, + "file_path": "same_file.py", + "from_node_id": "function:9b5e951f339c7fb15ae8717e06f1178e", + "language": "python", + "line": 59, + "reference_kind": "calls", + "reference_name": "register" + } +] diff --git a/reference/golden/python/schema.sql b/reference/golden/python/schema.sql new file mode 100644 index 00000000..d36218fe --- /dev/null +++ b/reference/golden/python/schema.sql @@ -0,0 +1,117 @@ +CREATE TABLE schema_versions ( +version INTEGER PRIMARY KEY, +applied_at INTEGER NOT NULL, +description TEXT +); +CREATE TABLE nodes ( +id TEXT PRIMARY KEY, +kind TEXT NOT NULL, +name TEXT NOT NULL, +qualified_name TEXT NOT NULL, +file_path TEXT NOT NULL, +language TEXT NOT NULL, +start_line INTEGER NOT NULL, +end_line INTEGER NOT NULL, +start_column INTEGER NOT NULL, +end_column INTEGER NOT NULL, +docstring TEXT, +signature TEXT, +visibility TEXT, +is_exported INTEGER DEFAULT 0, +is_async INTEGER DEFAULT 0, +is_static INTEGER DEFAULT 0, +is_abstract INTEGER DEFAULT 0, +decorators TEXT, -- JSON array +type_parameters TEXT, -- JSON array +return_type TEXT, -- normalized return/result type name (e.g. C++ method return, for receiver-type inference) +updated_at INTEGER NOT NULL +); +CREATE TABLE edges ( +id INTEGER PRIMARY KEY AUTOINCREMENT, +source TEXT NOT NULL, +target TEXT NOT NULL, +kind TEXT NOT NULL, +metadata TEXT, -- JSON object +line INTEGER, +col INTEGER, +provenance TEXT DEFAULT NULL, +FOREIGN KEY (source) REFERENCES nodes(id) ON DELETE CASCADE, +FOREIGN KEY (target) REFERENCES nodes(id) ON DELETE CASCADE +); +CREATE TABLE sqlite_sequence(name,seq); +CREATE TABLE files ( +path TEXT PRIMARY KEY, +content_hash TEXT NOT NULL, +language TEXT NOT NULL, +size INTEGER NOT NULL, +modified_at INTEGER NOT NULL, +indexed_at INTEGER NOT NULL, +node_count INTEGER DEFAULT 0, +errors TEXT -- JSON array +); +CREATE TABLE unresolved_refs ( +id INTEGER PRIMARY KEY AUTOINCREMENT, +from_node_id TEXT NOT NULL, +reference_name TEXT NOT NULL, +reference_kind TEXT NOT NULL, +line INTEGER NOT NULL, +col INTEGER NOT NULL, +candidates TEXT, -- JSON array +file_path TEXT NOT NULL DEFAULT '', +language TEXT NOT NULL DEFAULT 'unknown', +reference_subkind TEXT, +FOREIGN KEY (from_node_id) REFERENCES nodes(id) ON DELETE CASCADE +); +CREATE INDEX idx_nodes_kind ON nodes(kind); +CREATE INDEX idx_nodes_name ON nodes(name); +CREATE INDEX idx_nodes_qualified_name ON nodes(qualified_name); +CREATE INDEX idx_nodes_file_path ON nodes(file_path); +CREATE INDEX idx_nodes_language ON nodes(language); +CREATE INDEX idx_nodes_file_line ON nodes(file_path, start_line); +CREATE INDEX idx_nodes_lower_name ON nodes(lower(name)); +CREATE VIRTUAL TABLE nodes_fts USING fts5( +id, +name, +qualified_name, +docstring, +signature, +content='nodes', +content_rowid='rowid' +) +/* nodes_fts(id,name,qualified_name,docstring,signature) */; +CREATE TABLE 'nodes_fts_data'(id INTEGER PRIMARY KEY, block BLOB); +CREATE TABLE 'nodes_fts_idx'(segid, term, pgno, PRIMARY KEY(segid, term)) WITHOUT ROWID; +CREATE TABLE 'nodes_fts_docsize'(id INTEGER PRIMARY KEY, sz BLOB); +CREATE TABLE 'nodes_fts_config'(k PRIMARY KEY, v) WITHOUT ROWID; +CREATE TRIGGER nodes_ai AFTER INSERT ON nodes BEGIN +INSERT INTO nodes_fts(rowid, id, name, qualified_name, docstring, signature) +VALUES (NEW.rowid, NEW.id, NEW.name, NEW.qualified_name, NEW.docstring, NEW.signature); +END; +CREATE TRIGGER nodes_ad AFTER DELETE ON nodes BEGIN +INSERT INTO nodes_fts(nodes_fts, rowid, id, name, qualified_name, docstring, signature) +VALUES ('delete', OLD.rowid, OLD.id, OLD.name, OLD.qualified_name, OLD.docstring, OLD.signature); +END; +CREATE TRIGGER nodes_au AFTER UPDATE ON nodes BEGIN +INSERT INTO nodes_fts(nodes_fts, rowid, id, name, qualified_name, docstring, signature) +VALUES ('delete', OLD.rowid, OLD.id, OLD.name, OLD.qualified_name, OLD.docstring, OLD.signature); +INSERT INTO nodes_fts(rowid, id, name, qualified_name, docstring, signature) +VALUES (NEW.rowid, NEW.id, NEW.name, NEW.qualified_name, NEW.docstring, NEW.signature); +END; +CREATE INDEX idx_edges_kind ON edges(kind); +CREATE INDEX idx_edges_source_kind ON edges(source, kind); +CREATE INDEX idx_edges_target_kind ON edges(target, kind); +CREATE UNIQUE INDEX idx_edges_identity +ON edges(source, target, kind, IFNULL(line, -1), IFNULL(col, -1)); +CREATE INDEX idx_files_language ON files(language); +CREATE INDEX idx_files_modified_at ON files(modified_at); +CREATE INDEX idx_unresolved_from_node ON unresolved_refs(from_node_id); +CREATE INDEX idx_unresolved_name ON unresolved_refs(reference_name); +CREATE INDEX idx_unresolved_file_path ON unresolved_refs(file_path); +CREATE INDEX idx_unresolved_from_name ON unresolved_refs(from_node_id, reference_name); +CREATE INDEX idx_edges_provenance ON edges(provenance); +CREATE TABLE project_metadata ( +key TEXT PRIMARY KEY, +value TEXT NOT NULL, +updated_at INTEGER NOT NULL +); +CREATE TABLE sqlite_stat1(tbl,idx,stat);