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}