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