Skip to main content

yog_cli/
script.rs

1//! A mod's Yog Script (`src/**/*.ys`, `*.yns`): each source checked by the
2//! compiler (`yog-script-build`), what's wrong reported at its file, line
3//! and column; errors stop the build.
4
5pub mod jvm;
6
7use std::path::Path;
8
9use yog_ids::Format;
10
11use crate::diagnostics;
12
13/// Where a mod's code is: its scripts beside its Rust.
14use crate::project::manifest::SOURCES_DIR as SOURCES;
15
16/// What every source of a mod's Yog Script is checked with, read once:
17/// what its `yog.toml` says it does (sending to the server), its files (for
18/// `include_str!`), and every `.ys` source's declarations (the generated
19/// ones too) — the types each source reaches of the others.
20pub(crate) struct ScriptProject {
21    to_server: bool,
22    files: yog_script_build::FilesBeside,
23    decls: Vec<(String, Vec<yog_script_analyze::ast::Decl>)>,
24}
25
26impl ScriptProject {
27    /// The project of the mod at `root`.
28    pub(crate) fn read(root: &Path) -> Self {
29        let to_server =
30            crate::project::manifest::YogToml::read(&root.join(crate::project::manifest::MANIFEST))
31                .is_ok_and(|m| m.to_server);
32        let files = yog_script_build::FilesBeside {
33            base: root.to_path_buf(),
34        };
35        let env = yog_script_analyze::Env {
36            files: Some(&files),
37            to_server,
38            ..Default::default()
39        };
40        let decls = jvm::sources(root)
41            .unwrap_or_default()
42            .into_iter()
43            .map(|(name, text)| {
44                let d = yog_script_analyze::declarations(&name, &text, env);
45                (name, d)
46            })
47            .collect();
48        ScriptProject {
49            to_server,
50            files,
51            decls,
52        }
53    }
54
55    /// Source `name`'s `text` checked in the project, its content files
56    /// (`content`: each (kind, id)) known when they are.
57    pub(crate) fn check(
58        &self,
59        name: &str,
60        text: &str,
61        content: Option<&[(String, String)]>,
62    ) -> Vec<yog_data::Diagnostic> {
63        let others: Vec<&[yog_script_analyze::ast::Decl]> = self
64            .decls
65            .iter()
66            .filter(|(other, _)| other != name)
67            .map(|(_, d)| d.as_slice())
68            .collect();
69        let project = yog_script_build::Project {
70            to_server: self.to_server,
71            files: Some(&self.files),
72            content,
73            others: &others,
74            ..Default::default()
75        };
76        yog_script_build::check_source_using(name, text, yog_script_build::index::load, project)
77    }
78}
79
80/// A script's text checked in the mod at `root` (an editor's: one file,
81/// the project read for it).
82pub(crate) fn check_text(
83    name: &str,
84    text: &str,
85    root: &Path,
86    content: Option<&[(String, String)]>,
87) -> Vec<yog_data::Diagnostic> {
88    ScriptProject::read(root).check(name, text, content)
89}
90
91/// The mod a file is in: the nearest folder up from it with a `yog.toml`.
92pub(crate) fn mod_root(file: &Path) -> Option<&Path> {
93    file.ancestors()
94        .find(|dir| dir.join(crate::project::manifest::MANIFEST).is_file())
95}
96
97/// Checks every script of the mod at `root` (a `.yns` on each game version
98/// it targets), its content files' (kind, id) `content`; an error when one
99/// has errors.
100pub fn check(root: &Path, content: &[(String, String)]) -> Result<(), String> {
101    let dir = root.join(SOURCES);
102    let files: Vec<_> = [Format::Script, Format::Nanites]
103        .into_iter()
104        .flat_map(|format| crate::files::of(&dir, format))
105        .collect();
106    if files.is_empty() {
107        return Ok(());
108    }
109    // Every source's declarations once, not once a source.
110    let project = ScriptProject::read(root);
111    let mut errors = 0;
112    for file in files {
113        let text =
114            std::fs::read_to_string(&file).map_err(|e| format!("{}: {e}", file.display()))?;
115        let name = crate::shown_path(root, &file);
116        let problems = project.check(&name, &text, Some(content));
117        errors += problems.iter().filter(|d| d.is_error()).count();
118        diagnostics::report_all(&problems);
119        for d in &problems {
120            match d.is_error() {
121                true => yog_logging::error!("{d}"),
122                false => yog_logging::warn!("{d}"),
123            }
124        }
125    }
126    match errors {
127        0 => Ok(()),
128        n => Err(format!("{n} error(s) in the mod's Yog Script")),
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn a_mods_scripts_are_checked() {
138        let root = std::env::temp_dir().join(format!("yog-script-check-{}", std::process::id()));
139        let src = root.join(SOURCES).join("power");
140        std::fs::create_dir_all(&src).unwrap();
141        std::fs::write(src.join("Ok.ys"), "i32 Area(i32 w, i32 h) => w * h;\n").unwrap();
142        assert_eq!(check(&root, &[]), Ok(()));
143        // One source reaches another's types.
144        std::fs::write(src.join("Size.ys"), "public record Size(i32 W, i32 H);\n").unwrap();
145        std::fs::write(src.join("Use.ys"), "i32 W(Size s) => s.W;\n").unwrap();
146        assert_eq!(check(&root, &[]), Ok(()));
147        std::fs::write(src.join("Bad.ys"), "void F() {\n    break;\n}\n").unwrap();
148        assert!(check(&root, &[]).is_err());
149        std::fs::remove_dir_all(&root).unwrap();
150    }
151}