Skip to main content

hir_expand/
span_map.rs

1//! Span maps for real files and macro expansions.
2
3use base_db::SourceDatabase;
4use span::Span;
5use syntax::{AstNode, TextRange, ast};
6
7pub use span::RealSpanMap;
8
9use crate::{HirFileId, MacroCallId};
10
11pub type ExpansionSpanMap = span::SpanMap;
12
13/// Spanmap for a macro file or a real file
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum SpanMap<'db> {
16    /// Spanmap for a macro file
17    ExpansionSpanMap(&'db ExpansionSpanMap),
18    /// Spanmap for a real file
19    RealSpanMap(&'db RealSpanMap),
20}
21
22impl syntax_bridge::SpanMapper for SpanMap<'_> {
23    fn span_for(&self, range: TextRange) -> Span {
24        self.span_for_range(range)
25    }
26}
27
28impl<'db> SpanMap<'db> {
29    pub fn span_for_range(&self, range: TextRange) -> Span {
30        match self {
31            // FIXME: Is it correct for us to only take the span at the start? This feels somewhat
32            // wrong. The context will be right, but the range could be considered wrong. See
33            // https://github.com/rust-lang/rust/issues/23480, we probably want to fetch the span at
34            // the start and end, then merge them like rustc does in `Span::to`
35            Self::ExpansionSpanMap(span_map) => span_map.span_at(range.start()),
36            Self::RealSpanMap(span_map) => span_map.span_for_range(range),
37        }
38    }
39}
40
41impl HirFileId {
42    #[inline]
43    pub fn span_map<'db>(self, db: &'db dyn SourceDatabase) -> SpanMap<'db> {
44        match self {
45            HirFileId::FileId(file_id) => SpanMap::RealSpanMap(real_span_map(db, file_id)),
46            HirFileId::MacroFile(m) => SpanMap::ExpansionSpanMap(m.expansion_span_map(db)),
47        }
48    }
49}
50
51/// This is an implementation detail of [`HirFileId::span_map`]. Outside this crate, use
52/// `HirFileId::from(file_id).span_map(db)` instead of `real_span_map(db, file_id)`.
53#[salsa::tracked(returns(ref))]
54pub(crate) fn real_span_map(
55    db: &dyn SourceDatabase,
56    editioned_file_id: base_db::EditionedFileId,
57) -> RealSpanMap {
58    use syntax::ast::HasModuleItem;
59    let mut pairs = vec![(syntax::TextSize::new(0), span::ROOT_ERASED_FILE_AST_ID)];
60    let ast_id_map = HirFileId::from(editioned_file_id).ast_id_map(db);
61
62    let tree = editioned_file_id.parse(db).tree();
63    // This is an incrementality layer. Basically we can't use absolute ranges for our spans as that
64    // would mean we'd invalidate everything whenever we type. So instead we make the text ranges
65    // relative to some AstIds reducing the risk of invalidation as typing somewhere no longer
66    // affects all following spans in the file.
67    // There is some stuff to bear in mind here though, for one, the more "anchors" we create, the
68    // easier it gets to invalidate things again as spans are as stable as their anchor's ID.
69    // The other problem is proc-macros. Proc-macros have a `Span::join` api that allows them
70    // to join two spans that come from the same file. rust-analyzer's proc-macro server
71    // can only join two spans if they belong to the same anchor though, as the spans are relative
72    // to that anchor. To do cross anchor joining we'd need to access to the ast id map to resolve
73    // them again, something we might get access to in the future. But even then, proc-macros doing
74    // this kind of joining makes them as stable as the AstIdMap (which is basically changing on
75    // every input of the file)…
76
77    let item_to_entry =
78        |item: ast::Item| (item.syntax().text_range().start(), ast_id_map.ast_id(&item).erase());
79    // Top level items make for great anchors as they are the most stable and a decent boundary
80    pairs.extend(tree.items().map(item_to_entry));
81    // Unfortunately, assoc items are very common in Rust, so descend into those as well and make
82    // them anchors too, but only if they have no attributes attached, as those might be proc-macros
83    // and using different anchors inside of them will prevent spans from being joinable.
84    tree.items().for_each(|item| match &item {
85        ast::Item::ExternBlock(it) if ast::attrs_including_inner(it).next().is_none() => {
86            if let Some(extern_item_list) = it.extern_item_list() {
87                pairs.extend(
88                    extern_item_list.extern_items().map(ast::Item::from).map(item_to_entry),
89                );
90            }
91        }
92        ast::Item::Impl(it) if ast::attrs_including_inner(it).next().is_none() => {
93            if let Some(assoc_item_list) = it.assoc_item_list() {
94                pairs.extend(assoc_item_list.assoc_items().map(ast::Item::from).map(item_to_entry));
95            }
96        }
97        ast::Item::Module(it) if ast::attrs_including_inner(it).next().is_none() => {
98            if let Some(item_list) = it.item_list() {
99                pairs.extend(item_list.items().map(item_to_entry));
100            }
101        }
102        ast::Item::Trait(it) if ast::attrs_including_inner(it).next().is_none() => {
103            if let Some(assoc_item_list) = it.assoc_item_list() {
104                pairs.extend(assoc_item_list.assoc_items().map(ast::Item::from).map(item_to_entry));
105            }
106        }
107        _ => (),
108    });
109
110    RealSpanMap::from_file(
111        editioned_file_id.span_file_id(db),
112        pairs.into_boxed_slice(),
113        tree.syntax().text_range().end(),
114    )
115}
116
117impl MacroCallId {
118    pub fn expansion_span_map(self, db: &dyn SourceDatabase) -> &ExpansionSpanMap {
119        &self.parse_macro_expansion(db).value.1
120    }
121}