Skip to main content

yog_script_analyze/macros/
includes.rs

1//! `include_str!("help.txt")`: a file of the mod read when the source is,
2//! as Rust's — the call replaced by the file's text, a string written as
3//! it is (no `{…}` read in it). The build reads the file, beside the
4//! source; without one (an editor's check) the call stays, typed a string.
5
6use yog_analysis::Report;
7use yog_ids::script_problem;
8
9use super::builtin::INCLUDE_STR;
10use crate::source::token::{Punct, StrPart, Tok, Token};
11
12/// What reads a mod's files for its sources.
13pub trait ModFiles {
14    /// File `path`, written in source `source` (beside it): its text, or
15    /// why it can't be read.
16    fn read(&self, source: &str, path: &str) -> Result<String, String>;
17}
18
19/// Each `include_str!("path")` in `tokens` (of source `source`) replaced by
20/// the file's text; one that can't be read reported, left as it is.
21pub(crate) fn expand(
22    mut tokens: Vec<Token>,
23    (source, files): (&str, &dyn ModFiles),
24    report: &mut Report<'_>,
25) -> Vec<Token> {
26    let mut i = 0;
27    while i < tokens.len() {
28        let Some(path) = call_at(&tokens, i) else {
29            i += 1;
30            continue;
31        };
32        let at = tokens[i].span.to(tokens[i + CALL_LEN - 1].span);
33        match files.read(source, &path) {
34            Ok(text) => {
35                let lit = Token {
36                    tok: Tok::RawStr(text),
37                    span: at,
38                };
39                tokens.splice(i..i + CALL_LEN, [lit]);
40            }
41            Err(why) => {
42                report.error(
43                    script_problem::MACRO,
44                    at,
45                    format!("`{}!` can't read `{path}`: {why}", INCLUDE_STR.name),
46                );
47                i += CALL_LEN;
48            }
49        }
50        i += 1;
51    }
52    tokens
53}
54
55/// `include_str ! ( "path" )`: its tokens.
56const CALL_LEN: usize = 5;
57
58/// The path of the `include_str!` call at `i`, its argument a plain string.
59fn call_at(tokens: &[Token], i: usize) -> Option<String> {
60    let call = tokens.get(i..i + CALL_LEN)?;
61    let named = matches!(&call[0].tok, Tok::Ident(n) if n == INCLUDE_STR.name);
62    let shaped = call[1].tok == Tok::Punct(Punct::Bang)
63        && call[2].tok == Tok::Punct(Punct::LParen)
64        && call[4].tok == Tok::Punct(Punct::RParen);
65    if !named || !shaped {
66        return None;
67    }
68    match &call[3].tok {
69        Tok::RawStr(s) => Some(s.clone()),
70        Tok::Str(parts) => match parts.as_slice() {
71            [StrPart::Text(s)] => Some(s.clone()),
72            [] => Some(String::new()),
73            _ => None,
74        },
75        _ => None,
76    }
77}