1use std::collections::BTreeMap;
29
30use serde::{Deserialize, Serialize};
31
32use super::ContentHash;
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
44#[serde(rename_all = "snake_case")]
45pub enum SymbolKindTag {
46 Function,
48 Type,
50 Enum,
52 Trait,
54 Class,
56 Interface,
58 TypeAlias,
60 Const,
62 Module,
64 Other,
66}
67
68impl SymbolKindTag {
69 pub fn tag_byte(self) -> u8 {
72 match self {
73 SymbolKindTag::Function => 1,
74 SymbolKindTag::Type => 2,
75 SymbolKindTag::Enum => 3,
76 SymbolKindTag::Trait => 4,
77 SymbolKindTag::Class => 5,
78 SymbolKindTag::Interface => 6,
79 SymbolKindTag::TypeAlias => 7,
80 SymbolKindTag::Const => 8,
81 SymbolKindTag::Module => 9,
82 SymbolKindTag::Other => 10,
83 }
84 }
85}
86
87#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
89#[serde(rename_all = "snake_case")]
90pub enum SemanticEntryKind {
91 Dir,
93 File,
95 Opaque,
100}
101
102impl SemanticEntryKind {
103 pub fn tag_byte(self) -> u8 {
105 match self {
106 SemanticEntryKind::Dir => 1,
107 SemanticEntryKind::File => 2,
108 SemanticEntryKind::Opaque => 3,
109 }
110 }
111}
112
113#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
115pub struct SymbolEntry {
116 pub name: String,
118 pub kind: SymbolKindTag,
120 pub container_path: Vec<String>,
122 pub semantic_hash: ContentHash,
126 pub span: (u32, u32),
130}
131
132impl SymbolEntry {
133 pub fn address(&self) -> String {
136 if self.container_path.is_empty() {
137 self.name.clone()
138 } else {
139 format!("{}::{}", self.container_path.join("::"), self.name)
140 }
141 }
142
143 fn sort_key(&self) -> (&[String], &str, u8, ContentHash) {
145 (
146 &self.container_path,
147 self.name.as_str(),
148 self.kind.tag_byte(),
149 self.semantic_hash,
150 )
151 }
152}
153
154#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
157pub struct ByteSpan {
158 pub start: u32,
159 pub end: u32,
160}
161
162impl ByteSpan {
163 pub fn new(start: u32, end: u32) -> Self {
164 Self { start, end }
165 }
166}
167
168#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
170#[serde(rename_all = "snake_case")]
171pub enum ScopeKind {
172 Module,
173 Type,
174 Function,
175 Block,
176}
177
178impl ScopeKind {
179 fn tag_byte(self) -> u8 {
180 match self {
181 Self::Module => 1,
182 Self::Type => 2,
183 Self::Function => 3,
184 Self::Block => 4,
185 }
186 }
187}
188
189#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
191pub struct ScopeEntry {
192 pub local_id: u32,
193 pub parent: Option<u32>,
194 pub kind: ScopeKind,
195 pub span: ByteSpan,
196}
197
198#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
200#[serde(rename_all = "snake_case")]
201pub enum ImportKindTag {
202 Use,
203 Import,
204 Reexport,
205 Dynamic,
206}
207
208impl ImportKindTag {
209 fn tag_byte(self) -> u8 {
210 match self {
211 Self::Use => 1,
212 Self::Import => 2,
213 Self::Reexport => 3,
214 Self::Dynamic => 4,
215 }
216 }
217}
218
219#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
221#[serde(rename_all = "snake_case")]
222pub enum SymbolNamespace {
223 Value,
224 Type,
225 Both,
226}
227
228impl SymbolNamespace {
229 fn tag_byte(self) -> u8 {
230 match self {
231 Self::Value => 1,
232 Self::Type => 2,
233 Self::Both => 3,
234 }
235 }
236}
237
238#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
240pub struct ImportBinding {
241 pub imported: String,
242 pub local: String,
243 pub namespace: SymbolNamespace,
244}
245
246#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
248pub struct ImportEntry {
249 pub kind: ImportKindTag,
250 pub module_specifier: String,
251 pub bindings: Vec<ImportBinding>,
252 pub scope: u32,
253 pub span: ByteSpan,
254}
255
256#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
258#[serde(rename_all = "snake_case")]
259pub enum OccurrenceRole {
260 Definition,
261 Reference,
262 Call,
263 TypeReference,
264}
265
266impl OccurrenceRole {
267 fn tag_byte(self) -> u8 {
268 match self {
269 Self::Definition => 1,
270 Self::Reference => 2,
271 Self::Call => 3,
272 Self::TypeReference => 4,
273 }
274 }
275}
276
277#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
279pub struct OccurrenceEntry {
280 pub local_id: u32,
281 pub role: OccurrenceRole,
282 pub name: String,
283 pub qualifier: Vec<String>,
284 pub namespace: SymbolNamespace,
285 pub scope: u32,
286 pub span: ByteSpan,
287}
288
289#[derive(Clone, Debug, Default, PartialEq, Eq)]
291pub struct SemanticFileFacts {
292 pub symbols: Vec<SymbolEntry>,
293 pub scopes: Vec<ScopeEntry>,
294 pub imports: Vec<ImportEntry>,
295 pub occurrences: Vec<OccurrenceEntry>,
296}
297
298pub fn compute_symbol_semantic_hash(kind: SymbolKindTag, token_stream: &[u8]) -> ContentHash {
309 let mut buf = Vec::with_capacity(2 + token_stream.len());
310 buf.push(kind.tag_byte());
311 buf.push(0x00);
312 buf.extend_from_slice(token_stream);
313 ContentHash::compute_typed("hd-sem-sym-v1", &buf)
314}
315
316pub fn compute_file_scaffold_hash(token_stream: &[u8]) -> ContentHash {
326 ContentHash::compute_typed("hd-sem-scaffold-v1", token_stream)
327}
328
329pub fn compute_file_semantic_digest(
339 scaffold_hash: ContentHash,
340 symbols: &[SymbolEntry],
341 scopes: &[ScopeEntry],
342 imports: &[ImportEntry],
343 occurrences: &[OccurrenceEntry],
344) -> ContentHash {
345 let mut buf = Vec::new();
346 buf.extend_from_slice(scaffold_hash.as_bytes());
347 buf.extend_from_slice(&(symbols.len() as u32).to_le_bytes());
348 for symbol in symbols {
349 buf.extend_from_slice(&(symbol.container_path.len() as u32).to_le_bytes());
350 for segment in &symbol.container_path {
351 buf.extend_from_slice(&(segment.len() as u32).to_le_bytes());
352 buf.extend_from_slice(segment.as_bytes());
353 }
354 buf.extend_from_slice(&(symbol.name.len() as u32).to_le_bytes());
355 buf.extend_from_slice(symbol.name.as_bytes());
356 buf.push(symbol.kind.tag_byte());
357 buf.extend_from_slice(symbol.semantic_hash.as_bytes());
358 }
359 buf.extend_from_slice(&(scopes.len() as u32).to_le_bytes());
360 for scope in scopes {
361 buf.extend_from_slice(&scope.local_id.to_le_bytes());
362 match scope.parent {
363 Some(parent) => {
364 buf.push(1);
365 buf.extend_from_slice(&parent.to_le_bytes());
366 }
367 None => buf.push(0),
368 }
369 buf.push(scope.kind.tag_byte());
370 }
371 buf.extend_from_slice(&(imports.len() as u32).to_le_bytes());
372 for import in imports {
373 buf.push(import.kind.tag_byte());
374 push_str(&mut buf, &import.module_specifier);
375 buf.extend_from_slice(&(import.bindings.len() as u32).to_le_bytes());
376 for binding in &import.bindings {
377 push_str(&mut buf, &binding.imported);
378 push_str(&mut buf, &binding.local);
379 buf.push(binding.namespace.tag_byte());
380 }
381 buf.extend_from_slice(&import.scope.to_le_bytes());
382 }
383 buf.extend_from_slice(&(occurrences.len() as u32).to_le_bytes());
384 for occurrence in occurrences {
385 buf.extend_from_slice(&occurrence.local_id.to_le_bytes());
386 buf.push(occurrence.role.tag_byte());
387 push_str(&mut buf, &occurrence.name);
388 buf.extend_from_slice(&(occurrence.qualifier.len() as u32).to_le_bytes());
389 for segment in &occurrence.qualifier {
390 push_str(&mut buf, segment);
391 }
392 buf.push(occurrence.namespace.tag_byte());
393 buf.extend_from_slice(&occurrence.scope.to_le_bytes());
394 }
395 ContentHash::compute_typed("hd-sem-file-v3", &buf)
396}
397
398fn push_str(buf: &mut Vec<u8>, value: &str) {
399 buf.extend_from_slice(&(value.len() as u32).to_le_bytes());
400 buf.extend_from_slice(value.as_bytes());
401}
402
403pub fn compute_dir_semantic_digest(entries: &[SemanticTreeEntry]) -> ContentHash {
409 let mut buf = Vec::new();
410 for entry in entries {
411 buf.extend_from_slice(&(entry.name.len() as u32).to_le_bytes());
412 buf.extend_from_slice(entry.name.as_bytes());
413 buf.push(entry.kind.tag_byte());
414 buf.extend_from_slice(entry.semantic_digest.as_bytes());
415 }
416 ContentHash::compute_typed("hd-sem-dir-v2", &buf)
417}
418
419#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
422pub struct SemanticFileNode {
423 pub format_version: u8,
424 pub language: String,
425 pub grammar_version: String,
426 pub extractor_version: u32,
427 pub source_blob: ContentHash,
429 pub scaffold_hash: ContentHash,
433 pub symbols: Vec<SymbolEntry>,
435 pub scopes: Vec<ScopeEntry>,
437 pub imports: Vec<ImportEntry>,
439 pub occurrences: Vec<OccurrenceEntry>,
441 pub semantic_digest: ContentHash,
443}
444
445impl SemanticFileNode {
446 pub const FORMAT_VERSION: u8 = 2;
447
448 pub fn new(
451 language: impl Into<String>,
452 grammar_version: impl Into<String>,
453 extractor_version: u32,
454 source_blob: ContentHash,
455 scaffold_hash: ContentHash,
456 facts: SemanticFileFacts,
457 ) -> Self {
458 let SemanticFileFacts {
459 mut symbols,
460 mut scopes,
461 mut imports,
462 mut occurrences,
463 } = facts;
464 symbols.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
465 scopes.sort_by_key(|scope| scope.local_id);
466 imports.sort_by(|a, b| {
467 (a.kind, &a.module_specifier, a.scope, &a.bindings).cmp(&(
468 b.kind,
469 &b.module_specifier,
470 b.scope,
471 &b.bindings,
472 ))
473 });
474 occurrences.sort_by_key(|occurrence| occurrence.local_id);
475 let semantic_digest =
476 compute_file_semantic_digest(scaffold_hash, &symbols, &scopes, &imports, &occurrences);
477 Self {
478 format_version: Self::FORMAT_VERSION,
479 language: language.into(),
480 grammar_version: grammar_version.into(),
481 extractor_version,
482 source_blob,
483 scaffold_hash,
484 symbols,
485 scopes,
486 imports,
487 occurrences,
488 semantic_digest,
489 }
490 }
491
492 pub fn encode(&self) -> Result<Vec<u8>, SemanticIndexError> {
493 rmp_serde::to_vec_named(self).map_err(|err| SemanticIndexError::Encoding(err.to_string()))
494 }
495
496 pub fn decode(bytes: &[u8]) -> Result<Self, SemanticIndexError> {
497 let node: Self = rmp_serde::from_slice(bytes)
498 .map_err(|err| SemanticIndexError::Encoding(err.to_string()))?;
499 if node.format_version != Self::FORMAT_VERSION {
500 return Err(SemanticIndexError::UnsupportedVersion(node.format_version));
501 }
502 Ok(node)
503 }
504
505 pub fn symbol_by_address(&self, address: &str) -> Option<&SymbolEntry> {
507 self.symbols.iter().find(|s| s.address() == address)
508 }
509}
510
511#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
513pub struct SemanticTreeEntry {
514 pub name: String,
515 pub kind: SemanticEntryKind,
516 pub node: ContentHash,
520 pub semantic_digest: ContentHash,
522}
523
524#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
526pub struct SemanticTreeNode {
527 pub format_version: u8,
528 pub entries: Vec<SemanticTreeEntry>,
530}
531
532impl SemanticTreeNode {
533 pub const FORMAT_VERSION: u8 = 1;
534
535 pub fn new(mut entries: Vec<SemanticTreeEntry>) -> (Self, ContentHash) {
538 entries.sort_by(|a, b| a.name.cmp(&b.name));
539 let digest = compute_dir_semantic_digest(&entries);
540 (
541 Self {
542 format_version: Self::FORMAT_VERSION,
543 entries,
544 },
545 digest,
546 )
547 }
548
549 pub fn semantic_digest(&self) -> ContentHash {
551 compute_dir_semantic_digest(&self.entries)
552 }
553
554 pub fn encode(&self) -> Result<Vec<u8>, SemanticIndexError> {
555 rmp_serde::to_vec_named(self).map_err(|err| SemanticIndexError::Encoding(err.to_string()))
556 }
557
558 pub fn decode(bytes: &[u8]) -> Result<Self, SemanticIndexError> {
559 let node: Self = rmp_serde::from_slice(bytes)
560 .map_err(|err| SemanticIndexError::Encoding(err.to_string()))?;
561 if node.format_version != Self::FORMAT_VERSION {
562 return Err(SemanticIndexError::UnsupportedVersion(node.format_version));
563 }
564 Ok(node)
565 }
566
567 pub fn get(&self, name: &str) -> Option<&SemanticTreeEntry> {
568 self.entries
569 .binary_search_by(|e| e.name.as_str().cmp(name))
570 .ok()
571 .map(|i| &self.entries[i])
572 }
573}
574
575#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
578pub struct SemanticIndexRoot {
579 pub format_version: u8,
580 pub extractor_version: u32,
581 pub grammars: BTreeMap<String, String>,
583 pub tree: ContentHash,
585 pub semantic_digest: ContentHash,
587 #[serde(default)]
591 pub binding_delta: Option<ContentHash>,
592 #[serde(default)]
595 pub importer_index: Option<ContentHash>,
596 #[serde(default)]
598 pub resolver_version: u32,
599}
600
601impl SemanticIndexRoot {
602 pub const FORMAT_VERSION: u8 = 1;
603
604 pub fn new(
605 extractor_version: u32,
606 grammars: BTreeMap<String, String>,
607 tree: ContentHash,
608 semantic_digest: ContentHash,
609 ) -> Self {
610 Self {
611 format_version: Self::FORMAT_VERSION,
612 extractor_version,
613 grammars,
614 tree,
615 semantic_digest,
616 binding_delta: None,
617 importer_index: None,
618 resolver_version: 0,
619 }
620 }
621
622 pub fn with_binding_delta(mut self, binding_delta: ContentHash, resolver_version: u32) -> Self {
624 self.binding_delta = Some(binding_delta);
625 self.resolver_version = resolver_version;
626 self
627 }
628
629 pub fn with_importer_index(mut self, importer_index: ContentHash) -> Self {
631 self.importer_index = Some(importer_index);
632 self
633 }
634
635 pub fn encode(&self) -> Result<Vec<u8>, SemanticIndexError> {
636 rmp_serde::to_vec_named(self).map_err(|err| SemanticIndexError::Encoding(err.to_string()))
637 }
638
639 pub fn decode(bytes: &[u8]) -> Result<Self, SemanticIndexError> {
640 let root: Self = rmp_serde::from_slice(bytes)
641 .map_err(|err| SemanticIndexError::Encoding(err.to_string()))?;
642 if root.format_version != Self::FORMAT_VERSION {
643 return Err(SemanticIndexError::UnsupportedVersion(root.format_version));
644 }
645 Ok(root)
646 }
647}
648
649#[derive(Debug, thiserror::Error)]
650pub enum SemanticIndexError {
651 #[error("unsupported semantic index node version {0}")]
652 UnsupportedVersion(u8),
653 #[error("semantic index node encoding error: {0}")]
654 Encoding(String),
655}
656
657#[cfg(test)]
658mod tests {
659 use super::*;
660
661 fn h(seed: u8) -> ContentHash {
662 ContentHash::from_bytes([seed; 32])
663 }
664
665 fn sym(name: &str, container: &[&str], kind: SymbolKindTag, span: (u32, u32)) -> SymbolEntry {
666 SymbolEntry {
667 name: name.to_string(),
668 kind,
669 container_path: container.iter().map(|s| s.to_string()).collect(),
670 semantic_hash: ContentHash::compute(name.as_bytes()),
671 span,
672 }
673 }
674
675 #[test]
676 fn file_digest_excludes_span() {
677 let a = SemanticFileNode::new(
678 "rust",
679 "0.24",
680 1,
681 h(1),
682 h(0),
683 SemanticFileFacts {
684 symbols: vec![sym("foo", &[], SymbolKindTag::Function, (10, 20))],
685 ..SemanticFileFacts::default()
686 },
687 );
688 let b = SemanticFileNode::new(
690 "rust",
691 "0.24",
692 1,
693 h(1),
694 h(0),
695 SemanticFileFacts {
696 symbols: vec![sym("foo", &[], SymbolKindTag::Function, (99, 120))],
697 ..SemanticFileFacts::default()
698 },
699 );
700 assert_eq!(
701 a.semantic_digest, b.semantic_digest,
702 "span must not affect the file semantic_digest"
703 );
704 }
705
706 #[test]
707 fn semantic_content_hash_excludes_all_provenance_spans() {
708 let source_blob = ContentHash::compute(b"use crate::api::greet; greet();");
709 let scope = |span| ScopeEntry {
710 local_id: 0,
711 parent: None,
712 kind: ScopeKind::Module,
713 span,
714 };
715 let import = |module_specifier: &str, span| ImportEntry {
716 kind: ImportKindTag::Use,
717 module_specifier: module_specifier.to_string(),
718 bindings: vec![ImportBinding {
719 imported: "greet".to_string(),
720 local: "greet".to_string(),
721 namespace: SymbolNamespace::Both,
722 }],
723 scope: 0,
724 span,
725 };
726 let occurrence = |span| OccurrenceEntry {
727 local_id: 0,
728 role: OccurrenceRole::Call,
729 name: "greet".to_string(),
730 qualifier: Vec::new(),
731 namespace: SymbolNamespace::Value,
732 scope: 0,
733 span,
734 };
735 let node = |scope_span, import_spans: [ByteSpan; 2], occurrence_span| {
736 SemanticFileNode::new(
737 "rust",
738 "0.24",
739 4,
740 source_blob,
741 h(0),
742 SemanticFileFacts {
743 symbols: vec![],
744 scopes: vec![scope(scope_span)],
745 imports: vec![
746 import("crate::api", import_spans[0]),
747 import("crate::util", import_spans[1]),
748 ],
749 occurrences: vec![occurrence(occurrence_span)],
750 },
751 )
752 };
753 let a = node(
754 ByteSpan::new(0, 38),
755 [ByteSpan::new(0, 22), ByteSpan::new(23, 32)],
756 ByteSpan::new(23, 30),
757 );
758 let b = node(
759 ByteSpan::new(10, 48),
760 [ByteSpan::new(33, 42), ByteSpan::new(10, 32)],
761 ByteSpan::new(33, 40),
762 );
763
764 assert_eq!(
765 a.semantic_digest, b.semantic_digest,
766 "span-only differences must not affect semantic content identity"
767 );
768 assert_ne!(
769 a.encode().unwrap(),
770 b.encode().unwrap(),
771 "encoded provenance still records the distinct spans"
772 );
773 }
774
775 #[test]
776 fn file_node_roundtrip_preserves_source_local_facts() {
777 let node = SemanticFileNode::new(
778 "typescript",
779 "0.23",
780 4,
781 h(1),
782 h(0),
783 SemanticFileFacts {
784 symbols: vec![sym("run", &[], SymbolKindTag::Function, (2, 4))],
785 scopes: vec![ScopeEntry {
786 local_id: 0,
787 parent: None,
788 kind: ScopeKind::Module,
789 span: ByteSpan::new(0, 64),
790 }],
791 imports: vec![ImportEntry {
792 kind: ImportKindTag::Import,
793 module_specifier: "./api".to_string(),
794 bindings: vec![ImportBinding {
795 imported: "greet".to_string(),
796 local: "hello".to_string(),
797 namespace: SymbolNamespace::Value,
798 }],
799 scope: 0,
800 span: ByteSpan::new(0, 39),
801 }],
802 occurrences: vec![OccurrenceEntry {
803 local_id: 0,
804 role: OccurrenceRole::Call,
805 name: "hello".to_string(),
806 qualifier: Vec::new(),
807 namespace: SymbolNamespace::Value,
808 scope: 0,
809 span: ByteSpan::new(50, 55),
810 }],
811 },
812 );
813
814 assert_eq!(
815 SemanticFileNode::decode(&node.encode().unwrap()).unwrap(),
816 node
817 );
818 }
819
820 #[test]
821 fn file_digest_changes_on_symbol_hash_change() {
822 let mut s = sym("foo", &[], SymbolKindTag::Function, (1, 2));
823 let d1 = compute_file_semantic_digest(h(0), std::slice::from_ref(&s), &[], &[], &[]);
824 s.semantic_hash = ContentHash::compute(b"different-body");
825 let d2 = compute_file_semantic_digest(h(0), std::slice::from_ref(&s), &[], &[], &[]);
826 assert_ne!(d1, d2);
827 }
828
829 #[test]
830 fn file_digest_changes_on_scaffold_change() {
831 let syms = [sym("foo", &[], SymbolKindTag::Function, (1, 2))];
832 let d1 = compute_file_semantic_digest(
833 compute_file_scaffold_hash(b"use a;"),
834 &syms,
835 &[],
836 &[],
837 &[],
838 );
839 let d2 = compute_file_semantic_digest(
840 compute_file_scaffold_hash(b"use b;"),
841 &syms,
842 &[],
843 &[],
844 &[],
845 );
846 assert_ne!(
847 d1, d2,
848 "scaffold (non-definition top-level tokens) must affect the file digest"
849 );
850 }
851
852 #[test]
853 fn file_digest_framing_is_unambiguous() {
854 let one = sym("f", &["a::b"], SymbolKindTag::Function, (0, 0));
857 let two = sym("f", &["a", "b"], SymbolKindTag::Function, (0, 0));
858 assert_ne!(
859 compute_file_semantic_digest(h(0), &[one], &[], &[], &[]),
860 compute_file_semantic_digest(h(0), &[two], &[], &[], &[]),
861 );
862 }
863
864 #[test]
865 fn symbol_hash_stable_and_kind_sensitive() {
866 let ts = b"some token stream";
867 let a = compute_symbol_semantic_hash(SymbolKindTag::Function, ts);
868 let b = compute_symbol_semantic_hash(SymbolKindTag::Function, ts);
869 assert_eq!(a, b);
870 let c = compute_symbol_semantic_hash(SymbolKindTag::Type, ts);
871 assert_ne!(a, c, "kind participates in the symbol hash");
872 }
873
874 #[test]
875 fn symbols_sorted_canonically() {
876 let node = SemanticFileNode::new(
877 "rust",
878 "0.24",
879 1,
880 h(1),
881 h(0),
882 SemanticFileFacts {
883 symbols: vec![
884 sym("zed", &[], SymbolKindTag::Function, (1, 1)),
885 sym("abe", &["Impl"], SymbolKindTag::Function, (2, 2)),
886 sym("abe", &[], SymbolKindTag::Function, (3, 3)),
887 ],
888 ..SemanticFileFacts::default()
889 },
890 );
891 let names: Vec<_> = node.symbols.iter().map(|s| s.address()).collect();
892 assert_eq!(names, vec!["abe", "zed", "Impl::abe"]);
893 }
894
895 #[test]
896 fn dir_digest_stable_and_roundtrip() {
897 let e = SemanticTreeEntry {
898 name: "a.rs".to_string(),
899 kind: SemanticEntryKind::File,
900 node: h(5),
901 semantic_digest: h(6),
902 };
903 let (node, digest) = SemanticTreeNode::new(vec![e.clone()]);
904 assert_eq!(node.semantic_digest(), digest);
905 let bytes = node.encode().unwrap();
906 assert_eq!(SemanticTreeNode::decode(&bytes).unwrap(), node);
907 }
908
909 #[test]
910 fn address_spelling() {
911 assert_eq!(
912 sym("foo", &[], SymbolKindTag::Function, (0, 0)).address(),
913 "foo"
914 );
915 assert_eq!(
916 sym("open", &["Repository"], SymbolKindTag::Function, (0, 0)).address(),
917 "Repository::open"
918 );
919 }
920}