rustc_builtin_macros/
source_util.rs

1use std::path::{Path, PathBuf};
2use std::rc::Rc;
3use std::sync::Arc;
4
5use rustc_ast as ast;
6use rustc_ast::ptr::P;
7use rustc_ast::token;
8use rustc_ast::tokenstream::TokenStream;
9use rustc_ast_pretty::pprust;
10use rustc_expand::base::{
11    DummyResult, ExpandResult, ExtCtxt, MacEager, MacResult, MacroExpanderResult, resolve_path,
12};
13use rustc_expand::module::DirOwnership;
14use rustc_lint_defs::BuiltinLintDiag;
15use rustc_parse::parser::{ForceCollect, Parser};
16use rustc_parse::{new_parser_from_file, unwrap_or_emit_fatal, utf8_error};
17use rustc_session::lint::builtin::INCOMPLETE_INCLUDE;
18use rustc_span::source_map::SourceMap;
19use rustc_span::{Pos, Span, Symbol};
20use smallvec::SmallVec;
21
22use crate::errors;
23use crate::util::{
24    check_zero_tts, get_single_str_from_tts, get_single_str_spanned_from_tts, parse_expr,
25};
26
27// These macros all relate to the file system; they either return
28// the column/row/filename of the expression, or they include
29// a given file into the current one.
30
31/// line!(): expands to the current line number
32pub(crate) fn expand_line(
33    cx: &mut ExtCtxt<'_>,
34    sp: Span,
35    tts: TokenStream,
36) -> MacroExpanderResult<'static> {
37    let sp = cx.with_def_site_ctxt(sp);
38    check_zero_tts(cx, sp, tts, "line!");
39
40    let topmost = cx.expansion_cause().unwrap_or(sp);
41    let loc = cx.source_map().lookup_char_pos(topmost.lo());
42
43    ExpandResult::Ready(MacEager::expr(cx.expr_u32(topmost, loc.line as u32)))
44}
45
46/* column!(): expands to the current column number */
47pub(crate) fn expand_column(
48    cx: &mut ExtCtxt<'_>,
49    sp: Span,
50    tts: TokenStream,
51) -> MacroExpanderResult<'static> {
52    let sp = cx.with_def_site_ctxt(sp);
53    check_zero_tts(cx, sp, tts, "column!");
54
55    let topmost = cx.expansion_cause().unwrap_or(sp);
56    let loc = cx.source_map().lookup_char_pos(topmost.lo());
57
58    ExpandResult::Ready(MacEager::expr(cx.expr_u32(topmost, loc.col.to_usize() as u32 + 1)))
59}
60
61/// file!(): expands to the current filename */
62/// The source_file (`loc.file`) contains a bunch more information we could spit
63/// out if we wanted.
64pub(crate) fn expand_file(
65    cx: &mut ExtCtxt<'_>,
66    sp: Span,
67    tts: TokenStream,
68) -> MacroExpanderResult<'static> {
69    let sp = cx.with_def_site_ctxt(sp);
70    check_zero_tts(cx, sp, tts, "file!");
71
72    let topmost = cx.expansion_cause().unwrap_or(sp);
73    let loc = cx.source_map().lookup_char_pos(topmost.lo());
74
75    use rustc_session::RemapFileNameExt;
76    use rustc_session::config::RemapPathScopeComponents;
77    ExpandResult::Ready(MacEager::expr(cx.expr_str(
78        topmost,
79        Symbol::intern(
80            &loc.file.name.for_scope(cx.sess, RemapPathScopeComponents::MACRO).to_string_lossy(),
81        ),
82    )))
83}
84
85pub(crate) fn expand_stringify(
86    cx: &mut ExtCtxt<'_>,
87    sp: Span,
88    tts: TokenStream,
89) -> MacroExpanderResult<'static> {
90    let sp = cx.with_def_site_ctxt(sp);
91    let s = pprust::tts_to_string(&tts);
92    ExpandResult::Ready(MacEager::expr(cx.expr_str(sp, Symbol::intern(&s))))
93}
94
95pub(crate) fn expand_mod(
96    cx: &mut ExtCtxt<'_>,
97    sp: Span,
98    tts: TokenStream,
99) -> MacroExpanderResult<'static> {
100    let sp = cx.with_def_site_ctxt(sp);
101    check_zero_tts(cx, sp, tts, "module_path!");
102    let mod_path = &cx.current_expansion.module.mod_path;
103    let string = mod_path.iter().map(|x| x.to_string()).collect::<Vec<String>>().join("::");
104
105    ExpandResult::Ready(MacEager::expr(cx.expr_str(sp, Symbol::intern(&string))))
106}
107
108/// include! : parse the given file as an expr
109/// This is generally a bad idea because it's going to behave
110/// unhygienically.
111pub(crate) fn expand_include<'cx>(
112    cx: &'cx mut ExtCtxt<'_>,
113    sp: Span,
114    tts: TokenStream,
115) -> MacroExpanderResult<'cx> {
116    let sp = cx.with_def_site_ctxt(sp);
117    let ExpandResult::Ready(mac) = get_single_str_from_tts(cx, sp, tts, "include!") else {
118        return ExpandResult::Retry(());
119    };
120    let file = match mac {
121        Ok(file) => file,
122        Err(guar) => return ExpandResult::Ready(DummyResult::any(sp, guar)),
123    };
124    // The file will be added to the code map by the parser
125    let file = match resolve_path(&cx.sess, file.as_str(), sp) {
126        Ok(f) => f,
127        Err(err) => {
128            let guar = err.emit();
129            return ExpandResult::Ready(DummyResult::any(sp, guar));
130        }
131    };
132    let p = unwrap_or_emit_fatal(new_parser_from_file(cx.psess(), &file, Some(sp)));
133
134    // If in the included file we have e.g., `mod bar;`,
135    // then the path of `bar.rs` should be relative to the directory of `file`.
136    // See https://github.com/rust-lang/rust/pull/69838/files#r395217057 for a discussion.
137    // `MacroExpander::fully_expand_fragment` later restores, so "stack discipline" is maintained.
138    let dir_path = file.parent().unwrap_or(&file).to_owned();
139    cx.current_expansion.module = Rc::new(cx.current_expansion.module.with_dir_path(dir_path));
140    cx.current_expansion.dir_ownership = DirOwnership::Owned { relative: None };
141
142    struct ExpandInclude<'a> {
143        p: Parser<'a>,
144        node_id: ast::NodeId,
145    }
146    impl<'a> MacResult for ExpandInclude<'a> {
147        fn make_expr(mut self: Box<ExpandInclude<'a>>) -> Option<P<ast::Expr>> {
148            let expr = parse_expr(&mut self.p).ok()?;
149            if self.p.token != token::Eof {
150                self.p.psess.buffer_lint(
151                    INCOMPLETE_INCLUDE,
152                    self.p.token.span,
153                    self.node_id,
154                    BuiltinLintDiag::IncompleteInclude,
155                );
156            }
157            Some(expr)
158        }
159
160        fn make_items(mut self: Box<ExpandInclude<'a>>) -> Option<SmallVec<[P<ast::Item>; 1]>> {
161            let mut ret = SmallVec::new();
162            loop {
163                match self.p.parse_item(ForceCollect::No) {
164                    Err(err) => {
165                        err.emit();
166                        break;
167                    }
168                    Ok(Some(item)) => ret.push(item),
169                    Ok(None) => {
170                        if self.p.token != token::Eof {
171                            self.p
172                                .dcx()
173                                .create_err(errors::ExpectedItem {
174                                    span: self.p.token.span,
175                                    token: &pprust::token_to_string(&self.p.token),
176                                })
177                                .emit();
178                        }
179
180                        break;
181                    }
182                }
183            }
184            Some(ret)
185        }
186    }
187
188    ExpandResult::Ready(Box::new(ExpandInclude { p, node_id: cx.current_expansion.lint_node_id }))
189}
190
191/// `include_str!`: read the given file, insert it as a literal string expr
192pub(crate) fn expand_include_str(
193    cx: &mut ExtCtxt<'_>,
194    sp: Span,
195    tts: TokenStream,
196) -> MacroExpanderResult<'static> {
197    let sp = cx.with_def_site_ctxt(sp);
198    let ExpandResult::Ready(mac) = get_single_str_spanned_from_tts(cx, sp, tts, "include_str!")
199    else {
200        return ExpandResult::Retry(());
201    };
202    let (path, path_span) = match mac {
203        Ok(res) => res,
204        Err(guar) => return ExpandResult::Ready(DummyResult::any(sp, guar)),
205    };
206    ExpandResult::Ready(match load_binary_file(cx, path.as_str().as_ref(), sp, path_span) {
207        Ok((bytes, bsp)) => match std::str::from_utf8(&bytes) {
208            Ok(src) => {
209                let interned_src = Symbol::intern(src);
210                MacEager::expr(cx.expr_str(cx.with_def_site_ctxt(bsp), interned_src))
211            }
212            Err(utf8err) => {
213                let mut err = cx.dcx().struct_span_err(sp, format!("`{path}` wasn't a utf-8 file"));
214                utf8_error(cx.source_map(), path.as_str(), None, &mut err, utf8err, &bytes[..]);
215                DummyResult::any(sp, err.emit())
216            }
217        },
218        Err(dummy) => dummy,
219    })
220}
221
222pub(crate) fn expand_include_bytes(
223    cx: &mut ExtCtxt<'_>,
224    sp: Span,
225    tts: TokenStream,
226) -> MacroExpanderResult<'static> {
227    let sp = cx.with_def_site_ctxt(sp);
228    let ExpandResult::Ready(mac) = get_single_str_spanned_from_tts(cx, sp, tts, "include_bytes!")
229    else {
230        return ExpandResult::Retry(());
231    };
232    let (path, path_span) = match mac {
233        Ok(res) => res,
234        Err(guar) => return ExpandResult::Ready(DummyResult::any(sp, guar)),
235    };
236    ExpandResult::Ready(match load_binary_file(cx, path.as_str().as_ref(), sp, path_span) {
237        Ok((bytes, _bsp)) => {
238            // Don't care about getting the span for the raw bytes,
239            // because the console can't really show them anyway.
240            let expr = cx.expr(sp, ast::ExprKind::IncludedBytes(bytes));
241            MacEager::expr(expr)
242        }
243        Err(dummy) => dummy,
244    })
245}
246
247fn load_binary_file(
248    cx: &ExtCtxt<'_>,
249    original_path: &Path,
250    macro_span: Span,
251    path_span: Span,
252) -> Result<(Arc<[u8]>, Span), Box<dyn MacResult>> {
253    let resolved_path = match resolve_path(&cx.sess, original_path, macro_span) {
254        Ok(path) => path,
255        Err(err) => {
256            let guar = err.emit();
257            return Err(DummyResult::any(macro_span, guar));
258        }
259    };
260    match cx.source_map().load_binary_file(&resolved_path) {
261        Ok(data) => Ok(data),
262        Err(io_err) => {
263            let mut err = cx.dcx().struct_span_err(
264                macro_span,
265                format!("couldn't read `{}`: {io_err}", resolved_path.display()),
266            );
267
268            if original_path.is_relative() {
269                let source_map = cx.sess.source_map();
270                let new_path = source_map
271                    .span_to_filename(macro_span.source_callsite())
272                    .into_local_path()
273                    .and_then(|src| find_path_suggestion(source_map, src.parent()?, original_path))
274                    .and_then(|path| path.into_os_string().into_string().ok());
275
276                if let Some(new_path) = new_path {
277                    err.span_suggestion_verbose(
278                        path_span,
279                        "there is a file with the same name in a different directory",
280                        format!("\"{}\"", new_path.replace('\\', "/").escape_debug()),
281                        rustc_lint_defs::Applicability::MachineApplicable,
282                    );
283                }
284            }
285            let guar = err.emit();
286            Err(DummyResult::any(macro_span, guar))
287        }
288    }
289}
290
291fn find_path_suggestion(
292    source_map: &SourceMap,
293    base_dir: &Path,
294    wanted_path: &Path,
295) -> Option<PathBuf> {
296    // Fix paths that assume they're relative to cargo manifest dir
297    let mut base_c = base_dir.components();
298    let mut wanted_c = wanted_path.components();
299    let mut without_base = None;
300    while let Some(wanted_next) = wanted_c.next() {
301        if wanted_c.as_path().file_name().is_none() {
302            break;
303        }
304        // base_dir may be absolute
305        while let Some(base_next) = base_c.next() {
306            if base_next == wanted_next {
307                without_base = Some(wanted_c.as_path());
308                break;
309            }
310        }
311    }
312    let root_absolute = without_base.into_iter().map(PathBuf::from);
313
314    let base_dir_components = base_dir.components().count();
315    // Avoid going all the way to the root dir
316    let max_parent_components = if base_dir.is_relative() {
317        base_dir_components + 1
318    } else {
319        base_dir_components.saturating_sub(1)
320    };
321
322    // Try with additional leading ../
323    let mut prefix = PathBuf::new();
324    let add = std::iter::from_fn(|| {
325        prefix.push("..");
326        Some(prefix.join(wanted_path))
327    })
328    .take(max_parent_components.min(3));
329
330    // Try without leading directories
331    let mut trimmed_path = wanted_path;
332    let remove = std::iter::from_fn(|| {
333        let mut components = trimmed_path.components();
334        let removed = components.next()?;
335        trimmed_path = components.as_path();
336        let _ = trimmed_path.file_name()?; // ensure there is a file name left
337        Some([
338            Some(trimmed_path.to_path_buf()),
339            (removed != std::path::Component::ParentDir)
340                .then(|| Path::new("..").join(trimmed_path)),
341        ])
342    })
343    .flatten()
344    .flatten()
345    .take(4);
346
347    root_absolute
348        .chain(add)
349        .chain(remove)
350        .find(|new_path| source_map.file_exists(&base_dir.join(&new_path)))
351}