sqry-core 8.0.3

Core library for sqry - semantic code search engine
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
//! Binding query facade with declaration/reference classification.
//!
//! Provides `BindingQuery`, a builder-pattern API for resolving symbols and
//! classifying them as declarations, references, imports, or ambiguous. This
//! module builds on the witness-bearing resolution API in `resolution.rs`.

use super::concurrent::GraphSnapshot;
use super::edge::kind::{EdgeKind, ExportKind};
use super::node::id::NodeId;
use super::node::kind::NodeKind;
use super::resolution::{
    FileScope, NormalizedSymbolQuery, ResolutionMode, SymbolCandidateBucket,
    SymbolCandidateOutcome, SymbolQuery, SymbolResolutionOutcome,
};

/// How a resolved symbol relates to its definition site.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SymbolClassification {
    /// The node IS the declaration (target of `Defines`/`Contains` edges,
    /// `NodeKind` is not `CallSite`/`Import`/`Export`).
    Declaration,
    /// The node is a reference/use of a declaration elsewhere.
    Reference,
    /// The node is an import statement (has `Defines` edge but is not
    /// the source-of-truth declaration).
    Import,
    /// Multiple interpretations possible (e.g., re-export that is both
    /// a reference and a local declaration).
    Ambiguous,
    /// Classification could not be determined from graph structure.
    Unknown,
}

/// A single resolved binding with classification and provenance.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedBinding {
    /// The resolved node ID.
    pub node_id: NodeId,
    /// Declaration vs reference vs import classification.
    pub classification: SymbolClassification,
    /// Which resolution bucket produced this candidate.
    pub bucket: SymbolCandidateBucket,
    /// The node's kind (saves consumers a `get_node()` round-trip).
    pub kind: NodeKind,
}

/// Result of a binding query.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BindingResult {
    /// Normalized query (`None` if file scope resolution failed).
    pub query: Option<NormalizedSymbolQuery>,
    /// Resolved bindings with classification and provenance.
    pub bindings: Vec<ResolvedBinding>,
    /// Resolution outcome (same semantics as `resolve_symbol`).
    pub outcome: SymbolResolutionOutcome,
}

/// Builder for binding queries.
///
/// # Example
///
/// ```rust,ignore
/// let result = BindingQuery::new("MyClass")
///     .file_scope(FileScope::Any)
///     .mode(ResolutionMode::AllowSuffixCandidates)
///     .resolve(&snapshot);
/// ```
pub struct BindingQuery<'a> {
    symbol: &'a str,
    file_scope: FileScope<'a>,
    mode: ResolutionMode,
}

impl<'a> BindingQuery<'a> {
    /// Creates a new binding query for the given symbol.
    ///
    /// Defaults to `FileScope::Any` and `ResolutionMode::AllowSuffixCandidates`.
    #[must_use]
    pub fn new(symbol: &'a str) -> Self {
        Self {
            symbol,
            file_scope: FileScope::Any,
            mode: ResolutionMode::AllowSuffixCandidates,
        }
    }

    /// Restricts the query to a specific file scope.
    #[must_use]
    pub fn file_scope(mut self, scope: FileScope<'a>) -> Self {
        self.file_scope = scope;
        self
    }

    /// Sets the resolution mode.
    #[must_use]
    pub fn mode(mut self, mode: ResolutionMode) -> Self {
        self.mode = mode;
        self
    }

    /// Resolves the query against the given snapshot.
    ///
    /// Performs symbol resolution via the witness-bearing API, then classifies
    /// each candidate based on its `NodeKind` and incoming edge structure.
    #[must_use]
    pub fn resolve(self, snapshot: &GraphSnapshot) -> BindingResult {
        let witness = snapshot.find_symbol_candidates_with_witness(&SymbolQuery {
            symbol: self.symbol,
            file_scope: self.file_scope,
            mode: self.mode,
        });

        // Determine outcome
        let outcome = match &witness.outcome {
            SymbolCandidateOutcome::Candidates(candidates) => match candidates.as_slice() {
                [] => SymbolResolutionOutcome::NotFound,
                [node_id] => SymbolResolutionOutcome::Resolved(*node_id),
                _ => SymbolResolutionOutcome::Ambiguous(candidates.clone()),
            },
            SymbolCandidateOutcome::NotFound => SymbolResolutionOutcome::NotFound,
            SymbolCandidateOutcome::FileNotIndexed => SymbolResolutionOutcome::FileNotIndexed,
        };

        // Build bindings with classification
        let bindings: Vec<ResolvedBinding> = witness
            .candidates
            .iter()
            .filter_map(|candidate| {
                let entry = snapshot.get_node(candidate.node_id)?;
                let classification = classify_node(snapshot, candidate.node_id, entry.kind);
                Some(ResolvedBinding {
                    node_id: candidate.node_id,
                    classification,
                    bucket: candidate.bucket,
                    kind: entry.kind,
                })
            })
            .collect();

        BindingResult {
            query: witness.normalized_query,
            bindings,
            outcome,
        }
    }
}

