lsp-max 26.7.3

Law-state LSP runtime: max LSP 3.18 coverage, process-mining conformance, receipt-chain admission
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
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
//! LSIF → Oxigraph RDF named-graph import.
//!
//! Reads an LSIF JSONL file and loads a queryable RDF projection into an
//! Oxigraph named graph, using the same vocabulary
//! `views::populate_defs_refs`'s SPARQL queries expect (the real LSIF 0.6.0
//! spec namespace plus the `max:` position-property vocabulary), so that
//! imported data is actually reachable via `textDocument/definition` and
//! `textDocument/references` lookups:
//!
//! | LSIF entry                        | RDF triple(s)                                  |
//! |------------------------------------|------------------------------------------------|
//! | `document` vertex                  | `lsif:document` type; `max:uri`                |
//! | `moniker` vertex                   | `lsif:symbol` type; `lsif:identifier`/`lsif:kind` |
//! | `referenceResult` vertex           | `lsif:referenceOf` type                        |
//! | `range` vertex                     | `max:startLine`/`startCharacter`/`endLine`/`endCharacter` |
//! | `contains` edge (outV, inVs)       | `lsif:contains` per id in `inVs`               |
//! | `next` edge (outV, inV)            | `lsif:next`                                    |
//! | `item` edge (outV, inVs)           | `lsif:item` per id in `inVs`                   |
//! | `textDocument/definition` edge     | `lsif:textDocument_definition`                 |
//! | `textDocument/references` edge     | `lsif:textDocument_references`                 |
//!
//! All other LSIF labels are skipped.
//!
//! # INVARIANT: OXIGRAPH_NOT_ON_HOT_PATH — lsif_import is cold-path only.
//! This module MUST NOT be called from `did_change`, `did_open`, or any LSP
//! notification handler.  It is a batch import step run at admission time.

use std::io::{BufRead, BufReader};
use std::path::Path;

use oxigraph::model::{GraphNameRef, Literal, NamedNode, NamedNodeRef, Quad, Subject, Term};
use oxigraph::store::Store;

// ---------------------------------------------------------------------------
// RDF namespaces
// ---------------------------------------------------------------------------

/// Real LSIF 0.6.0 spec namespace — must match the `PREFIX lsif:` used in
/// `views::populate_defs_refs`'s SPARQL queries.
const NS: &str =
    "https://microsoft.github.io/language-server-protocol/specifications/lsif/0.6.0/specification/";

/// `max:` position/uri vocabulary — must match the `PREFIX max:` used in
/// `views::populate_defs_refs`'s SPARQL queries.
const MAX_NS: &str = "urn:lsp-max:core:";

fn ns(local: &str) -> NamedNode {
    NamedNode::new(format!("{NS}{local}")).expect("static LSIF namespace must be valid")
}

fn max_ns(local: &str) -> NamedNode {
    NamedNode::new(format!("{MAX_NS}{local}")).expect("static max: namespace must be valid")
}

fn vertex_node(id: u64) -> NamedNode {
    NamedNode::new(format!("urn:lsif:v/{id}")).expect("LSIF vertex URN must be valid")
}

fn u64_literal(n: u64) -> Term {
    // `views::helpers::term_to_u32` reads the literal's lexical value via
    // `Literal::value()` regardless of datatype, so a simple literal (same
    // pattern the rest of this file already uses) is sufficient here.
    Term::Literal(Literal::new_simple_literal(n.to_string()))
}

// ---------------------------------------------------------------------------
// Stats
// ---------------------------------------------------------------------------

/// Counts of RDF triples loaded per category during an LSIF import.
///
/// Counts reflect triples parsed from the file, not net additions to the store
/// (Oxigraph is a set store — duplicate triples are silently ignored).
/// This ensures `LsifImportStats` is identical across idempotent re-imports.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct LsifImportStats {
    pub document_triples: usize,
    pub moniker_triples: usize,
    pub reference_triples: usize,
    pub range_triples: usize,
    pub edge_triples: usize,
    pub total_triples: usize,
}

