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