safety_parser/
configuration.rs

1//! Property definition through config file.
2use indexmap::IndexMap;
3use serde::Deserialize;
4use std::{
5    env::{self, var},
6    fs,
7    sync::LazyLock,
8};
9
10pub type Str = Box<str>;
11pub type OptStr = Option<Box<str>>;
12
13#[derive(Debug, Deserialize)]
14pub struct Configuration {
15    pub package: Package,
16    pub tag: IndexMap<Str, Tag>,
17}
18
19impl Configuration {
20    pub fn read_toml(path: &str) -> Self {
21        if !fs::exists(path).unwrap() {
22            panic!("{path:?} doesn't exist.")
23        }
24        let text =
25            &fs::read_to_string(path).unwrap_or_else(|e| panic!("Failed to read {path}:\n{e}"));
26        toml::from_str(text).unwrap_or_else(|e| panic!("Failed to parse {path}:\n{e}"))
27    }
28}
29
30#[derive(Debug, Deserialize)]
31pub struct Package {
32    pub name: Box<str>,
33    pub version: OptStr,
34    pub crate_name: OptStr,
35}
36
37#[derive(Debug, Deserialize)]
38pub struct Tag {
39    pub args: Box<[Str]>,
40    pub desc: OptStr,
41    pub expr: OptStr,
42    #[serde(default = "default_types")]
43    pub types: Box<[TagType]>,
44    pub url: OptStr,
45}
46
47#[derive(Clone, Copy, Debug, Deserialize, Default, PartialEq, Eq)]
48#[serde(rename_all = "lowercase")]
49pub enum TagType {
50    #[default]
51    Precond,
52    Hazard,
53    Option,
54}
55
56impl TagType {
57    pub fn new(s: &str) -> Self {
58        match s {
59            "precond" => Self::Precond,
60            "hazard" => Self::Hazard,
61            "option" => Self::Option,
62            _ => panic!("Only support: precond, hazard, and option."),
63        }
64    }
65}
66
67/// If types field doesn't exist, default to Precond.
68fn default_types() -> Box<[TagType]> {
69    Box::new([TagType::Precond])
70}
71
72/// Single toml config file path.
73pub const ENV_SP_FILE: &str = "SP_FILE";
74/// Folder where all toml files are searched.
75pub const ENV_SP_DIR: &str = "SP_DIR";
76
77/// If ENV_SP_DIR or ENV_SP_DIR is provided, check tag and emit `#[doc]` for each tag.
78/// If neither is provided, do nothing.
79pub fn config_exists() -> bool {
80    static EMIT: LazyLock<bool> =
81        LazyLock::new(|| var(ENV_SP_FILE).is_ok() || var(ENV_SP_DIR).is_ok());
82    *EMIT
83}
84
85/// Paths to toml config. Pass one of these env vars:
86/// * if `SP_FILE` is specified, use that toml path
87/// * if `SP_DIR` is specified, use that path to find toml files
88/// * if both are given, only respect `SP_FILE`
89pub fn toml_file_paths() -> Vec<String> {
90    if let Ok(file) = env::var(ENV_SP_FILE) {
91        vec![file]
92    } else if let Ok(dir) = env::var(ENV_SP_DIR) {
93        let mut files = Vec::new();
94        for entry in
95            fs::read_dir(&dir).unwrap_or_else(|e| panic!("Failed to read {dir} folder:\n{e}"))
96        {
97            let entry = entry.unwrap();
98            let path = entry.path();
99            if path.extension().map(|ext| ext == "toml").unwrap_or(false) {
100                files.push(path.into_os_string().into_string().unwrap());
101            }
102        }
103        files
104    } else {
105        panic!("Environment variable `SP_FILE` or `SP_DIR` should be specified.");
106    }
107}
108
109/// Data shared in `#[safety]` proc macro.
110#[derive(Debug)]
111pub struct Key {
112    /// Tag defined in config file.
113    pub tag: Tag,
114    /// File path where the tag is defined: we must be sure each tag only
115    /// derives from single file path.
116    pub src: Str,
117}
118
119pub static TAGS: LazyLock<IndexMap<Str, Key>> = LazyLock::new(|| {
120    let configs: Vec<_> =
121        toml_file_paths().into_iter().map(|f| (Configuration::read_toml(&f), f)).collect();
122    let cap = configs.iter().map(|c| c.0.tag.len()).sum();
123    let mut map = IndexMap::with_capacity(cap);
124    for (config, path) in configs {
125        for (name, tag) in config.tag {
126            if let Some(old) = map.get(&name) {
127                panic!("Tag {name:?} has been defined: {old:?}");
128            }
129            _ = map.insert(name, Key { tag, src: (&*path).into() });
130        }
131    }
132    map.sort_unstable_keys();
133    eprintln!("Got {} tags.", map.len());
134    map
135});
136
137pub fn get_tag(name: &str) -> &'static Tag {
138    &TAGS.get(name).unwrap_or_else(|| panic!("Tag {name:?} is not defined")).tag
139}