]> git.lizzy.rs Git - rust.git/blob - crates/hir_expand/src/builtin_fn_macro.rs
Merge #10417
[rust.git] / crates / hir_expand / src / builtin_fn_macro.rs
1 //! Builtin macro
2 use crate::{
3     db::AstDatabase, name, quote, AstId, CrateId, MacroCallId, MacroCallLoc, MacroDefId,
4     MacroDefKind,
5 };
6
7 use base_db::{AnchoredPath, Edition, FileId};
8 use cfg::CfgExpr;
9 use either::Either;
10 use mbe::{parse_exprs_with_sep, parse_to_token_tree, ExpandResult};
11 use syntax::ast::{self, AstToken};
12
13 macro_rules! register_builtin {
14     ( LAZY: $(($name:ident, $kind: ident) => $expand:ident),* , EAGER: $(($e_name:ident, $e_kind: ident) => $e_expand:ident),*  ) => {
15         #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16         pub enum BuiltinFnLikeExpander {
17             $($kind),*
18         }
19
20         #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21         pub enum EagerExpander {
22             $($e_kind),*
23         }
24
25         impl BuiltinFnLikeExpander {
26             pub fn expand(
27                 &self,
28                 db: &dyn AstDatabase,
29                 id: MacroCallId,
30                 tt: &tt::Subtree,
31             ) -> ExpandResult<tt::Subtree> {
32                 let expander = match *self {
33                     $( BuiltinFnLikeExpander::$kind => $expand, )*
34                 };
35                 expander(db, id, tt)
36             }
37         }
38
39         impl EagerExpander {
40             pub fn expand(
41                 &self,
42                 db: &dyn AstDatabase,
43                 arg_id: MacroCallId,
44                 tt: &tt::Subtree,
45             ) -> ExpandResult<Option<ExpandedEager>> {
46                 let expander = match *self {
47                     $( EagerExpander::$e_kind => $e_expand, )*
48                 };
49                 expander(db, arg_id, tt)
50             }
51         }
52
53         fn find_by_name(ident: &name::Name) -> Option<Either<BuiltinFnLikeExpander, EagerExpander>> {
54             match ident {
55                 $( id if id == &name::name![$name] => Some(Either::Left(BuiltinFnLikeExpander::$kind)), )*
56                 $( id if id == &name::name![$e_name] => Some(Either::Right(EagerExpander::$e_kind)), )*
57                 _ => return None,
58             }
59         }
60     };
61 }
62
63 #[derive(Debug)]
64 pub struct ExpandedEager {
65     pub(crate) subtree: tt::Subtree,
66     /// The included file ID of the include macro.
67     pub(crate) included_file: Option<FileId>,
68 }
69
70 impl ExpandedEager {
71     fn new(subtree: tt::Subtree) -> Self {
72         ExpandedEager { subtree, included_file: None }
73     }
74 }
75
76 pub fn find_builtin_macro(
77     ident: &name::Name,
78     krate: CrateId,
79     ast_id: AstId<ast::Macro>,
80 ) -> Option<MacroDefId> {
81     let kind = find_by_name(ident)?;
82
83     match kind {
84         Either::Left(kind) => Some(MacroDefId {
85             krate,
86             kind: MacroDefKind::BuiltIn(kind, ast_id),
87             local_inner: false,
88         }),
89         Either::Right(kind) => Some(MacroDefId {
90             krate,
91             kind: MacroDefKind::BuiltInEager(kind, ast_id),
92             local_inner: false,
93         }),
94     }
95 }
96
97 register_builtin! {
98     LAZY:
99     (column, Column) => column_expand,
100     (file, File) => file_expand,
101     (line, Line) => line_expand,
102     (module_path, ModulePath) => module_path_expand,
103     (assert, Assert) => assert_expand,
104     (stringify, Stringify) => stringify_expand,
105     (format_args, FormatArgs) => format_args_expand,
106     (const_format_args, ConstFormatArgs) => format_args_expand,
107     // format_args_nl only differs in that it adds a newline in the end,
108     // so we use the same stub expansion for now
109     (format_args_nl, FormatArgsNl) => format_args_expand,
110     (llvm_asm, LlvmAsm) => asm_expand,
111     (asm, Asm) => asm_expand,
112     (global_asm, GlobalAsm) => global_asm_expand,
113     (cfg, Cfg) => cfg_expand,
114     (core_panic, CorePanic) => panic_expand,
115     (std_panic, StdPanic) => panic_expand,
116
117     EAGER:
118     (compile_error, CompileError) => compile_error_expand,
119     (concat, Concat) => concat_expand,
120     (concat_idents, ConcatIdents) => concat_idents_expand,
121     (include, Include) => include_expand,
122     (include_bytes, IncludeBytes) => include_bytes_expand,
123     (include_str, IncludeStr) => include_str_expand,
124     (env, Env) => env_expand,
125     (option_env, OptionEnv) => option_env_expand
126 }
127
128 fn module_path_expand(
129     _db: &dyn AstDatabase,
130     _id: MacroCallId,
131     _tt: &tt::Subtree,
132 ) -> ExpandResult<tt::Subtree> {
133     // Just return a dummy result.
134     ExpandResult::ok(quote! { "module::path" })
135 }
136
137 fn line_expand(
138     _db: &dyn AstDatabase,
139     _id: MacroCallId,
140     _tt: &tt::Subtree,
141 ) -> ExpandResult<tt::Subtree> {
142     // dummy implementation for type-checking purposes
143     let line_num = 0;
144     let expanded = quote! {
145         #line_num
146     };
147
148     ExpandResult::ok(expanded)
149 }
150
151 fn stringify_expand(
152     _db: &dyn AstDatabase,
153     _id: MacroCallId,
154     tt: &tt::Subtree,
155 ) -> ExpandResult<tt::Subtree> {
156     let pretty = tt::pretty(&tt.token_trees);
157
158     let expanded = quote! {
159         #pretty
160     };
161
162     ExpandResult::ok(expanded)
163 }
164
165 fn column_expand(
166     _db: &dyn AstDatabase,
167     _id: MacroCallId,
168     _tt: &tt::Subtree,
169 ) -> ExpandResult<tt::Subtree> {
170     // dummy implementation for type-checking purposes
171     let col_num = 0;
172     let expanded = quote! {
173         #col_num
174     };
175
176     ExpandResult::ok(expanded)
177 }
178
179 fn assert_expand(
180     _db: &dyn AstDatabase,
181     _id: MacroCallId,
182     tt: &tt::Subtree,
183 ) -> ExpandResult<tt::Subtree> {
184     let krate = tt::Ident { text: "$crate".into(), id: tt::TokenId::unspecified() };
185     let args = parse_exprs_with_sep(tt, ',');
186     let expanded = match &*args {
187         [cond, panic_args @ ..] => {
188             let comma = tt::Subtree {
189                 delimiter: None,
190                 token_trees: vec![tt::TokenTree::Leaf(tt::Leaf::Punct(tt::Punct {
191                     char: ',',
192                     spacing: tt::Spacing::Alone,
193                     id: tt::TokenId::unspecified(),
194                 }))],
195             };
196             let cond = cond.clone();
197             let panic_args = itertools::Itertools::intersperse(panic_args.iter().cloned(), comma);
198             quote! {{
199                 if !#cond {
200                     #krate::panic!(##panic_args);
201                 }
202             }}
203         }
204         [] => quote! {{}},
205     };
206
207     ExpandResult::ok(expanded)
208 }
209
210 fn file_expand(
211     _db: &dyn AstDatabase,
212     _id: MacroCallId,
213     _tt: &tt::Subtree,
214 ) -> ExpandResult<tt::Subtree> {
215     // FIXME: RA purposefully lacks knowledge of absolute file names
216     // so just return "".
217     let file_name = "";
218
219     let expanded = quote! {
220         #file_name
221     };
222
223     ExpandResult::ok(expanded)
224 }
225
226 fn format_args_expand(
227     _db: &dyn AstDatabase,
228     _id: MacroCallId,
229     tt: &tt::Subtree,
230 ) -> ExpandResult<tt::Subtree> {
231     // We expand `format_args!("", a1, a2)` to
232     // ```
233     // std::fmt::Arguments::new_v1(&[], &[
234     //   std::fmt::ArgumentV1::new(&arg1,std::fmt::Display::fmt),
235     //   std::fmt::ArgumentV1::new(&arg2,std::fmt::Display::fmt),
236     // ])
237     // ```,
238     // which is still not really correct, but close enough for now
239     let mut args = parse_exprs_with_sep(tt, ',');
240
241     if args.is_empty() {
242         return ExpandResult::only_err(mbe::ExpandError::NoMatchingRule);
243     }
244     for arg in &mut args {
245         // Remove `key =`.
246         if matches!(arg.token_trees.get(1), Some(tt::TokenTree::Leaf(tt::Leaf::Punct(p))) if p.char == '=' && p.spacing != tt::Spacing::Joint)
247         {
248             arg.token_trees.drain(..2);
249         }
250     }
251     let _format_string = args.remove(0);
252     let arg_tts = args.into_iter().flat_map(|arg| {
253         quote! { std::fmt::ArgumentV1::new(&(#arg), std::fmt::Display::fmt), }
254     }.token_trees);
255     let expanded = quote! {
256         // It's unsafe since https://github.com/rust-lang/rust/pull/83302
257         // Wrap an unsafe block to avoid false-positive `missing-unsafe` lint.
258         // FIXME: Currently we don't have `unused_unsafe` lint so an extra unsafe block won't cause issues on early
259         // stable rust-src.
260         unsafe {
261             std::fmt::Arguments::new_v1(&[], &[##arg_tts])
262         }
263     };
264     ExpandResult::ok(expanded)
265 }
266
267 fn asm_expand(
268     _db: &dyn AstDatabase,
269     _id: MacroCallId,
270     tt: &tt::Subtree,
271 ) -> ExpandResult<tt::Subtree> {
272     // We expand all assembly snippets to `format_args!` invocations to get format syntax
273     // highlighting for them.
274
275     let krate = tt::Ident { text: "$crate".into(), id: tt::TokenId::unspecified() };
276
277     let mut literals = Vec::new();
278     for tt in tt.token_trees.chunks(2) {
279         match tt {
280             [tt::TokenTree::Leaf(tt::Leaf::Literal(lit))]
281             | [tt::TokenTree::Leaf(tt::Leaf::Literal(lit)), tt::TokenTree::Leaf(tt::Leaf::Punct(tt::Punct { char: ',', id: _, spacing: _ }))] =>
282             {
283                 let krate = krate.clone();
284                 literals.push(quote!(#krate::format_args!(#lit);));
285             }
286             _ => break,
287         }
288     }
289
290     let expanded = quote! {{
291         ##literals
292         ()
293     }};
294     ExpandResult::ok(expanded)
295 }
296
297 fn global_asm_expand(
298     _db: &dyn AstDatabase,
299     _id: MacroCallId,
300     _tt: &tt::Subtree,
301 ) -> ExpandResult<tt::Subtree> {
302     // Expand to nothing (at item-level)
303     ExpandResult::ok(quote! {})
304 }
305
306 fn cfg_expand(
307     db: &dyn AstDatabase,
308     id: MacroCallId,
309     tt: &tt::Subtree,
310 ) -> ExpandResult<tt::Subtree> {
311     let loc = db.lookup_intern_macro(id);
312     let expr = CfgExpr::parse(tt);
313     let enabled = db.crate_graph()[loc.krate].cfg_options.check(&expr) != Some(false);
314     let expanded = if enabled { quote!(true) } else { quote!(false) };
315     ExpandResult::ok(expanded)
316 }
317
318 fn panic_expand(
319     db: &dyn AstDatabase,
320     id: MacroCallId,
321     tt: &tt::Subtree,
322 ) -> ExpandResult<tt::Subtree> {
323     let loc: MacroCallLoc = db.lookup_intern_macro(id);
324     // Expand to a macro call `$crate::panic::panic_{edition}`
325     let krate = tt::Ident { text: "$crate".into(), id: tt::TokenId::unspecified() };
326     let mut call = if db.crate_graph()[loc.krate].edition == Edition::Edition2021 {
327         quote!(#krate::panic::panic_2021!)
328     } else {
329         quote!(#krate::panic::panic_2015!)
330     };
331
332     // Pass the original arguments
333     call.token_trees.push(tt::TokenTree::Subtree(tt.clone()));
334     ExpandResult::ok(call)
335 }
336
337 fn unquote_str(lit: &tt::Literal) -> Option<String> {
338     let lit = ast::make::tokens::literal(&lit.to_string());
339     let token = ast::String::cast(lit)?;
340     token.value().map(|it| it.into_owned())
341 }
342
343 fn compile_error_expand(
344     _db: &dyn AstDatabase,
345     _id: MacroCallId,
346     tt: &tt::Subtree,
347 ) -> ExpandResult<Option<ExpandedEager>> {
348     let err = match &*tt.token_trees {
349         [tt::TokenTree::Leaf(tt::Leaf::Literal(it))] => {
350             let text = it.text.as_str();
351             if text.starts_with('"') && text.ends_with('"') {
352                 // FIXME: does not handle raw strings
353                 mbe::ExpandError::Other(text[1..text.len() - 1].to_string())
354             } else {
355                 mbe::ExpandError::BindingError("`compile_error!` argument must be a string".into())
356             }
357         }
358         _ => mbe::ExpandError::BindingError("`compile_error!` argument must be a string".into()),
359     };
360
361     ExpandResult { value: Some(ExpandedEager::new(quote! {})), err: Some(err) }
362 }
363
364 fn concat_expand(
365     _db: &dyn AstDatabase,
366     _arg_id: MacroCallId,
367     tt: &tt::Subtree,
368 ) -> ExpandResult<Option<ExpandedEager>> {
369     let mut err = None;
370     let mut text = String::new();
371     for (i, t) in tt.token_trees.iter().enumerate() {
372         match t {
373             tt::TokenTree::Leaf(tt::Leaf::Literal(it)) if i % 2 == 0 => {
374                 // concat works with string and char literals, so remove any quotes.
375                 // It also works with integer, float and boolean literals, so just use the rest
376                 // as-is.
377                 let component = unquote_str(it).unwrap_or_else(|| it.text.to_string());
378                 text.push_str(&component);
379             }
380             // handle boolean literals
381             tt::TokenTree::Leaf(tt::Leaf::Ident(id))
382                 if i % 2 == 0 && (id.text == "true" || id.text == "false") =>
383             {
384                 text.push_str(id.text.as_str());
385             }
386             tt::TokenTree::Leaf(tt::Leaf::Punct(punct)) if i % 2 == 1 && punct.char == ',' => (),
387             _ => {
388                 err.get_or_insert(mbe::ExpandError::UnexpectedToken);
389             }
390         }
391     }
392     ExpandResult { value: Some(ExpandedEager::new(quote!(#text))), err }
393 }
394
395 fn concat_idents_expand(
396     _db: &dyn AstDatabase,
397     _arg_id: MacroCallId,
398     tt: &tt::Subtree,
399 ) -> ExpandResult<Option<ExpandedEager>> {
400     let mut err = None;
401     let mut ident = String::new();
402     for (i, t) in tt.token_trees.iter().enumerate() {
403         match t {
404             tt::TokenTree::Leaf(tt::Leaf::Ident(id)) => {
405                 ident.push_str(id.text.as_str());
406             }
407             tt::TokenTree::Leaf(tt::Leaf::Punct(punct)) if i % 2 == 1 && punct.char == ',' => (),
408             _ => {
409                 err.get_or_insert(mbe::ExpandError::UnexpectedToken);
410             }
411         }
412     }
413     let ident = tt::Ident { text: ident.into(), id: tt::TokenId::unspecified() };
414     ExpandResult { value: Some(ExpandedEager::new(quote!(#ident))), err }
415 }
416
417 fn relative_file(
418     db: &dyn AstDatabase,
419     call_id: MacroCallId,
420     path_str: &str,
421     allow_recursion: bool,
422 ) -> Result<FileId, mbe::ExpandError> {
423     let call_site = call_id.as_file().original_file(db);
424     let path = AnchoredPath { anchor: call_site, path: path_str };
425     let res = db
426         .resolve_path(path)
427         .ok_or_else(|| mbe::ExpandError::Other(format!("failed to load file `{}`", path_str)))?;
428     // Prevent include itself
429     if res == call_site && !allow_recursion {
430         Err(mbe::ExpandError::Other(format!("recursive inclusion of `{}`", path_str)))
431     } else {
432         Ok(res)
433     }
434 }
435
436 fn parse_string(tt: &tt::Subtree) -> Result<String, mbe::ExpandError> {
437     tt.token_trees
438         .get(0)
439         .and_then(|tt| match tt {
440             tt::TokenTree::Leaf(tt::Leaf::Literal(it)) => unquote_str(it),
441             _ => None,
442         })
443         .ok_or(mbe::ExpandError::ConversionError)
444 }
445
446 fn include_expand(
447     db: &dyn AstDatabase,
448     arg_id: MacroCallId,
449     tt: &tt::Subtree,
450 ) -> ExpandResult<Option<ExpandedEager>> {
451     let res = (|| {
452         let path = parse_string(tt)?;
453         let file_id = relative_file(db, arg_id, &path, false)?;
454
455         let subtree =
456             parse_to_token_tree(&db.file_text(file_id)).ok_or(mbe::ExpandError::ConversionError)?.0;
457         Ok((subtree, file_id))
458     })();
459
460     match res {
461         Ok((subtree, file_id)) => {
462             ExpandResult::ok(Some(ExpandedEager { subtree, included_file: Some(file_id) }))
463         }
464         Err(e) => ExpandResult::only_err(e),
465     }
466 }
467
468 fn include_bytes_expand(
469     _db: &dyn AstDatabase,
470     _arg_id: MacroCallId,
471     tt: &tt::Subtree,
472 ) -> ExpandResult<Option<ExpandedEager>> {
473     if let Err(e) = parse_string(tt) {
474         return ExpandResult::only_err(e);
475     }
476
477     // FIXME: actually read the file here if the user asked for macro expansion
478     let res = tt::Subtree {
479         delimiter: None,
480         token_trees: vec![tt::TokenTree::Leaf(tt::Leaf::Literal(tt::Literal {
481             text: r#"b"""#.into(),
482             id: tt::TokenId::unspecified(),
483         }))],
484     };
485     ExpandResult::ok(Some(ExpandedEager::new(res)))
486 }
487
488 fn include_str_expand(
489     db: &dyn AstDatabase,
490     arg_id: MacroCallId,
491     tt: &tt::Subtree,
492 ) -> ExpandResult<Option<ExpandedEager>> {
493     let path = match parse_string(tt) {
494         Ok(it) => it,
495         Err(e) => return ExpandResult::only_err(e),
496     };
497
498     // FIXME: we're not able to read excluded files (which is most of them because
499     // it's unusual to `include_str!` a Rust file), but we can return an empty string.
500     // Ideally, we'd be able to offer a precise expansion if the user asks for macro
501     // expansion.
502     let file_id = match relative_file(db, arg_id, &path, true) {
503         Ok(file_id) => file_id,
504         Err(_) => {
505             return ExpandResult::ok(Some(ExpandedEager::new(quote!(""))));
506         }
507     };
508
509     let text = db.file_text(file_id);
510     let text = &*text;
511
512     ExpandResult::ok(Some(ExpandedEager::new(quote!(#text))))
513 }
514
515 fn get_env_inner(db: &dyn AstDatabase, arg_id: MacroCallId, key: &str) -> Option<String> {
516     let krate = db.lookup_intern_macro(arg_id).krate;
517     db.crate_graph()[krate].env.get(key)
518 }
519
520 fn env_expand(
521     db: &dyn AstDatabase,
522     arg_id: MacroCallId,
523     tt: &tt::Subtree,
524 ) -> ExpandResult<Option<ExpandedEager>> {
525     let key = match parse_string(tt) {
526         Ok(it) => it,
527         Err(e) => return ExpandResult::only_err(e),
528     };
529
530     let mut err = None;
531     let s = get_env_inner(db, arg_id, &key).unwrap_or_else(|| {
532         // The only variable rust-analyzer ever sets is `OUT_DIR`, so only diagnose that to avoid
533         // unnecessary diagnostics for eg. `CARGO_PKG_NAME`.
534         if key == "OUT_DIR" {
535             err = Some(mbe::ExpandError::Other(
536                 r#"`OUT_DIR` not set, enable "run build scripts" to fix"#.into(),
537             ));
538         }
539
540         // If the variable is unset, still return a dummy string to help type inference along.
541         // We cannot use an empty string here, because for
542         // `include!(concat!(env!("OUT_DIR"), "/foo.rs"))` will become
543         // `include!("foo.rs"), which might go to infinite loop
544         "__RA_UNIMPLEMENTED__".to_string()
545     });
546     let expanded = quote! { #s };
547
548     ExpandResult { value: Some(ExpandedEager::new(expanded)), err }
549 }
550
551 fn option_env_expand(
552     db: &dyn AstDatabase,
553     arg_id: MacroCallId,
554     tt: &tt::Subtree,
555 ) -> ExpandResult<Option<ExpandedEager>> {
556     let key = match parse_string(tt) {
557         Ok(it) => it,
558         Err(e) => return ExpandResult::only_err(e),
559     };
560
561     let expanded = match get_env_inner(db, arg_id, &key) {
562         None => quote! { std::option::Option::None::<&str> },
563         Some(s) => quote! { std::option::Some(#s) },
564     };
565
566     ExpandResult::ok(Some(ExpandedEager::new(expanded)))
567 }