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