// ---------------------------------------------------------------------------
// Import entry point
// ---------------------------------------------------------------------------

/// Import an LSIF JSONL file into an Oxigraph named graph.
///
/// # INVARIANT: OXIGRAPH_NOT_ON_HOT_PATH — lsif_import is cold-path only.
/// Call only at admission / receipt generation time, never from an LSP handler.
///
/// Returns counts of triples inserted per category.  The counts reflect the
/// number of triples parsed from the file; Oxigraph deduplicates automatically.
pub fn import_lsif_into_graph(
    store: &Store,
    lsif_path: &Path,
    graph_name: &str,
) -> anyhow::Result<LsifImportStats> {
    let graph_node = NamedNode::new(graph_name)
        .map_err(|e| anyhow::anyhow!("invalid graph name URI {graph_name:?}: {e}"))?;
    let graph_ref = GraphNameRef::NamedNode(graph_node.as_ref());

    let file = std::fs::File::open(lsif_path)
        .map_err(|e| anyhow::anyhow!("cannot open LSIF file {}: {e}", lsif_path.display()))?;
    let reader = BufReader::new(file);

    let mut stats = LsifImportStats::default();

    // Pre-build type/property predicates.
    let rdf_type = NamedNode::new("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")
        .expect("rdf:type must be valid");
    let lsif_document = ns("document");
    let lsif_symbol = ns("symbol");
    let lsif_reference_of = ns("referenceOf");
    let lsif_identifier_pred = ns("identifier");
    let lsif_kind_pred = ns("kind");
    let max_uri_pred = max_ns("uri");
    let max_start_line = max_ns("startLine");
    let max_start_char = max_ns("startCharacter");
    let max_end_line = max_ns("endLine");
    let max_end_char = max_ns("endCharacter");
    let lsif_contains = ns("contains");
    let lsif_next = ns("next");
    let lsif_item = ns("item");
    let lsif_text_document_definition = ns("textDocument_definition");
    let lsif_text_document_references = ns("textDocument_references");

    for (line_no, line) in reader.lines().enumerate() {
        let line = line.map_err(|e| anyhow::anyhow!("read error at line {line_no}: {e}"))?;
        let line = line.trim();
        if line.is_empty() {
            continue;
        }

        let v: serde_json::Value = match serde_json::from_str(line) {
            Ok(v) => v,
            Err(_) => continue, // skip malformed lines
        };

        let entry_type = match v.get("type").and_then(|t| t.as_str()) {
            Some(t) => t,
            None => continue,
        };

        let label = match v.get("label").and_then(|l| l.as_str()) {
            Some(l) => l,
            None => continue,
        };

        match entry_type {
            "vertex" => {
                let id = match v.get("id").and_then(|i| i.as_u64()) {
                    Some(id) => id,
                    None => continue,
                };
                let subject = Subject::NamedNode(vertex_node(id));

                match label {
                    "document" => {
                        let uri_val = v
                            .get("uri")
                            .and_then(|u| u.as_str())
                            .unwrap_or("")
                            .to_string();

                        // <vertex_node> rdf:type lsif:document
                        insert_quad(
                            store,
                            subject.clone(),
                            rdf_type.clone(),
                            Term::NamedNode(lsif_document.clone()),
                            graph_ref,
                        );
                        stats.document_triples += 1;

                        // <vertex_node> max:uri "file:///..." — the property
                        // populate_defs_refs's SPARQL actually queries for.
                        insert_quad(
                            store,
                            subject,
                            max_uri_pred.clone(),
                            Term::Literal(Literal::new_simple_literal(uri_val)),
                            graph_ref,
                        );
                        stats.document_triples += 1;
                    }

                    "moniker" => {
                        let identifier = v
                            .get("identifier")
                            .and_then(|i| i.as_str())
                            .unwrap_or("")
                            .to_string();
                        let kind = v
                            .get("kind")
                            .and_then(|k| k.as_str())
                            .unwrap_or("local")
                            .to_string();

                        // <vertex_node> rdf:type lsif:symbol
                        insert_quad(
                            store,
                            subject.clone(),
                            rdf_type.clone(),
                            Term::NamedNode(lsif_symbol.clone()),
                            graph_ref,
                        );
                        stats.moniker_triples += 1;

                        // <vertex_node> lsif:identifier "crate::Symbol"
                        insert_quad(
                            store,
                            subject.clone(),
                            lsif_identifier_pred.clone(),
                            Term::Literal(Literal::new_simple_literal(identifier)),
                            graph_ref,
                        );
                        stats.moniker_triples += 1;

                        // <vertex_node> lsif:kind "export"
                        insert_quad(
                            store,
                            subject,
                            lsif_kind_pred.clone(),
                            Term::Literal(Literal::new_simple_literal(kind)),
                            graph_ref,
                        );
                        stats.moniker_triples += 1;
                    }

                    "referenceResult" => {
                        // <vertex_node> rdf:type lsif:referenceOf
                        insert_quad(
                            store,
                            subject,
                            rdf_type.clone(),
                            Term::NamedNode(lsif_reference_of.clone()),
                            graph_ref,
                        );
                        stats.reference_triples += 1;
                    }

                    "range" => {
                        // <vertex_node> max:startLine/startCharacter/endLine/endCharacter — the
                        // exact properties populate_defs_refs's SPARQL projects for both the
                        // source and destination range of a definition/reference.
                        let start = v.get("start");
                        let end = v.get("end");
                        let start_line = start.and_then(|s| s.get("line")).and_then(|n| n.as_u64());
                        let start_char = start
                            .and_then(|s| s.get("character"))
                            .and_then(|n| n.as_u64());
                        let end_line = end.and_then(|e| e.get("line")).and_then(|n| n.as_u64());
                        let end_char = end
                            .and_then(|e| e.get("character"))
                            .and_then(|n| n.as_u64());

                        if let (Some(sl), Some(sc), Some(el), Some(ec)) =
                            (start_line, start_char, end_line, end_char)
                        {
                            insert_quad(
                                store,
                                subject.clone(),
                                max_start_line.clone(),
                                u64_literal(sl),
                                graph_ref,
                            );
                            insert_quad(
                                store,
                                subject.clone(),
                                max_start_char.clone(),
                                u64_literal(sc),
                                graph_ref,
                            );
                            insert_quad(
                                store,
                                subject.clone(),
                                max_end_line.clone(),
                                u64_literal(el),
                                graph_ref,
                            );
                            insert_quad(
                                store,
                                subject,
                                max_end_char.clone(),
                                u64_literal(ec),
                                graph_ref,
                            );
                            stats.range_triples += 4;
                        }
                    }

                    _ => {} // skip all other vertex labels
                }
            }

            "edge" => {
                let out_v = match v.get("outV").and_then(|i| i.as_u64()) {
                    Some(id) => id,
                    None => continue,
                };
                let out_node = Subject::NamedNode(vertex_node(out_v));

                match label {
                    "contains" => {
                        // <outV> lsif:contains <inV> for each id in inVs.
                        if let Some(in_vs) = v.get("inVs").and_then(|a| a.as_array()) {
                            for in_v in in_vs.iter().filter_map(|i| i.as_u64()) {
                                insert_quad(
                                    store,
                                    out_node.clone(),
                                    lsif_contains.clone(),
                                    Term::NamedNode(vertex_node(in_v)),
                                    graph_ref,
                                );
                                stats.edge_triples += 1;
                            }
                        }
                    }

                    "next" => {
                        // <outV> lsif:next <inV>
                        if let Some(in_v) = v.get("inV").and_then(|i| i.as_u64()) {
                            insert_quad(
                                store,
                                out_node,
                                lsif_next.clone(),
                                Term::NamedNode(vertex_node(in_v)),
                                graph_ref,
                            );
                            stats.edge_triples += 1;
                        }
                    }

                    "item" => {
                        // <outV> lsif:item <inV> for each id in inVs.
                        if let Some(in_vs) = v.get("inVs").and_then(|a| a.as_array()) {
                            for in_v in in_vs.iter().filter_map(|i| i.as_u64()) {
                                insert_quad(
                                    store,
                                    out_node.clone(),
                                    lsif_item.clone(),
                                    Term::NamedNode(vertex_node(in_v)),
                                    graph_ref,
                                );
                                stats.edge_triples += 1;
                            }
                        }
                    }

                    "textDocument/definition" => {
                        // <outV> lsif:textDocument_definition <inV> — note the
                        // underscore: the SPARQL prefix concatenation expects
                        // `lsif:textDocument_definition`, not a `/`-containing
                        // local name (which isn't valid in a SPARQL PN_LOCAL
                        // without escaping anyway).
                        if let Some(in_v) = v.get("inV").and_then(|i| i.as_u64()) {
                            insert_quad(
                                store,
                                out_node,
                                lsif_text_document_definition.clone(),
                                Term::NamedNode(vertex_node(in_v)),
                                graph_ref,
                            );
                            stats.edge_triples += 1;
                        }
                    }

                    "textDocument/references" => {
                        if let Some(in_v) = v.get("inV").and_then(|i| i.as_u64()) {
                            insert_quad(
                                store,
                                out_node,
                                lsif_text_document_references.clone(),
                                Term::NamedNode(vertex_node(in_v)),
                                graph_ref,
                            );
                            stats.edge_triples += 1;
                        }
                    }

                    _ => {} // skip all other edge labels
                }
            }

            _ => {} // skip anything that's neither vertex nor edge
        }
    }

    stats.total_triples = stats.document_triples
        + stats.moniker_triples
        + stats.reference_triples
        + stats.range_triples
        + stats.edge_triples;
    Ok(stats)
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/// Insert a single quad into the store, silently ignoring errors
/// (Oxigraph set semantics — duplicate insertions are no-ops).
fn insert_quad(
    store: &Store,
    subject: Subject,
    predicate: NamedNode,
    object: Term,
    graph_name: GraphNameRef<'_>,
) {
    let quad = Quad::new(subject, predicate, object, graph_name);
    let _ = store.insert(quad.as_ref());
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use oxigraph::store::Store;

    fn real_lsif_path() -> std::path::PathBuf {
        // CARGO_MANIFEST_DIR for the root lsp-max crate = /Users/sac/lsp-max
        // receipts/ lives directly under the workspace root.
        std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("receipts/v26.6.28-lsif.lsif")
    }

    const GRAPH: &str = "https://lsp-max.dev/graphs/v26.6.28";

    // ------------------------------------------------------------------
    // TRUE: an imported definition is actually reachable via
    // `views::lookup_definition` — the concrete proof that this importer's
    // vocabulary and `populate_defs_refs`'s SPARQL now agree. Before this
    // fix, this test could not pass anywhere in the codebase: the importer
    // dropped every edge and used a different RDF namespace than the SPARQL
    // queries expect.
    // ------------------------------------------------------------------
    #[test]
    fn imported_definition_is_reachable_via_lookup_definition() {
        use crate::runtime::control_plane::views::{
            lookup_definition, update_views, MaterializedViewStore,
        };
        use lsp_max_lsif::indexer_rust::index_rust_source;
        use lsp_max_lsif::lsif::ToolInfo;
        use lsp_max_lsif::lsif_builder::LsifBuilder;

        let source = "fn helper() {}\nfn run() { helper(); }\n";
        let uri = "file:///run.rs";

        let mut buf = Vec::new();
        {
            let mut builder = LsifBuilder::new(&mut buf);
            builder
                .emit_metadata(
                    "0.6.0",
                    "file:///w",
                    ToolInfo {
                        name: "lsp-max-lsif".to_string(),
                        version: None,
                        args: None,
                    },
                )
                .unwrap();
            index_rust_source(source, uri, &mut builder).unwrap();
        }

        let tmp =
            std::env::temp_dir().join(format!("lsif_import_test_{}.lsif", std::process::id()));
        std::fs::write(&tmp, &buf).unwrap();

        let store = Store::new().unwrap();
        let stats =
            import_lsif_into_graph(&store, &tmp, "https://lsp-max.dev/graphs/test-defs").unwrap();
        std::fs::remove_file(&tmp).ok();

        assert!(
            stats.range_triples > 0,
            "expected range vertices to be imported; got {stats:?}"
        );
        assert!(
            stats.edge_triples > 0,
            "expected edges (next/item/textDocument_definition/contains) to be imported; got {stats:?}"
        );

        let views = MaterializedViewStore::new();
        update_views(&store, &views);

        // The call site `helper()` on line 1 (0-indexed) starts right after
        // "fn run() { " — column 11 lands inside the `helper` identifier.
        let call_site_uri = url::Url::parse(uri).unwrap();
        let call_site_pos = lsp_types_max::Position {
            line: 1,
            character: 11,
        };

        let location = lookup_definition(&views, &call_site_uri, call_site_pos);
        assert!(
            location.is_some(),
            "expected a definition location for the call to `helper()`, got None"
        );
    }

    // ------------------------------------------------------------------
    // TRUE: document_triples > 0
    // ------------------------------------------------------------------
    #[test]
    fn lsif_import_loads_document_triples() {
        let lsif_path = real_lsif_path();
        if !lsif_path.exists() {
            eprintln!("SKIP: real LSIF file not found at {}", lsif_path.display());
            return;
        }
        let store = Store::new().unwrap();
        let stats = import_lsif_into_graph(&store, &lsif_path, GRAPH).unwrap();
        assert!(
            stats.document_triples > 0,
            "document_triples must be > 0; got {stats:?}"
        );
    }

    // ------------------------------------------------------------------
    // TRUE: moniker_triples > 0
    // ------------------------------------------------------------------
    #[test]
    fn lsif_import_loads_moniker_triples() {
        let lsif_path = real_lsif_path();
        if !lsif_path.exists() {
            eprintln!("SKIP: real LSIF file not found at {}", lsif_path.display());
            return;
        }
        let store = Store::new().unwrap();
        let stats = import_lsif_into_graph(&store, &lsif_path, GRAPH).unwrap();
        assert!(
            stats.moniker_triples > 0,
            "moniker_triples must be > 0; got {stats:?}"
        );
    }

    // ------------------------------------------------------------------
    // TRUE: total_triples > 0
    // ------------------------------------------------------------------
    #[test]
    fn lsif_import_stats_are_nonzero() {
        let lsif_path = real_lsif_path();
        if !lsif_path.exists() {
            eprintln!("SKIP: real LSIF file not found at {}", lsif_path.display());
            return;
        }
        let store = Store::new().unwrap();
        let stats = import_lsif_into_graph(&store, &lsif_path, GRAPH).unwrap();
        assert!(
            stats.total_triples > 0,
            "total_triples must be > 0; got {stats:?}"
        );
        assert_eq!(
            stats.total_triples,
            stats.document_triples + stats.moniker_triples + stats.reference_triples,
            "total_triples must equal sum of categories"
        );
    }

    // ------------------------------------------------------------------
    // COUNTERFACTUAL: importing same file twice produces identical stats
    // (Oxigraph is a set store — duplicate quads are silently ignored)
    // ------------------------------------------------------------------
    #[test]
    fn lsif_import_is_idempotent() {
        let lsif_path = real_lsif_path();
        if !lsif_path.exists() {
            eprintln!("SKIP: real LSIF file not found at {}", lsif_path.display());
            return;
        }
        let store = Store::new().unwrap();
        let stats1 = import_lsif_into_graph(&store, &lsif_path, GRAPH).unwrap();
        let stats2 = import_lsif_into_graph(&store, &lsif_path, GRAPH).unwrap();
        assert_eq!(
            stats1, stats2,
            "importing the same LSIF file twice must produce identical stats"
        );
    }
}