]> git.lizzy.rs Git - rust.git/blob - crates/hir-expand/src/hygiene.rs
Auto merge of #12652 - lnicola:openvsx, r=lnicola
[rust.git] / crates / hir-expand / src / hygiene.rs
1 //! This modules handles hygiene information.
2 //!
3 //! Specifically, `ast` + `Hygiene` allows you to create a `Name`. Note that, at
4 //! this moment, this is horribly incomplete and handles only `$crate`.
5 use std::sync::Arc;
6
7 use base_db::CrateId;
8 use db::TokenExpander;
9 use either::Either;
10 use mbe::Origin;
11 use syntax::{
12     ast::{self, HasDocComments},
13     AstNode, SyntaxKind, SyntaxNode, TextRange, TextSize,
14 };
15
16 use crate::{
17     db::{self, AstDatabase},
18     fixup,
19     name::{AsName, Name},
20     HirFileId, HirFileIdRepr, InFile, MacroCallKind, MacroCallLoc, MacroDefKind, MacroFile,
21 };
22
23 #[derive(Clone, Debug)]
24 pub struct Hygiene {
25     frames: Option<HygieneFrames>,
26 }
27
28 impl Hygiene {
29     pub fn new(db: &dyn AstDatabase, file_id: HirFileId) -> Hygiene {
30         Hygiene { frames: Some(HygieneFrames::new(db, file_id)) }
31     }
32
33     pub fn new_unhygienic() -> Hygiene {
34         Hygiene { frames: None }
35     }
36
37     // FIXME: this should just return name
38     pub fn name_ref_to_name(
39         &self,
40         db: &dyn AstDatabase,
41         name_ref: ast::NameRef,
42     ) -> Either<Name, CrateId> {
43         if let Some(frames) = &self.frames {
44             if name_ref.text() == "$crate" {
45                 if let Some(krate) = frames.root_crate(db, name_ref.syntax()) {
46                     return Either::Right(krate);
47                 }
48             }
49         }
50
51         Either::Left(name_ref.as_name())
52     }
53
54     pub fn local_inner_macros(&self, db: &dyn AstDatabase, path: ast::Path) -> Option<CrateId> {
55         let mut token = path.syntax().first_token()?.text_range();
56         let frames = self.frames.as_ref()?;
57         let mut current = &frames.0;
58
59         loop {
60             let (mapped, origin) = current.expansion.as_ref()?.map_ident_up(db, token)?;
61             if origin == Origin::Def {
62                 return if current.local_inner {
63                     frames.root_crate(db, path.syntax())
64                 } else {
65                     None
66                 };
67             }
68             current = current.call_site.as_ref()?;
69             token = mapped.value;
70         }
71     }
72 }
73
74 #[derive(Clone, Debug)]
75 struct HygieneFrames(Arc<HygieneFrame>);
76
77 #[derive(Clone, Debug, Eq, PartialEq)]
78 pub struct HygieneFrame {
79     expansion: Option<HygieneInfo>,
80
81     // Indicate this is a local inner macro
82     local_inner: bool,
83     krate: Option<CrateId>,
84
85     call_site: Option<Arc<HygieneFrame>>,
86     def_site: Option<Arc<HygieneFrame>>,
87 }
88
89 impl HygieneFrames {
90     fn new(db: &dyn AstDatabase, file_id: HirFileId) -> Self {
91         // Note that this intentionally avoids the `hygiene_frame` query to avoid blowing up memory
92         // usage. The query is only helpful for nested `HygieneFrame`s as it avoids redundant work.
93         HygieneFrames(Arc::new(HygieneFrame::new(db, file_id)))
94     }
95
96     fn root_crate(&self, db: &dyn AstDatabase, node: &SyntaxNode) -> Option<CrateId> {
97         let mut token = node.first_token()?.text_range();
98         let mut result = self.0.krate;
99         let mut current = self.0.clone();
100
101         while let Some((mapped, origin)) =
102             current.expansion.as_ref().and_then(|it| it.map_ident_up(db, token))
103         {
104             result = current.krate;
105
106             let site = match origin {
107                 Origin::Def => &current.def_site,
108                 Origin::Call => &current.call_site,
109             };
110
111             let site = match site {
112                 None => break,
113                 Some(it) => it,
114             };
115
116             current = site.clone();
117             token = mapped.value;
118         }
119
120         result
121     }
122 }
123
124 #[derive(Debug, Clone, PartialEq, Eq)]
125 struct HygieneInfo {
126     file: MacroFile,
127     /// The start offset of the `macro_rules!` arguments or attribute input.
128     attr_input_or_mac_def_start: Option<InFile<TextSize>>,
129
130     macro_def: Arc<TokenExpander>,
131     macro_arg: Arc<(tt::Subtree, mbe::TokenMap, fixup::SyntaxFixupUndoInfo)>,
132     macro_arg_shift: mbe::Shift,
133     exp_map: Arc<mbe::TokenMap>,
134 }
135
136 impl HygieneInfo {
137     fn map_ident_up(
138         &self,
139         db: &dyn AstDatabase,
140         token: TextRange,
141     ) -> Option<(InFile<TextRange>, Origin)> {
142         let token_id = self.exp_map.token_by_range(token)?;
143         let (mut token_id, origin) = self.macro_def.map_id_up(token_id);
144
145         let loc = db.lookup_intern_macro_call(self.file.macro_call_id);
146
147         let (token_map, tt) = match &loc.kind {
148             MacroCallKind::Attr { attr_args, .. } => match self.macro_arg_shift.unshift(token_id) {
149                 Some(unshifted) => {
150                     token_id = unshifted;
151                     (&attr_args.1, self.attr_input_or_mac_def_start?)
152                 }
153                 None => (
154                     &self.macro_arg.1,
155                     InFile::new(loc.kind.file_id(), loc.kind.arg(db)?.text_range().start()),
156                 ),
157             },
158             _ => match origin {
159                 mbe::Origin::Call => (
160                     &self.macro_arg.1,
161                     InFile::new(loc.kind.file_id(), loc.kind.arg(db)?.text_range().start()),
162                 ),
163                 mbe::Origin::Def => match (&*self.macro_def, &self.attr_input_or_mac_def_start) {
164                     (TokenExpander::DeclarativeMacro { def_site_token_map, .. }, Some(tt)) => {
165                         (def_site_token_map, *tt)
166                     }
167                     _ => panic!("`Origin::Def` used with non-`macro_rules!` macro"),
168                 },
169             },
170         };
171
172         let range = token_map.first_range_by_token(token_id, SyntaxKind::IDENT)?;
173         Some((tt.with_value(range + tt.value), origin))
174     }
175 }
176
177 fn make_hygiene_info(
178     db: &dyn AstDatabase,
179     macro_file: MacroFile,
180     loc: &MacroCallLoc,
181 ) -> Option<HygieneInfo> {
182     let def = loc.def.ast_id().left().and_then(|id| {
183         let def_tt = match id.to_node(db) {
184             ast::Macro::MacroRules(mac) => mac.token_tree()?,
185             ast::Macro::MacroDef(mac) => mac.body()?,
186         };
187         Some(InFile::new(id.file_id, def_tt))
188     });
189     let attr_input_or_mac_def = def.or_else(|| match loc.kind {
190         MacroCallKind::Attr { ast_id, invoc_attr_index, .. } => {
191             let tt = ast_id
192                 .to_node(db)
193                 .doc_comments_and_attrs()
194                 .nth(invoc_attr_index as usize)
195                 .and_then(Either::left)?
196                 .token_tree()?;
197             Some(InFile::new(ast_id.file_id, tt))
198         }
199         _ => None,
200     });
201
202     let macro_def = db.macro_def(loc.def).ok()?;
203     let (_, exp_map) = db.parse_macro_expansion(macro_file).value?;
204     let macro_arg = db.macro_arg(macro_file.macro_call_id)?;
205
206     Some(HygieneInfo {
207         file: macro_file,
208         attr_input_or_mac_def_start: attr_input_or_mac_def
209             .map(|it| it.map(|tt| tt.syntax().text_range().start())),
210         macro_arg_shift: mbe::Shift::new(&macro_arg.0),
211         macro_arg,
212         macro_def,
213         exp_map,
214     })
215 }
216
217 impl HygieneFrame {
218     pub(crate) fn new(db: &dyn AstDatabase, file_id: HirFileId) -> HygieneFrame {
219         let (info, krate, local_inner) = match file_id.0 {
220             HirFileIdRepr::FileId(_) => (None, None, false),
221             HirFileIdRepr::MacroFile(macro_file) => {
222                 let loc = db.lookup_intern_macro_call(macro_file.macro_call_id);
223                 let info =
224                     make_hygiene_info(db, macro_file, &loc).map(|info| (loc.kind.file_id(), info));
225                 match loc.def.kind {
226                     MacroDefKind::Declarative(_) => {
227                         (info, Some(loc.def.krate), loc.def.local_inner)
228                     }
229                     MacroDefKind::BuiltIn(..) => (info, Some(loc.def.krate), false),
230                     MacroDefKind::BuiltInAttr(..) => (info, None, false),
231                     MacroDefKind::BuiltInDerive(..) => (info, None, false),
232                     MacroDefKind::BuiltInEager(..) => (info, None, false),
233                     MacroDefKind::ProcMacro(..) => (info, None, false),
234                 }
235             }
236         };
237
238         let (calling_file, info) = match info {
239             None => {
240                 return HygieneFrame {
241                     expansion: None,
242                     local_inner,
243                     krate,
244                     call_site: None,
245                     def_site: None,
246                 };
247             }
248             Some(it) => it,
249         };
250
251         let def_site = info.attr_input_or_mac_def_start.map(|it| db.hygiene_frame(it.file_id));
252         let call_site = Some(db.hygiene_frame(calling_file));
253
254         HygieneFrame { expansion: Some(info), local_inner, krate, call_site, def_site }
255     }
256 }