1use std::{
24 any::type_name,
25 fmt,
26 hash::{BuildHasher, Hash, Hasher},
27 marker::PhantomData,
28};
29
30use la_arena::{Arena, Idx, RawIdx};
31use rustc_hash::{FxBuildHasher, FxHashMap};
32use syntax::{
33 AstNode, AstPtr, SyntaxKind, SyntaxNode, SyntaxNodePtr,
34 ast::{self, HasName},
35 match_ast,
36};
37
38pub const ROOT_ERASED_FILE_AST_ID: ErasedFileAstId =
43 ErasedFileAstId(pack_hash_index_and_kind(0, 0, ErasedFileAstIdKind::Root as u32));
44
45pub const FIXUP_ERASED_FILE_AST_ID_MARKER: ErasedFileAstId =
49 ErasedFileAstId(pack_hash_index_and_kind(0, 0, ErasedFileAstIdKind::Fixup as u32));
50
51pub const NO_DOWNMAP_ERASED_FILE_AST_ID_MARKER: ErasedFileAstId =
54 ErasedFileAstId(pack_hash_index_and_kind(0, 0, ErasedFileAstIdKind::NoDownmap as u32));
55
56#[derive(Clone, Copy, PartialEq, Eq, Hash)]
58pub struct ErasedFileAstId(u32);
59
60impl fmt::Debug for ErasedFileAstId {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 let kind = self.kind();
63 macro_rules! kind {
64 ($($kind:ident),* $(,)?) => {
65 if false {
66 match ErasedFileAstIdKind::Root {
68 $( ErasedFileAstIdKind::$kind => {} )*
69 }
70 unreachable!()
71 }
72 $( else if kind == ErasedFileAstIdKind::$kind as u32 {
73 stringify!($kind)
74 } )*
75 else {
76 "Unknown"
77 }
78 };
79 }
80 let kind = kind!(
81 Root,
82 Enum,
83 Struct,
84 Union,
85 ExternCrate,
86 MacroDef,
87 MacroRules,
88 Module,
89 Static,
90 Trait,
91 Variant,
92 Const,
93 Fn,
94 MacroCall,
95 TypeAlias,
96 ExternBlock,
97 Use,
98 Impl,
99 BlockExpr,
100 AsmExpr,
101 Fixup,
102 NoDownmap,
103 );
104 if f.alternate() {
105 write!(f, "{kind}[{:04X}, {}]", self.hash_value(), self.index())
106 } else {
107 f.debug_struct("ErasedFileAstId")
108 .field("kind", &format_args!("{kind}"))
109 .field("index", &self.index())
110 .field("hash", &format_args!("{:04X}", self.hash_value()))
111 .finish()
112 }
113 }
114}
115
116#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
117#[repr(u8)]
118enum ErasedFileAstIdKind {
119 Fixup = 0,
121 Enum,
123 Struct,
124 Union,
125 ExternCrate,
126 MacroDef,
127 MacroRules,
128 Module,
129 Static,
130 Trait,
131 Variant,
134 Const,
135 Fn,
136 MacroCall,
137 TypeAlias,
138 ExternBlock,
141 Use,
149 Impl,
151 BlockExpr,
153 AsmExpr,
157 NoDownmap,
160 Root,
162}
163
164const HASH_BITS: u32 = 16;
166const INDEX_BITS: u32 = 11;
167const KIND_BITS: u32 = 5;
168const _: () = assert!(ErasedFileAstIdKind::Root as u32 <= ((1 << KIND_BITS) - 1));
169const _: () = assert!(HASH_BITS + INDEX_BITS + KIND_BITS == u32::BITS);
170
171#[inline]
172const fn u16_hash(hash: u64) -> u16 {
173 const K: u16 = 0xecc5;
177 let (part1, part2, part3, part4) =
178 (hash as u16, (hash >> 16) as u16, (hash >> 32) as u16, (hash >> 48) as u16);
179 part1
180 .wrapping_add(part2)
181 .wrapping_mul(K)
182 .wrapping_add(part3)
183 .wrapping_mul(K)
184 .wrapping_add(part4)
185 .wrapping_mul(K)
186}
187
188#[inline]
189const fn pack_hash_index_and_kind(hash: u16, index: u32, kind: u32) -> u32 {
190 (hash as u32) | (index << HASH_BITS) | (kind << (HASH_BITS + INDEX_BITS))
191}
192
193impl ErasedFileAstId {
194 #[inline]
195 fn hash_value(self) -> u16 {
196 self.0 as u16
197 }
198
199 #[inline]
200 fn index(self) -> u32 {
201 (self.0 << KIND_BITS) >> (HASH_BITS + KIND_BITS)
202 }
203
204 #[inline]
205 fn kind(self) -> u32 {
206 self.0 >> (HASH_BITS + INDEX_BITS)
207 }
208
209 fn ast_id_for(
210 node: &SyntaxNode,
211 index_map: &mut ErasedAstIdNextIndexMap,
212 parent: Option<&ErasedFileAstId>,
213 ) -> Option<ErasedFileAstId> {
214 has_name_ast_id(node, index_map)
216 .or_else(|| assoc_item_ast_id(node, index_map, parent))
217 .or_else(|| extern_block_ast_id(node, index_map))
218 .or_else(|| use_ast_id(node, index_map))
219 .or_else(|| impl_ast_id(node, index_map))
220 .or_else(|| asm_expr_ast_id(node, index_map))
221 }
222
223 fn should_alloc(node: &SyntaxNode) -> bool {
224 let kind = node.kind();
225 should_alloc_has_name(kind)
226 || should_alloc_assoc_item(kind)
227 || ast::ExternBlock::can_cast(kind)
228 || ast::Use::can_cast(kind)
229 || ast::Impl::can_cast(kind)
230 || ast::AsmExpr::can_cast(kind)
231 }
232
233 #[inline]
234 pub fn into_raw(self) -> u32 {
235 self.0
236 }
237
238 #[inline]
239 pub const fn from_raw(v: u32) -> Self {
240 Self(v)
241 }
242}
243
244pub trait AstIdNode: AstNode {}
245
246pub struct FileAstId<N> {
248 raw: ErasedFileAstId,
249 _marker: PhantomData<fn() -> N>,
250}
251
252impl<N> Clone for FileAstId<N> {
254 #[inline]
255 fn clone(&self) -> FileAstId<N> {
256 *self
257 }
258}
259impl<N> Copy for FileAstId<N> {}
260
261impl<N> PartialEq for FileAstId<N> {
262 fn eq(&self, other: &Self) -> bool {
263 self.raw == other.raw
264 }
265}
266impl<N> Eq for FileAstId<N> {}
267impl<N> Hash for FileAstId<N> {
268 fn hash<H: Hasher>(&self, hasher: &mut H) {
269 self.raw.hash(hasher);
270 }
271}
272
273impl<N> fmt::Debug for FileAstId<N> {
274 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275 write!(f, "FileAstId::<{}>({:?})", type_name::<N>(), self.raw)
276 }
277}
278
279impl<N> FileAstId<N> {
280 #[inline]
282 pub fn upcast<M: AstIdNode>(self) -> FileAstId<M>
283 where
284 N: Into<M>,
285 {
286 FileAstId { raw: self.raw, _marker: PhantomData }
287 }
288
289 #[inline]
290 pub fn erase(self) -> ErasedFileAstId {
291 self.raw
292 }
293}
294
295#[derive(Hash)]
296struct ErasedHasNameFileAstId<'a> {
297 name: &'a str,
298}
299
300#[derive(Hash)]
302struct ErasedAssocItemFileAstId<'a> {
303 parent: Option<ErasedFileAstId>,
308 properties: ErasedHasNameFileAstId<'a>,
309}
310
311#[derive(Debug, Clone, PartialEq, Eq, Hash)]
312struct ImplFileAstId<'a> {
313 self_ty_name: Option<&'a str>,
315 trait_name: Option<&'a str>,
317}
318
319#[derive(Debug, Clone, PartialEq, Eq, Hash)]
320struct BlockExprFileAstId {
321 parent: Option<ErasedFileAstId>,
322}
323
324impl AstIdNode for ast::ExternBlock {}
325
326fn extern_block_ast_id(
327 node: &SyntaxNode,
328 index_map: &mut ErasedAstIdNextIndexMap,
329) -> Option<ErasedFileAstId> {
330 if ast::ExternBlock::can_cast(node.kind()) {
331 Some(index_map.new_id(ErasedFileAstIdKind::ExternBlock, ()))
332 } else {
333 None
334 }
335}
336
337impl AstIdNode for ast::Use {}
338
339fn use_ast_id(
340 node: &SyntaxNode,
341 index_map: &mut ErasedAstIdNextIndexMap,
342) -> Option<ErasedFileAstId> {
343 if ast::Use::can_cast(node.kind()) {
344 Some(index_map.new_id(ErasedFileAstIdKind::Use, ()))
345 } else {
346 None
347 }
348}
349
350impl AstIdNode for ast::AsmExpr {}
351
352fn asm_expr_ast_id(
353 node: &SyntaxNode,
354 index_map: &mut ErasedAstIdNextIndexMap,
355) -> Option<ErasedFileAstId> {
356 if ast::AsmExpr::can_cast(node.kind()) {
357 Some(index_map.new_id(ErasedFileAstIdKind::AsmExpr, ()))
358 } else {
359 None
360 }
361}
362
363impl AstIdNode for ast::Impl {}
364
365fn impl_ast_id(
366 node: &SyntaxNode,
367 index_map: &mut ErasedAstIdNextIndexMap,
368) -> Option<ErasedFileAstId> {
369 if let Some(node) = ast::Impl::cast(node.clone()) {
370 let type_as_name = |ty: Option<ast::Type>| match ty? {
371 ast::Type::PathType(it) => Some(it.path()?.segment()?.name_ref()?),
372 _ => None,
373 };
374 let self_ty_name = type_as_name(node.self_ty());
375 let trait_name = type_as_name(node.trait_());
376 let data = ImplFileAstId {
377 self_ty_name: self_ty_name.as_ref().map(|it| it.text_non_mutable()),
378 trait_name: trait_name.as_ref().map(|it| it.text_non_mutable()),
379 };
380 Some(index_map.new_id(ErasedFileAstIdKind::Impl, data))
381 } else {
382 None
383 }
384}
385
386fn block_expr_ast_id(
391 node: &SyntaxNode,
392 index_map: &mut ErasedAstIdNextIndexMap,
393 parent: Option<&ErasedFileAstId>,
394) -> Option<ErasedFileAstId> {
395 if ast::BlockExpr::can_cast(node.kind()) {
396 Some(
397 index_map.new_id(
398 ErasedFileAstIdKind::BlockExpr,
399 BlockExprFileAstId { parent: parent.copied() },
400 ),
401 )
402 } else {
403 None
404 }
405}
406
407#[derive(Default)]
408struct ErasedAstIdNextIndexMap(FxHashMap<(ErasedFileAstIdKind, u16), u32>);
409
410impl ErasedAstIdNextIndexMap {
411 #[inline]
412 fn new_id(&mut self, kind: ErasedFileAstIdKind, data: impl Hash) -> ErasedFileAstId {
413 let hash = FxBuildHasher.hash_one(&data);
414 let initial_hash = u16_hash(hash);
415 let mut hash = initial_hash;
419 let index = loop {
420 match self.0.entry((kind, hash)) {
421 std::collections::hash_map::Entry::Occupied(mut entry) => {
422 let i = entry.get_mut();
423 if *i < ((1 << INDEX_BITS) - 1) {
424 *i += 1;
425 break *i;
426 }
427 }
428 std::collections::hash_map::Entry::Vacant(entry) => {
429 entry.insert(0);
430 break 0;
431 }
432 }
433 hash = hash.wrapping_add(1);
434 if hash == initial_hash {
435 panic!("you have way too many items in the same file!");
437 }
438 };
439 let kind = kind as u32;
440 ErasedFileAstId(pack_hash_index_and_kind(hash, index, kind))
441 }
442}
443
444macro_rules! register_enum_ast_id {
445 (impl $AstIdNode:ident for $($ident:ident),+ ) => {
446 $(
447 impl $AstIdNode for ast::$ident {}
448 )+
449 };
450}
451register_enum_ast_id! {
452 impl AstIdNode for
453 Item, AnyHasGenericParams, Adt, Macro,
454 AssocItem
455}
456
457macro_rules! register_has_name_ast_id {
458 (impl $AstIdNode:ident for $($ident:ident = $name_method:ident),+ ) => {
459 $(
460 impl $AstIdNode for ast::$ident {}
461 )+
462
463 fn has_name_ast_id(node: &SyntaxNode, index_map: &mut ErasedAstIdNextIndexMap) -> Option<ErasedFileAstId> {
464 match_ast! {
465 match node {
466 $(
467 ast::$ident(node) => {
468 let name = node.$name_method();
469 let name = name.as_ref().map_or("", |it| it.text_non_mutable());
470 let result = ErasedHasNameFileAstId {
471 name,
472 };
473 Some(index_map.new_id(ErasedFileAstIdKind::$ident, result))
474 },
475 )*
476 _ => None,
477 }
478 }
479 }
480
481 fn should_alloc_has_name(kind: SyntaxKind) -> bool {
482 false $( || ast::$ident::can_cast(kind) )*
483 }
484 };
485}
486register_has_name_ast_id! {
487 impl AstIdNode for
488 Enum = name,
489 Struct = name,
490 Union = name,
491 ExternCrate = name_ref,
492 MacroDef = name,
493 MacroRules = name,
494 Module = name,
495 Static = name,
496 Trait = name
497}
498
499macro_rules! register_assoc_item_ast_id {
500 (impl $AstIdNode:ident for $($ident:ident = $name_callback:expr),+ ) => {
501 $(
502 impl $AstIdNode for ast::$ident {}
503 )+
504
505 fn assoc_item_ast_id(
506 node: &SyntaxNode,
507 index_map: &mut ErasedAstIdNextIndexMap,
508 parent: Option<&ErasedFileAstId>,
509 ) -> Option<ErasedFileAstId> {
510 match_ast! {
511 match node {
512 $(
513 ast::$ident(node) => {
514 let name = $name_callback(node);
515 let name = name.as_ref().map_or("", |it| it.text_non_mutable());
516 let properties = ErasedHasNameFileAstId {
517 name,
518 };
519 let result = ErasedAssocItemFileAstId {
520 parent: parent.copied(),
521 properties,
522 };
523 Some(index_map.new_id(ErasedFileAstIdKind::$ident, result))
524 },
525 )*
526 _ => None,
527 }
528 }
529 }
530
531 fn should_alloc_assoc_item(kind: SyntaxKind) -> bool {
532 false $( || ast::$ident::can_cast(kind) )*
533 }
534 };
535}
536register_assoc_item_ast_id! {
537 impl AstIdNode for
538 Variant = |it: ast::Variant| it.name(),
539 Const = |it: ast::Const| it.name(),
540 Fn = |it: ast::Fn| it.name(),
541 MacroCall = |it: ast::MacroCall| it.path().and_then(|path| path.segment()?.name_ref()),
542 TypeAlias = |it: ast::TypeAlias| it.name()
543}
544
545#[derive(Default)]
547pub struct AstIdMap {
548 arena: Arena<(SyntaxNodePtr, ErasedFileAstId)>,
550 ptr_map: hashbrown::HashTable<ArenaId>,
552 id_map: hashbrown::HashTable<ArenaId>,
554}
555
556type ArenaId = Idx<(SyntaxNodePtr, ErasedFileAstId)>;
557
558impl fmt::Debug for AstIdMap {
559 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
560 f.debug_struct("AstIdMap").field("arena", &self.arena).finish()
561 }
562}
563
564impl PartialEq for AstIdMap {
565 fn eq(&self, other: &Self) -> bool {
566 self.arena == other.arena
567 }
568}
569impl Eq for AstIdMap {}
570
571#[derive(Debug, Clone, Copy, PartialEq, Eq)]
572enum ContainsItems {
573 Yes,
574 No,
575}
576
577impl AstIdMap {
578 pub fn from_source(node: &SyntaxNode) -> AstIdMap {
579 assert!(node.parent().is_none());
580 let mut res = AstIdMap::default();
581 let mut index_map = ErasedAstIdNextIndexMap::default();
582
583 res.arena.alloc((SyntaxNodePtr::new(node), ROOT_ERASED_FILE_AST_ID));
585
586 let mut blocks = Vec::new();
604 let mut curr_layer = Vec::with_capacity(32);
605 curr_layer.push((node.clone(), None));
606 let mut next_layer = Vec::with_capacity(32);
607 while !curr_layer.is_empty() {
608 curr_layer.drain(..).for_each(|(node, parent_idx)| {
609 let mut preorder = node.preorder();
610 while let Some(event) = preorder.next() {
611 match event {
612 syntax::WalkEvent::Enter(node) => {
613 if ast::BlockExpr::can_cast(node.kind()) {
614 blocks.push((node, ContainsItems::No));
615 } else if ErasedFileAstId::should_alloc(&node) {
616 if let Some((
621 last_block_node,
622 already_allocated @ ContainsItems::No,
623 )) = blocks.last_mut()
624 {
625 let block_ast_id = block_expr_ast_id(
626 last_block_node,
627 &mut index_map,
628 parent_of(parent_idx, &res),
629 )
630 .expect("not a BlockExpr");
631 res.arena
632 .alloc((SyntaxNodePtr::new(last_block_node), block_ast_id));
633 *already_allocated = ContainsItems::Yes;
634 }
635
636 let parent = parent_of(parent_idx, &res);
637 let ast_id =
638 ErasedFileAstId::ast_id_for(&node, &mut index_map, parent)
639 .expect("this node should have an ast id");
640 let idx = res.arena.alloc((SyntaxNodePtr::new(&node), ast_id));
641
642 next_layer.extend(node.children().map(|child| (child, Some(idx))));
643 preorder.skip_subtree();
644 }
645 }
646 syntax::WalkEvent::Leave(node) => {
647 if ast::BlockExpr::can_cast(node.kind()) {
648 assert_eq!(
649 blocks.pop().map(|it| it.0),
650 Some(node),
651 "left a BlockExpr we never entered"
652 );
653 }
654 }
655 }
656 }
657 });
658 std::mem::swap(&mut curr_layer, &mut next_layer);
659 assert!(blocks.is_empty(), "didn't leave all BlockExprs");
660 }
661
662 res.ptr_map = hashbrown::HashTable::with_capacity(res.arena.len());
663 res.id_map = hashbrown::HashTable::with_capacity(res.arena.len());
664 for (idx, (ptr, ast_id)) in res.arena.iter() {
665 let ptr_hash = hash_ptr(ptr);
666 let ast_id_hash = hash_ast_id(ast_id);
667 match res.ptr_map.entry(
668 ptr_hash,
669 |idx2| *idx2 == idx,
670 |&idx| hash_ptr(&res.arena[idx].0),
671 ) {
672 hashbrown::hash_table::Entry::Occupied(_) => unreachable!(),
673 hashbrown::hash_table::Entry::Vacant(entry) => {
674 entry.insert(idx);
675 }
676 }
677 match res.id_map.entry(
678 ast_id_hash,
679 |idx2| *idx2 == idx,
680 |&idx| hash_ast_id(&res.arena[idx].1),
681 ) {
682 hashbrown::hash_table::Entry::Occupied(_) => unreachable!(),
683 hashbrown::hash_table::Entry::Vacant(entry) => {
684 entry.insert(idx);
685 }
686 }
687 }
688 res.arena.shrink_to_fit();
689 return res;
690
691 fn parent_of(parent_idx: Option<ArenaId>, res: &AstIdMap) -> Option<&ErasedFileAstId> {
692 let mut parent = parent_idx.map(|parent_idx| &res.arena[parent_idx].1);
693 if parent.is_some_and(|parent| parent.kind() == ErasedFileAstIdKind::ExternBlock as u32)
694 {
695 parent = None;
707 }
708 parent
709 }
710 }
711
712 pub fn root(&self) -> SyntaxNodePtr {
714 self.arena[Idx::from_raw(RawIdx::from_u32(0))].0
715 }
716
717 pub fn ast_id<N: AstIdNode>(&self, item: &N) -> FileAstId<N> {
718 self.ast_id_for_ptr(AstPtr::new(item))
719 }
720
721 pub fn ast_id_for_block(&self, block: &ast::BlockExpr) -> Option<FileAstId<ast::BlockExpr>> {
723 self.ast_id_for_ptr_for_block(AstPtr::new(block))
724 }
725
726 pub fn ast_id_for_ptr<N: AstIdNode>(&self, ptr: AstPtr<N>) -> FileAstId<N> {
727 let ptr = ptr.syntax_node_ptr();
728 FileAstId { raw: self.erased_ast_id(ptr), _marker: PhantomData }
729 }
730
731 pub fn ast_id_for_ptr_for_block(
733 &self,
734 ptr: AstPtr<ast::BlockExpr>,
735 ) -> Option<FileAstId<ast::BlockExpr>> {
736 let ptr = ptr.syntax_node_ptr();
737 self.try_erased_ast_id(ptr).map(|raw| FileAstId { raw, _marker: PhantomData })
738 }
739
740 fn erased_ast_id(&self, ptr: SyntaxNodePtr) -> ErasedFileAstId {
741 self.try_erased_ast_id(ptr).unwrap_or_else(|| {
742 panic!(
743 "Can't find SyntaxNodePtr {:?} in AstIdMap:\n{:?}",
744 ptr,
745 self.arena.iter().map(|(_id, i)| i).collect::<Vec<_>>(),
746 )
747 })
748 }
749
750 fn try_erased_ast_id(&self, ptr: SyntaxNodePtr) -> Option<ErasedFileAstId> {
751 let hash = hash_ptr(&ptr);
752 let idx = *self.ptr_map.find(hash, |&idx| self.arena[idx].0 == ptr)?;
753 Some(self.arena[idx].1)
754 }
755
756 pub fn get<N: AstNode>(&self, id: FileAstId<N>) -> AstPtr<N> {
760 let ptr = self.get_erased(id.raw);
761 AstPtr::try_from_raw(ptr)
762 .unwrap_or_else(|| panic!("AstIdMap node mismatch with node `{ptr:?}`"))
763 }
764
765 pub fn get_erased(&self, id: ErasedFileAstId) -> SyntaxNodePtr {
766 let hash = hash_ast_id(&id);
767 match self.id_map.find(hash, |&idx| self.arena[idx].1 == id) {
768 Some(&idx) => self.arena[idx].0,
769 None => panic!(
770 "Can't find ast id {:?} in AstIdMap:\n{:?}",
771 id,
772 self.arena.iter().map(|(_id, i)| i).collect::<Vec<_>>(),
773 ),
774 }
775 }
776}
777
778#[cfg(not(no_salsa_async_drops))]
779impl Drop for AstIdMap {
780 fn drop(&mut self) {
781 let arena = std::mem::take(&mut self.arena);
782 let ptr_map = std::mem::take(&mut self.ptr_map);
783 let id_map = std::mem::take(&mut self.id_map);
784 static AST_ID_MAP_DROP_THREAD: std::sync::OnceLock<
785 std::sync::mpsc::Sender<(
786 Arena<(SyntaxNodePtr, ErasedFileAstId)>,
787 hashbrown::HashTable<ArenaId>,
788 hashbrown::HashTable<ArenaId>,
789 )>,
790 > = std::sync::OnceLock::new();
791 AST_ID_MAP_DROP_THREAD
792 .get_or_init(|| {
793 let (sender, receiver) = std::sync::mpsc::channel::<(
794 Arena<(SyntaxNodePtr, ErasedFileAstId)>,
795 hashbrown::HashTable<ArenaId>,
796 hashbrown::HashTable<ArenaId>,
797 )>();
798 std::thread::Builder::new()
799 .name("AstIdMapDropper".to_owned())
800 .spawn(move || {
801 loop {
802 _ = receiver.recv();
804 while receiver.try_recv().is_ok() {}
806 std::thread::sleep(std::time::Duration::from_millis(100));
808 }
809 })
812 .unwrap();
813 sender
814 })
815 .send((arena, ptr_map, id_map))
816 .unwrap();
817 }
818}
819
820#[inline]
821fn hash_ptr(ptr: &SyntaxNodePtr) -> u64 {
822 FxBuildHasher.hash_one(ptr)
823}
824
825#[inline]
826fn hash_ast_id(ptr: &ErasedFileAstId) -> u64 {
827 FxBuildHasher.hash_one(ptr)
828}
829
830#[cfg(test)]
831mod tests {
832 use syntax::{AstNode, Edition, SourceFile, SyntaxKind, SyntaxNodePtr, WalkEvent, ast};
833
834 use crate::AstIdMap;
835
836 #[test]
837 fn check_all_nodes() {
838 let syntax = SourceFile::parse(
839 r#"
840extern crate foo;
841fn foo() {
842 union U {}
843}
844struct S;
845macro_rules! m {}
846macro m2() {}
847trait Trait {}
848impl Trait for S {}
849impl S {}
850impl m!() {}
851impl m2!() for m!() {}
852type T = i32;
853enum E {
854 V1(),
855 V2 {},
856 V3,
857}
858struct S; // duplicate
859extern "C" {
860 static S: i32;
861}
862static mut S: i32 = 0;
863const FOO: i32 = 0;
864 "#,
865 Edition::CURRENT,
866 )
867 .syntax_node();
868 let ast_id_map = AstIdMap::from_source(&syntax);
869 for node in syntax.preorder() {
870 let WalkEvent::Enter(node) = node else { continue };
871 if !matches!(
872 node.kind(),
873 SyntaxKind::EXTERN_CRATE
874 | SyntaxKind::FN
875 | SyntaxKind::UNION
876 | SyntaxKind::STRUCT
877 | SyntaxKind::MACRO_RULES
878 | SyntaxKind::MACRO_DEF
879 | SyntaxKind::MACRO_CALL
880 | SyntaxKind::TRAIT
881 | SyntaxKind::IMPL
882 | SyntaxKind::TYPE_ALIAS
883 | SyntaxKind::ENUM
884 | SyntaxKind::VARIANT
885 | SyntaxKind::EXTERN_BLOCK
886 | SyntaxKind::STATIC
887 | SyntaxKind::CONST
888 ) {
889 continue;
890 }
891 let ptr = SyntaxNodePtr::new(&node);
892 let ast_id = ast_id_map.erased_ast_id(ptr);
893 let turn_back = ast_id_map.get_erased(ast_id);
894 assert_eq!(ptr, turn_back);
895 }
896 }
897
898 #[test]
899 fn different_names_get_different_hashes() {
900 let syntax = SourceFile::parse(
901 r#"
902fn foo() {}
903fn bar() {}
904 "#,
905 Edition::CURRENT,
906 )
907 .syntax_node();
908 let ast_id_map = AstIdMap::from_source(&syntax);
909 let fns = syntax.descendants().filter_map(ast::Fn::cast).collect::<Vec<_>>();
910 let [foo_fn, bar_fn] = fns.as_slice() else {
911 panic!("not exactly 2 functions");
912 };
913 let foo_fn_id = ast_id_map.ast_id(foo_fn);
914 let bar_fn_id = ast_id_map.ast_id(bar_fn);
915 assert_ne!(foo_fn_id.raw.hash_value(), bar_fn_id.raw.hash_value(), "hashes are equal");
916 }
917
918 #[test]
919 fn different_parents_get_different_hashes() {
920 let syntax = SourceFile::parse(
921 r#"
922fn foo() {
923 m!();
924}
925fn bar() {
926 m!();
927}
928 "#,
929 Edition::CURRENT,
930 )
931 .syntax_node();
932 let ast_id_map = AstIdMap::from_source(&syntax);
933 let macro_calls = syntax.descendants().filter_map(ast::MacroCall::cast).collect::<Vec<_>>();
934 let [macro_call_foo, macro_call_bar] = macro_calls.as_slice() else {
935 panic!("not exactly 2 macro calls");
936 };
937 let macro_call_foo_id = ast_id_map.ast_id(macro_call_foo);
938 let macro_call_bar_id = ast_id_map.ast_id(macro_call_bar);
939 assert_ne!(
940 macro_call_foo_id.raw.hash_value(),
941 macro_call_bar_id.raw.hash_value(),
942 "hashes are equal"
943 );
944 }
945
946 #[test]
947 fn blocks_with_no_items_have_no_id() {
948 let syntax = SourceFile::parse(
949 r#"
950fn foo() {
951 let foo = 1;
952 bar(foo);
953}
954 "#,
955 Edition::CURRENT,
956 )
957 .syntax_node();
958 let ast_id_map = AstIdMap::from_source(&syntax);
959 let block = syntax.descendants().find_map(ast::BlockExpr::cast).expect("no block");
960 assert!(ast_id_map.ast_id_for_block(&block).is_none());
961 }
962}