Skip to main content

hir_expand/
files.rs

1//! Things to wrap other things in file ids.
2use std::borrow::Borrow;
3
4use base_db::SourceDatabase;
5use either::Either;
6use span::{AstIdNode, ErasedFileAstId, FileAstId, FileId, SyntaxContext};
7use syntax::{AstNode, AstPtr, SyntaxNode, SyntaxNodePtr, SyntaxToken, TextRange, TextSize};
8
9use crate::{
10    EditionedFileId, HirFileId, MacroCallId, MacroKind, map_node_range_up,
11    map_node_range_up_rooted, span_for_offset,
12};
13
14/// `InFile<T>` stores a value of `T` inside a particular file/syntax tree.
15///
16/// Typical usages are:
17///
18/// * `InFile<SyntaxNode>` -- syntax node in a file
19/// * `InFile<ast::FnDef>` -- ast node in a file
20/// * `InFile<TextSize>` -- offset in a file
21#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
22pub struct InFileWrapper<FileKind, T> {
23    pub file_id: FileKind,
24    pub value: T,
25}
26pub type InFile<T> = InFileWrapper<HirFileId, T>;
27pub type InMacroFile<T> = InFileWrapper<MacroCallId, T>;
28pub type InRealFile<T> = InFileWrapper<EditionedFileId, T>;
29
30#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
31pub struct FilePositionWrapper<FileKind> {
32    pub file_id: FileKind,
33    pub offset: TextSize,
34}
35pub type HirFilePosition = FilePositionWrapper<HirFileId>;
36pub type MacroFilePosition = FilePositionWrapper<MacroCallId>;
37pub type FilePosition = FilePositionWrapper<EditionedFileId>;
38
39impl FilePosition {
40    #[inline]
41    pub fn into_file_id(self, db: &dyn SourceDatabase) -> FilePositionWrapper<FileId> {
42        FilePositionWrapper { file_id: self.file_id.file_id(db), offset: self.offset }
43    }
44}
45
46impl From<FileRange> for HirFileRange {
47    fn from(value: FileRange) -> Self {
48        HirFileRange { file_id: value.file_id.into(), range: value.range }
49    }
50}
51
52impl From<FilePosition> for HirFilePosition {
53    fn from(value: FilePosition) -> Self {
54        HirFilePosition { file_id: value.file_id.into(), offset: value.offset }
55    }
56}
57
58impl HirFileRange {
59    pub fn file_range(self) -> Option<FileRange> {
60        Some(FileRange { file_id: self.file_id.file_id()?, range: self.range })
61    }
62}
63
64#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
65pub struct FileRangeWrapper<FileKind> {
66    pub file_id: FileKind,
67    pub range: TextRange,
68}
69pub type HirFileRange = FileRangeWrapper<HirFileId>;
70pub type MacroFileRange = FileRangeWrapper<MacroCallId>;
71pub type FileRange = FileRangeWrapper<EditionedFileId>;
72
73impl FileRange {
74    #[inline]
75    pub fn into_file_id(self, db: &dyn SourceDatabase) -> FileRangeWrapper<FileId> {
76        FileRangeWrapper { file_id: self.file_id.file_id(db), range: self.range }
77    }
78
79    #[inline]
80    pub fn file_text(self, db: &dyn SourceDatabase) -> &triomphe::Arc<str> {
81        db.file_text(self.file_id.file_id(db)).text(db)
82    }
83
84    #[inline]
85    pub fn text(self, db: &dyn SourceDatabase) -> &str {
86        &self.file_text(db)[self.range]
87    }
88}
89
90/// `AstId` points to an AST node in any file.
91///
92/// It is stable across reparses, and can be used as salsa key/value.
93pub type AstId<N> = crate::InFile<FileAstId<N>>;
94
95impl<N: AstNode> AstId<N> {
96    pub fn to_node(&self, db: &dyn SourceDatabase) -> N {
97        self.to_ptr(db).to_node(&self.file_id.parse_or_expand(db))
98    }
99    pub fn to_range(&self, db: &dyn SourceDatabase) -> TextRange {
100        self.to_ptr(db).text_range()
101    }
102    pub fn to_in_file_node(&self, db: &dyn SourceDatabase) -> crate::InFile<N> {
103        crate::InFile::new(self.file_id, self.to_ptr(db).to_node(&self.file_id.parse_or_expand(db)))
104    }
105    pub fn to_ptr(&self, db: &dyn SourceDatabase) -> AstPtr<N> {
106        self.file_id.ast_id_map(db).get(self.value)
107    }
108    pub fn erase(&self) -> ErasedAstId {
109        crate::InFile::new(self.file_id, self.value.erase())
110    }
111    #[inline]
112    pub fn upcast<M: AstIdNode>(self) -> AstId<M>
113    where
114        N: Into<M>,
115    {
116        self.map(|it| it.upcast())
117    }
118}
119
120pub type ErasedAstId = crate::InFile<ErasedFileAstId>;
121
122impl ErasedAstId {
123    pub fn to_range(&self, db: &dyn SourceDatabase) -> TextRange {
124        self.to_ptr(db).text_range()
125    }
126    pub fn to_ptr(&self, db: &dyn SourceDatabase) -> SyntaxNodePtr {
127        self.file_id.ast_id_map(db).get_erased(self.value)
128    }
129}
130
131impl<FileKind, N: AstNode> InFileWrapper<FileKind, AstPtr<N>> {
132    #[inline]
133    pub fn upcast<M: AstNode>(self) -> InFileWrapper<FileKind, AstPtr<M>>
134    where
135        N: Into<M>,
136    {
137        self.map(|it| it.upcast())
138    }
139}
140
141impl<FileKind, T> InFileWrapper<FileKind, T> {
142    pub fn new(file_id: FileKind, value: T) -> Self {
143        Self { file_id, value }
144    }
145
146    pub fn map<F: FnOnce(T) -> U, U>(self, f: F) -> InFileWrapper<FileKind, U> {
147        InFileWrapper::new(self.file_id, f(self.value))
148    }
149}
150
151impl<FileKind: Copy, T> InFileWrapper<FileKind, T> {
152    pub fn with_value<U>(&self, value: U) -> InFileWrapper<FileKind, U> {
153        InFileWrapper::new(self.file_id, value)
154    }
155
156    pub fn as_ref(&self) -> InFileWrapper<FileKind, &T> {
157        self.with_value(&self.value)
158    }
159
160    pub fn borrow<U>(&self) -> InFileWrapper<FileKind, &U>
161    where
162        T: Borrow<U>,
163    {
164        self.with_value(self.value.borrow())
165    }
166}
167
168impl<FileKind: Copy, T: Clone> InFileWrapper<FileKind, &T> {
169    pub fn cloned(&self) -> InFileWrapper<FileKind, T> {
170        self.with_value(self.value.clone())
171    }
172}
173
174impl<T> From<InMacroFile<T>> for InFile<T> {
175    fn from(InMacroFile { file_id, value }: InMacroFile<T>) -> Self {
176        InFile { file_id: file_id.into(), value }
177    }
178}
179
180impl<T> From<InRealFile<T>> for InFile<T> {
181    fn from(InRealFile { file_id, value }: InRealFile<T>) -> Self {
182        InFile { file_id: file_id.into(), value }
183    }
184}
185
186// region:transpose impls
187
188impl<FileKind, T> InFileWrapper<FileKind, Option<T>> {
189    pub fn transpose(self) -> Option<InFileWrapper<FileKind, T>> {
190        Some(InFileWrapper::new(self.file_id, self.value?))
191    }
192}
193
194impl<FileKind, L, R> InFileWrapper<FileKind, Either<L, R>> {
195    pub fn transpose(self) -> Either<InFileWrapper<FileKind, L>, InFileWrapper<FileKind, R>> {
196        match self.value {
197            Either::Left(l) => Either::Left(InFileWrapper::new(self.file_id, l)),
198            Either::Right(r) => Either::Right(InFileWrapper::new(self.file_id, r)),
199        }
200    }
201}
202
203// endregion:transpose impls
204
205trait FileIdToSyntax: Copy {
206    fn file_syntax(self, db: &dyn SourceDatabase) -> SyntaxNode;
207}
208
209impl FileIdToSyntax for EditionedFileId {
210    fn file_syntax(self, db: &dyn SourceDatabase) -> SyntaxNode {
211        self.parse(db).syntax_node()
212    }
213}
214impl FileIdToSyntax for MacroCallId {
215    fn file_syntax(self, db: &dyn SourceDatabase) -> SyntaxNode {
216        self.parse_macro_expansion(db).value.0.syntax_node()
217    }
218}
219impl FileIdToSyntax for HirFileId {
220    fn file_syntax(self, db: &dyn SourceDatabase) -> SyntaxNode {
221        self.parse_or_expand(db)
222    }
223}
224
225#[allow(private_bounds)]
226impl<FileId: FileIdToSyntax, T> InFileWrapper<FileId, T> {
227    pub fn file_syntax(&self, db: &dyn SourceDatabase) -> SyntaxNode {
228        FileIdToSyntax::file_syntax(self.file_id, db)
229    }
230}
231
232#[allow(private_bounds)]
233impl<FileId: FileIdToSyntax, N: AstNode> InFileWrapper<FileId, AstPtr<N>> {
234    pub fn to_node(&self, db: &dyn SourceDatabase) -> N {
235        self.value.to_node(&self.file_syntax(db))
236    }
237}
238
239impl<FileId: Copy, N: AstNode> InFileWrapper<FileId, N> {
240    pub fn syntax(&self) -> InFileWrapper<FileId, &SyntaxNode> {
241        self.with_value(self.value.syntax())
242    }
243    pub fn node_file_range(&self) -> FileRangeWrapper<FileId> {
244        FileRangeWrapper { file_id: self.file_id, range: self.value.syntax().text_range() }
245    }
246}
247
248impl<FileId: Copy, N: AstNode> InFileWrapper<FileId, &N> {
249    // unfortunately `syntax` collides with the impl above, because `&_` is fundamental
250    pub fn syntax_ref(&self) -> InFileWrapper<FileId, &SyntaxNode> {
251        self.with_value(self.value.syntax())
252    }
253}
254
255// region:specific impls
256impl<FileId: Copy, SN: Borrow<SyntaxNode>> InFileWrapper<FileId, SN> {
257    pub fn file_range(&self) -> FileRangeWrapper<FileId> {
258        FileRangeWrapper { file_id: self.file_id, range: self.value.borrow().text_range() }
259    }
260}
261
262impl<SN: Borrow<SyntaxNode>> InFile<SN> {
263    pub fn parent_ancestors_with_macros(
264        self,
265        db: &dyn SourceDatabase,
266    ) -> impl Iterator<Item = InFile<SyntaxNode>> + '_ {
267        let succ = move |node: &InFile<SyntaxNode>| match node.value.parent() {
268            Some(parent) => Some(node.with_value(parent)),
269            None => node
270                .file_id
271                .macro_file()?
272                .loc(db)
273                .to_node_item(db)
274                .syntax()
275                .cloned()
276                .map(|node| node.parent())
277                .transpose(),
278        };
279        std::iter::successors(succ(&self.borrow().cloned()), succ)
280    }
281
282    pub fn ancestors_with_macros(
283        self,
284        db: &dyn SourceDatabase,
285    ) -> impl Iterator<Item = InFile<SyntaxNode>> + '_ {
286        let succ = move |node: &InFile<SyntaxNode>| match node.value.parent() {
287            Some(parent) => Some(node.with_value(parent)),
288            None => node
289                .file_id
290                .macro_file()?
291                .loc(db)
292                .to_node_item(db)
293                .syntax()
294                .cloned()
295                .map(|node| node.parent())
296                .transpose(),
297        };
298        std::iter::successors(Some(self.borrow().cloned()), succ)
299    }
300
301    pub fn kind(&self) -> parser::SyntaxKind {
302        self.value.borrow().kind()
303    }
304
305    pub fn text_range(&self) -> TextRange {
306        self.value.borrow().text_range()
307    }
308
309    /// Falls back to the macro call range if the node cannot be mapped up fully.
310    ///
311    /// For attributes and derives, this will point back to the attribute only.
312    /// For the entire item use `InFile::original_file_range_full`.
313    pub fn original_file_range_rooted(self, db: &dyn SourceDatabase) -> FileRange {
314        self.borrow().map(SyntaxNode::text_range).original_node_file_range_rooted(db)
315    }
316
317    /// Falls back to the macro call range if the node cannot be mapped up fully.
318    pub fn original_file_range_with_macro_call_input(self, db: &dyn SourceDatabase) -> FileRange {
319        self.borrow().map(SyntaxNode::text_range).original_node_file_range_with_macro_call_input(db)
320    }
321
322    pub fn original_syntax_node_rooted(
323        self,
324        db: &dyn SourceDatabase,
325    ) -> Option<InRealFile<SyntaxNode>> {
326        // This kind of upmapping can only be achieved in attribute expanded files,
327        // as we don't have node inputs otherwise and therefore can't find an `N` node in the input
328        let file_id = match self.file_id {
329            HirFileId::FileId(file_id) => {
330                return Some(InRealFile { file_id, value: self.value.borrow().clone() });
331            }
332            HirFileId::MacroFile(m)
333                if matches!(m.kind(db), MacroKind::Attr | MacroKind::AttrBuiltIn) =>
334            {
335                m
336            }
337            _ => return None,
338        };
339
340        let FileRange { file_id: editioned_file_id, range } = map_node_range_up_rooted(
341            db,
342            file_id.expansion_span_map(db),
343            self.value.borrow().text_range(),
344        )?;
345
346        let kind = self.kind();
347        let value = editioned_file_id
348            .parse(db)
349            .syntax_node()
350            .covering_element(range)
351            .ancestors()
352            .take_while(|it| it.text_range() == range)
353            .find(|it| it.kind() == kind)?;
354        Some(InRealFile::new(editioned_file_id, value))
355    }
356}
357
358impl InFile<&SyntaxNode> {
359    /// Attempts to map the syntax node back up its macro calls.
360    pub fn original_file_range_opt(
361        self,
362        db: &dyn SourceDatabase,
363    ) -> Option<(FileRange, SyntaxContext)> {
364        self.borrow().map(SyntaxNode::text_range).original_node_file_range_opt(db)
365    }
366}
367
368impl InMacroFile<SyntaxToken> {
369    pub fn upmap_once(self, db: &dyn SourceDatabase) -> InFile<smallvec::SmallVec<[TextRange; 1]>> {
370        self.file_id.expansion_info(db).map_range_up_once(db, self.value.text_range())
371    }
372}
373
374impl InFile<SyntaxToken> {
375    /// Falls back to the macro call range if the node cannot be mapped up fully.
376    pub fn original_file_range(self, db: &dyn SourceDatabase) -> FileRange {
377        match self.file_id {
378            HirFileId::FileId(file_id) => FileRange { file_id, range: self.value.text_range() },
379            HirFileId::MacroFile(mac_file) => {
380                let (range, ctxt) = span_for_offset(
381                    db,
382                    mac_file.expansion_span_map(db),
383                    self.value.text_range().start(),
384                );
385
386                // FIXME: Figure out an API that makes proper use of ctx, this only exists to
387                // keep pre-token map rewrite behaviour.
388                if ctxt.is_root() {
389                    return range;
390                }
391
392                // Fall back to whole macro call.
393                let loc = mac_file.loc(db);
394                loc.kind.original_call_range(db, loc.krate)
395            }
396        }
397    }
398
399    /// Attempts to map the syntax node back up its macro calls.
400    pub fn original_file_range_opt(self, db: &dyn SourceDatabase) -> Option<FileRange> {
401        match self.file_id {
402            HirFileId::FileId(file_id) => {
403                Some(FileRange { file_id, range: self.value.text_range() })
404            }
405            HirFileId::MacroFile(mac_file) => {
406                let (range, ctxt) = span_for_offset(
407                    db,
408                    mac_file.expansion_span_map(db),
409                    self.value.text_range().start(),
410                );
411
412                // FIXME: Figure out an API that makes proper use of ctx, this only exists to
413                // keep pre-token map rewrite behaviour.
414                if ctxt.is_root() { Some(range) } else { None }
415            }
416        }
417    }
418}
419
420impl InMacroFile<TextSize> {
421    pub fn original_file_range(self, db: &dyn SourceDatabase) -> (FileRange, SyntaxContext) {
422        span_for_offset(db, self.file_id.expansion_span_map(db), self.value)
423    }
424}
425
426impl InFile<TextRange> {
427    pub fn original_node_file_range(self, db: &dyn SourceDatabase) -> (FileRange, SyntaxContext) {
428        match self.file_id {
429            HirFileId::FileId(file_id) => {
430                (FileRange { file_id, range: self.value }, SyntaxContext::root(file_id.edition(db)))
431            }
432            HirFileId::MacroFile(mac_file) => {
433                match map_node_range_up(db, mac_file.expansion_span_map(db), self.value) {
434                    Some(it) => it,
435                    None => {
436                        let loc = mac_file.loc(db);
437                        (
438                            loc.kind.original_call_range(db, loc.krate),
439                            SyntaxContext::root(loc.def.edition),
440                        )
441                    }
442                }
443            }
444        }
445    }
446
447    pub fn original_node_file_range_rooted(self, db: &dyn SourceDatabase) -> FileRange {
448        match self.file_id {
449            HirFileId::FileId(file_id) => FileRange { file_id, range: self.value },
450            HirFileId::MacroFile(mac_file) => {
451                match map_node_range_up_rooted(db, mac_file.expansion_span_map(db), self.value) {
452                    Some(it) => it,
453                    _ => {
454                        let loc = mac_file.loc(db);
455                        loc.kind.original_call_range(db, loc.krate)
456                    }
457                }
458            }
459        }
460    }
461
462    pub fn original_node_file_range_with_macro_call_input(
463        self,
464        db: &dyn SourceDatabase,
465    ) -> FileRange {
466        match self.file_id {
467            HirFileId::FileId(file_id) => FileRange { file_id, range: self.value },
468            HirFileId::MacroFile(mac_file) => {
469                match map_node_range_up_rooted(db, mac_file.expansion_span_map(db), self.value) {
470                    Some(it) => it,
471                    _ => {
472                        let loc = mac_file.loc(db);
473                        loc.kind.original_call_range_with_input(db)
474                    }
475                }
476            }
477        }
478    }
479
480    pub fn original_node_file_range_opt(
481        self,
482        db: &dyn SourceDatabase,
483    ) -> Option<(FileRange, SyntaxContext)> {
484        match self.file_id {
485            HirFileId::FileId(file_id) => Some((
486                FileRange { file_id, range: self.value },
487                SyntaxContext::root(file_id.edition(db)),
488            )),
489            HirFileId::MacroFile(mac_file) => {
490                map_node_range_up(db, mac_file.expansion_span_map(db), self.value)
491            }
492        }
493    }
494
495    pub fn original_node_file_range_rooted_opt(self, db: &dyn SourceDatabase) -> Option<FileRange> {
496        match self.file_id {
497            HirFileId::FileId(file_id) => Some(FileRange { file_id, range: self.value }),
498            HirFileId::MacroFile(mac_file) => {
499                map_node_range_up_rooted(db, mac_file.expansion_span_map(db), self.value)
500            }
501        }
502    }
503}
504
505impl<N: AstNode> InFile<N> {
506    pub fn original_ast_node_rooted(self, db: &dyn SourceDatabase) -> Option<InRealFile<N>> {
507        // This kind of upmapping can only be achieved in attribute expanded files,
508        // as we don't have node inputs otherwise and therefore can't find an `N` node in the input
509        let file_id = match self.file_id {
510            HirFileId::FileId(file_id) => {
511                return Some(InRealFile { file_id, value: self.value });
512            }
513            HirFileId::MacroFile(m) => m,
514        };
515        if !matches!(file_id.kind(db), MacroKind::Attr | MacroKind::AttrBuiltIn) {
516            return None;
517        }
518
519        let FileRange { file_id: editioned_file_id, range } = map_node_range_up_rooted(
520            db,
521            file_id.expansion_span_map(db),
522            self.value.syntax().text_range(),
523        )?;
524
525        // FIXME: This heuristic is brittle and with the right macro may select completely unrelated nodes?
526        let anc = editioned_file_id.parse(db).syntax_node().covering_element(range);
527        let value = anc.ancestors().find_map(N::cast)?;
528        Some(InRealFile::new(editioned_file_id, value))
529    }
530}
531
532impl<T> InFile<T> {
533    pub fn into_real_file(self) -> Result<InRealFile<T>, InFile<T>> {
534        match self.file_id {
535            HirFileId::FileId(file_id) => Ok(InRealFile { file_id, value: self.value }),
536            HirFileId::MacroFile(_) => Err(self),
537        }
538    }
539}