Skip to main content

zenith_core/ast/
policy.rs

1//! Document-level diagnostic-policy AST types.
2//!
3//! A `.zen` document may carry a root `diagnostics { … }` block that adjusts how
4//! specific diagnostic codes are *reported*. This is a lint-level model in the
5//! spirit of rustc lint levels:
6//!
7//! ```text
8//! diagnostics {
9//!     allow "layout.off_canvas"     // suppress this advisory
10//!     deny  "font.local"            // elevate to a blocking Error (CI gate)
11//!     warn  "node.unknown_property" // force to Warning
12//! }
13//! ```
14//!
15//! The policy affects ONLY which diagnostics are surfaced by validation — it is
16//! consulted in [`crate::validate()`] and nowhere else. The scene compiler and the
17//! render path never see it, so a policy can never change rendered output. A
18//! document with no `diagnostics` block parses to an empty [`DiagnosticPolicy`],
19//! which is an identity pass (no entries → no effect), so the default-off path is
20//! byte-identical to before this feature existed.
21//!
22//! Bright lines (see [`crate::validate()`] for the application logic):
23//! - Policy applies to **Warning**- and **Advisory**-severity diagnostics only.
24//!   **Error** severity is IMMUTABLE: an `allow` never drops an Error and a
25//!   `warn` never weakens an Error.
26//! - **Last-wins** for duplicate codes: a later entry for the same code overrides
27//!   any earlier one (exactly like rustc lint levels on the command line).
28
29use super::Span;
30
31/// The verb of a single [`PolicyEntry`] — how a diagnostic code's reporting is
32/// adjusted.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum PolicyVerb {
35    /// Suppress the diagnostic when its severity is Warning or Advisory. An
36    /// Error-severity diagnostic is left unchanged (Errors are immutable).
37    Allow,
38    /// Elevate the diagnostic to Error severity (turning a Warning/Advisory into
39    /// a blocking Error). An already-Error diagnostic stays Error.
40    Deny,
41    /// Force the diagnostic to Warning severity when it is currently Warning or
42    /// Advisory. An Error-severity diagnostic is left unchanged.
43    Warn,
44}
45
46/// A single entry inside a document's `diagnostics { … }` block: one verb
47/// applied to one diagnostic code.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct PolicyEntry {
50    /// What this entry does to the named code.
51    pub verb: PolicyVerb,
52    /// The diagnostic code this entry governs, e.g. `"layout.off_canvas"`.
53    pub code: String,
54    /// Source declaration span, when available.
55    pub source_span: Option<Span>,
56}
57
58/// The complete document-level diagnostic policy: an ordered list of
59/// [`PolicyEntry`] records as written in the `diagnostics { … }` block.
60///
61/// The default value is empty, which is an identity pass: with no entries the
62/// policy has no effect on validation output. Resolution is **last-wins** — see
63/// [`DiagnosticPolicy::verb_for`].
64#[derive(Debug, Clone, PartialEq, Eq, Default)]
65pub struct DiagnosticPolicy {
66    /// Policy entries in source order. Declaration order is preserved so the
67    /// formatter can round-trip the block verbatim; resolution applies last-wins.
68    pub entries: Vec<PolicyEntry>,
69}
70
71impl DiagnosticPolicy {
72    /// The effective verb for `code`, or `None` if no entry governs it.
73    ///
74    /// Resolution is **last-wins**: when several entries name the same code, the
75    /// last one written takes effect, so we scan in reverse and return the first
76    /// match.
77    pub fn verb_for(&self, code: &str) -> Option<&PolicyVerb> {
78        self.entries
79            .iter()
80            .rev()
81            .find(|e| e.code == code)
82            .map(|e| &e.verb)
83    }
84}
85
86// `Eq` is derivable on `DiagnosticPolicy`/`PolicyEntry` only because `PolicyVerb`
87// is `Eq` and `Span`/`String` are `Eq`; if a future field breaks `Eq`, drop it
88// from the derive rather than suppressing.
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    fn entry(verb: PolicyVerb, code: &str) -> PolicyEntry {
95        PolicyEntry {
96            verb,
97            code: code.to_owned(),
98            source_span: None,
99        }
100    }
101
102    #[test]
103    fn default_policy_is_empty_and_inert() {
104        let p = DiagnosticPolicy::default();
105        assert!(p.entries.is_empty());
106        assert_eq!(p.verb_for("anything"), None);
107    }
108
109    #[test]
110    fn verb_for_returns_the_governing_verb() {
111        let p = DiagnosticPolicy {
112            entries: vec![entry(PolicyVerb::Allow, "layout.off_canvas")],
113        };
114        assert_eq!(p.verb_for("layout.off_canvas"), Some(&PolicyVerb::Allow));
115        assert_eq!(p.verb_for("token.unused"), None);
116    }
117
118    #[test]
119    fn verb_for_is_last_wins() {
120        let p = DiagnosticPolicy {
121            entries: vec![
122                entry(PolicyVerb::Deny, "node.unknown_property"),
123                entry(PolicyVerb::Warn, "node.unknown_property"),
124            ],
125        };
126        // The later `warn` overrides the earlier `deny`.
127        assert_eq!(p.verb_for("node.unknown_property"), Some(&PolicyVerb::Warn));
128    }
129}