safety_parser/configuration/
mod.rs

1//! Property definition through config file.
2use indexmap::IndexMap;
3use serde::Deserialize;
4use std::{fs, sync::LazyLock};
5
6pub mod env;
7
8pub type Str = Box<str>;
9pub type OptStr = Option<Box<str>>;
10
11#[derive(Debug, Deserialize)]
12pub struct Configuration {
13    pub package: Option<Package>,
14    pub tag: IndexMap<Str, Tag>,
15    #[serde(default)]
16    pub doc: GenDocOption,
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    pub fn as_str(&self) -> &'static str {
68        match self {
69            TagType::Precond => "precond",
70            TagType::Hazard => "Hazard",
71            TagType::Option => "option",
72        }
73    }
74}
75
76/// If types field doesn't exist, default to Precond.
77fn default_types() -> Box<[TagType]> {
78    Box::new([TagType::Precond])
79}
80
81#[derive(Clone, Copy, Debug, Deserialize, Default)]
82pub struct GenDocOption {
83    /// Generate `/// Safety` at the beginning.
84    #[serde(default)]
85    pub heading_safety_title: bool,
86    /// Generate `Tag:` before `desc`.
87    #[serde(default)]
88    pub heading_tag: bool,
89}
90
91impl GenDocOption {
92    fn merge(&mut self, other: &Self) {
93        if other.heading_safety_title {
94            self.heading_safety_title = true;
95        }
96        if other.heading_tag {
97            self.heading_tag = true;
98        }
99    }
100}
101
102/// `any` tag is denied in user's spec, and special in doc generation.
103pub const ANY: &str = "any";
104
105/// Data shared in `#[safety]` proc macro.
106#[derive(Debug)]
107struct Key {
108    /// Tag defined in config file.
109    tag: Tag,
110    /// File path where the tag is defined: we must be sure each tag only
111    /// derives from single file path.
112    #[allow(dead_code)]
113    src: Str,
114}
115
116#[derive(Default)]
117struct Cache {
118    /// Defined tags.
119    map: IndexMap<Str, Key>,
120    /// Merged doc generation options: if any is true, set true.
121    doc: GenDocOption,
122}
123
124static CACHE: LazyLock<Cache> = LazyLock::new(|| {
125    let mut cache = Cache::default();
126
127    let configs: Vec<_> = env::toml_file_paths()
128        .into_iter()
129        .map(|f| (Configuration::read_toml(&f), f.into_boxed_str()))
130        .collect();
131    let cap = configs.iter().map(|c| c.0.tag.len()).sum();
132    cache.map.reserve(cap);
133
134    for (config, path) in configs {
135        for (name, tag) in config.tag {
136            if &*name == ANY {
137                panic!("`any` is a builtin tag. Please remove it from spec.");
138            }
139            if let Some(old) = cache.map.get(&name) {
140                panic!("Tag {name:?} has been defined: {old:?}");
141            }
142            _ = cache.map.insert(name, Key { tag, src: path.clone() });
143        }
144        cache.doc.merge(&config.doc);
145    }
146
147    cache.map.sort_unstable_keys();
148    eprintln!("Got {} tags.", cache.map.len());
149    cache
150});
151
152pub fn get_tag(name: &str) -> &'static Tag {
153    &CACHE.map.get(name).unwrap_or_else(|| panic!("Tag {name:?} is not defined")).tag
154}
155
156pub fn get_tag_opt(name: &str) -> Option<&'static Tag> {
157    CACHE.map.get(name).map(|val| &val.tag)
158}
159
160pub fn doc_option() -> GenDocOption {
161    CACHE.doc
162}
163
164pub struct DefinedTag {
165    pub name: &'static str,
166    pub args: &'static Tag,
167}
168
169impl DefinedTag {
170    pub fn hover_detail(&self) -> String {
171        let name = self.name;
172        let args = &*self.args.args;
173        if args.is_empty() {
174            name.to_owned()
175        } else {
176            let args = args.join(", ");
177            format!("{name}({args})")
178        }
179    }
180
181    pub fn hover_documentation(&self) -> String {
182        use std::fmt::Write;
183
184        let DefinedTag { args: Tag { desc, expr, types, url, .. }, .. } = self;
185        let mut doc = String::new();
186
187        let types_field = if types.len() == 1 { "type" } else { "types" };
188        let types = types.iter().map(|t| t.as_str()).collect::<Vec<_>>().join(", ");
189        _ = writeln!(&mut doc, "**{types_field}**: {types}\n");
190
191        if let Some(desc) = desc {
192            _ = writeln!(&mut doc, "**desc**: {desc}\n");
193        }
194        if let Some(expr) = expr {
195            _ = writeln!(&mut doc, "**expr**: {expr}\n");
196        }
197        if let Some(url) = url {
198            _ = writeln!(&mut doc, "**url**: <{url}>");
199        }
200        doc
201    }
202}
203
204/// Get all tags defined in all spec TOMLs.
205pub fn get_tags() -> Box<[DefinedTag]> {
206    CACHE.map.iter().map(|(k, v)| DefinedTag { name: k, args: &v.tag }).collect()
207}