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