hir_expand/
span_map.rs

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