/// Classify a node as declaration, reference, import, or ambiguous.
///
/// Classification logic:
/// 1. `NodeKind::Import` → `Import`
/// 2. `NodeKind::Export` → check for re-export edge → `Ambiguous` if re-export, else `Import`
/// 3. `NodeKind::CallSite` → `Reference`
/// 4. Has incoming `Defines` or `Contains` edge → `Declaration`
/// 5. No structural incoming edges → `Reference`
/// 6. Fallback → `Unknown`
fn classify_node(
    snapshot: &GraphSnapshot,
    node_id: NodeId,
    kind: NodeKind,
) -> SymbolClassification {
    // Step 1: Check NodeKind first
    if kind == NodeKind::Import {
        return SymbolClassification::Import;
    }

    if kind == NodeKind::Export {
        // Check if this export has a re-export edge
        let incoming = snapshot.edges().edges_to(node_id);
        let has_reexport = incoming.iter().any(|e| {
            matches!(
                &e.kind,
                EdgeKind::Exports {
                    kind: ExportKind::Reexport | ExportKind::Namespace,
                    ..
                }
            )
        });
        // Also check outgoing exports for re-export classification
        let outgoing = snapshot.edges().edges_from(node_id);
        let has_reexport_outgoing = outgoing.iter().any(|e| {
            matches!(
                &e.kind,
                EdgeKind::Exports {
                    kind: ExportKind::Reexport | ExportKind::Namespace,
                    ..
                }
            )
        });

        return if has_reexport || has_reexport_outgoing {
            SymbolClassification::Ambiguous
        } else {
            SymbolClassification::Import
        };
    }

    if kind == NodeKind::CallSite {
        return SymbolClassification::Reference;
    }

    // Step 2: Check incoming edges for structural (Defines/Contains)
    let incoming = snapshot.edges().edges_to(node_id);
    let has_structural = incoming
        .iter()
        .any(|e| matches!(&e.kind, EdgeKind::Defines | EdgeKind::Contains));

    if has_structural {
        return SymbolClassification::Declaration;
    }

    // Step 3: No structural incoming edges → Reference
    if !incoming.is_empty() {
        return SymbolClassification::Reference;
    }

    // Step 4: No incoming edges at all — could be a root declaration or orphan
    // If the node has outgoing Defines/Contains edges, it's likely a root module/declaration
    let outgoing = snapshot.edges().edges_from(node_id);
    let has_outgoing_structural = outgoing
        .iter()
        .any(|e| matches!(&e.kind, EdgeKind::Defines | EdgeKind::Contains));

    if has_outgoing_structural {
        return SymbolClassification::Declaration;
    }

    SymbolClassification::Unknown
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::graph::node::Language;
    use crate::graph::unified::concurrent::CodeGraph;
    use crate::graph::unified::file::FileId;
    use crate::graph::unified::storage::arena::NodeEntry;

    struct TestGraph {
        graph: CodeGraph,
        file_id: Option<FileId>,
    }

    impl TestGraph {
        fn new() -> Self {
            Self {
                graph: CodeGraph::new(),
                file_id: None,
            }
        }

        fn ensure_file_id(&mut self) -> FileId {
            if let Some(fid) = self.file_id {
                return fid;
            }
            let file_path = std::path::PathBuf::from("/bind-tests/test.rs");
            let fid = self
                .graph
                .files_mut()
                .register_with_language(&file_path, Some(Language::Rust))
                .unwrap();
            self.file_id = Some(fid);
            fid
        }

        fn add_node(&mut self, name: &str, kind: NodeKind) -> NodeId {
            let file_id = self.ensure_file_id();
            let name_id = self.graph.strings_mut().intern(name).unwrap();
            let qn_id = self
                .graph
                .strings_mut()
                .intern(&format!("test::{name}"))
                .unwrap();

            let entry = NodeEntry::new(kind, name_id, file_id)
                .with_qualified_name(qn_id)
                .with_location(1, 0, 10, 0);

            let node_id = self.graph.nodes_mut().alloc(entry).unwrap();
            self.graph
                .indices_mut()
                .add(node_id, kind, name_id, Some(qn_id), file_id);
            node_id
        }

        fn add_edge(&mut self, source: NodeId, target: NodeId, kind: EdgeKind) {
            let file_id = self.ensure_file_id();
            self.graph
                .edges_mut()
                .add_edge(source, target, kind, file_id);
        }

        fn snapshot(&self) -> GraphSnapshot {
            self.graph.snapshot()
        }
    }

    #[test]
    fn declaration_classification() {
        let mut tg = TestGraph::new();
        let module_node = tg.add_node("my_module", NodeKind::Module);
        let func_node = tg.add_node("my_func", NodeKind::Function);
        tg.add_edge(module_node, func_node, EdgeKind::Defines);

        let snapshot = tg.snapshot();
        let result = BindingQuery::new("my_func").resolve(&snapshot);

        assert!(!result.bindings.is_empty(), "expected at least one binding");
        let binding = result
            .bindings
            .iter()
            .find(|b| b.node_id == func_node)
            .expect("expected binding for func_node");
        assert_eq!(binding.classification, SymbolClassification::Declaration);
        assert_eq!(binding.kind, NodeKind::Function);
    }

    #[test]
    fn reference_classification_callsite() {
        let mut tg = TestGraph::new();
        let _call_node = tg.add_node("some_call", NodeKind::CallSite);

        let snapshot = tg.snapshot();
        let result = BindingQuery::new("some_call").resolve(&snapshot);

        assert!(!result.bindings.is_empty());
        assert_eq!(
            result.bindings[0].classification,
            SymbolClassification::Reference
        );
    }

    #[test]
    fn import_classification() {
        let mut tg = TestGraph::new();
        let _import_node = tg.add_node("imported_sym", NodeKind::Import);

        let snapshot = tg.snapshot();
        let result = BindingQuery::new("imported_sym").resolve(&snapshot);

        assert!(!result.bindings.is_empty());
        assert_eq!(
            result.bindings[0].classification,
            SymbolClassification::Import
        );
    }

    #[test]
    fn export_direct_classification() {
        let mut tg = TestGraph::new();
        let _export_node = tg.add_node("exported_sym", NodeKind::Export);

        let snapshot = tg.snapshot();
        let result = BindingQuery::new("exported_sym").resolve(&snapshot);

        assert!(!result.bindings.is_empty());
        // Direct export without re-export edges → Import classification
        assert_eq!(
            result.bindings[0].classification,
            SymbolClassification::Import
        );
    }

    #[test]
    fn export_reexport_ambiguous() {
        let mut tg = TestGraph::new();
        let source = tg.add_node("source_mod", NodeKind::Module);
        let export_node = tg.add_node("reexported", NodeKind::Export);
        tg.add_edge(
            source,
            export_node,
            EdgeKind::Exports {
                kind: ExportKind::Reexport,
                alias: None,
            },
        );

        let snapshot = tg.snapshot();
        let result = BindingQuery::new("reexported").resolve(&snapshot);

        assert!(!result.bindings.is_empty());
        let binding = result
            .bindings
            .iter()
            .find(|b| b.node_id == export_node)
            .expect("expected binding for export_node");
        assert_eq!(binding.classification, SymbolClassification::Ambiguous);
    }

    #[test]
    fn builder_defaults() {
        let query = BindingQuery::new("test_sym");
        assert_eq!(query.symbol, "test_sym");
        assert_eq!(query.file_scope, FileScope::Any);
        assert_eq!(query.mode, ResolutionMode::AllowSuffixCandidates);
    }

    #[test]
    fn not_found_result() {
        let tg = TestGraph::new();
        let snapshot = tg.snapshot();
        let result = BindingQuery::new("nonexistent_symbol_xyz").resolve(&snapshot);

        assert!(result.bindings.is_empty());
        assert_eq!(result.outcome, SymbolResolutionOutcome::NotFound);
    }

    #[test]
    fn declaration_via_contains_edge() {
        let mut tg = TestGraph::new();
        let class_node = tg.add_node("MyClass", NodeKind::Class);
        let method_node = tg.add_node("my_method", NodeKind::Method);
        tg.add_edge(class_node, method_node, EdgeKind::Contains);

        let snapshot = tg.snapshot();
        let result = BindingQuery::new("my_method").resolve(&snapshot);

        assert!(!result.bindings.is_empty());
        let binding = result
            .bindings
            .iter()
            .find(|b| b.node_id == method_node)
            .expect("expected binding for method_node");
        assert_eq!(binding.classification, SymbolClassification::Declaration);
    }

    #[test]
    fn root_declaration_with_outgoing_defines() {
        let mut tg = TestGraph::new();
        let module_node = tg.add_node("root_mod", NodeKind::Module);
        let child = tg.add_node("child_func", NodeKind::Function);
        tg.add_edge(module_node, child, EdgeKind::Defines);

        let snapshot = tg.snapshot();
        let result = BindingQuery::new("root_mod").resolve(&snapshot);

        assert!(!result.bindings.is_empty());
        let binding = result
            .bindings
            .iter()
            .find(|b| b.node_id == module_node)
            .expect("expected binding for module_node");
        // Root module has no incoming edges but has outgoing Defines → Declaration
        assert_eq!(binding.classification, SymbolClassification::Declaration);
    }
}