Skip to main content

rustc_builtin_macros/
source_util.rs

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