Skip to main content

zenith_core/validate/check/
policy.rs

1//! Diagnostic-policy application and self-validation.
2//!
3//! The document-level [`DiagnosticPolicy`] (parsed from a root `diagnostics { … }`
4//! block) adjusts how diagnostic codes are *reported*. This module holds the two
5//! steps the [driver](super::driver) runs at the end of validation:
6//!
7//! 1. [`apply_policy`] rewrites the assembled diagnostic list per the policy.
8//! 2. [`check_policy_entries`] appends diagnostics ABOUT the policy itself
9//!    (unknown codes, entries that try to weaken an Error).
10//!
11//! These run in that order so a policy can never suppress the warnings about
12//! itself: self-validation is appended *after* `apply_policy` has already run.
13//!
14//! ## Bright lines
15//!
16//! - Policy applies to **Warning**- and **Advisory**-severity diagnostics only.
17//!   **Error** severity is IMMUTABLE.
18//!   - `allow`: severity != Error → drop it; Error → keep unchanged.
19//!   - `deny` : severity != Error → set Error, keep; already-Error → keep.
20//!   - `warn` : severity != Error → set Warning, keep; Error → keep unchanged.
21//! - The policy NEVER changes rendered output — it is consulted only here, in
22//!   validation. With an empty policy, `apply_policy` is the identity function.
23
24use crate::ast::policy::{DiagnosticPolicy, PolicyVerb};
25use crate::diag_catalog;
26use crate::diagnostics::{Diagnostic, Severity};
27
28/// Apply `policy` to an assembled diagnostic list, returning the adjusted list.
29///
30/// Each diagnostic is matched against the policy's effective verb for its code
31/// and subject id (last-wins). Error-severity diagnostics are never dropped or
32/// weakened. With an empty policy this returns the input unchanged (identity
33/// pass), preserving the default-off byte-identical guarantee.
34pub fn apply_policy(diagnostics: Vec<Diagnostic>, policy: &DiagnosticPolicy) -> Vec<Diagnostic> {
35    // Fast path: an empty policy is an exact identity pass.
36    if policy.entries.is_empty() {
37        return diagnostics;
38    }
39
40    let mut out: Vec<Diagnostic> = Vec::with_capacity(diagnostics.len());
41    for mut diag in diagnostics {
42        match policy.verb_for(&diag.code, diag.subject_id.as_deref()) {
43            None => out.push(diag),
44            Some(verb) => match verb {
45                PolicyVerb::Allow => {
46                    // Drop Warning/Advisory; keep Error untouched (immutable).
47                    match diag.severity {
48                        Severity::Error => out.push(diag),
49                        Severity::Warning | Severity::Advisory => {
50                            // Suppressed: do not push.
51                        }
52                    }
53                }
54                PolicyVerb::Deny => {
55                    // Elevate Warning/Advisory to Error; already-Error stays.
56                    match diag.severity {
57                        Severity::Error => {}
58                        Severity::Warning | Severity::Advisory => {
59                            diag.severity = Severity::Error;
60                        }
61                    }
62                    out.push(diag);
63                }
64                PolicyVerb::Warn => {
65                    // Force Warning/Advisory to Warning; Error is immutable.
66                    match diag.severity {
67                        Severity::Error => {}
68                        Severity::Warning | Severity::Advisory => {
69                            diag.severity = Severity::Warning;
70                        }
71                    }
72                    out.push(diag);
73                }
74            },
75        }
76    }
77    out
78}
79
80/// Append diagnostics ABOUT the policy itself onto `diagnostics`.
81///
82/// - A code not present in the catalog → `policy.unknown_code` (Warning).
83/// - An `allow`/`warn` entry naming an always-Error catalog code →
84///   `policy.ineffective_on_error` (Warning), explaining that Errors cannot be
85///   weakened. A `deny` on an Error code is a silent no-op (it is already an
86///   Error), so it is NOT flagged.
87///
88/// Called AFTER [`apply_policy`] so these warnings cannot be suppressed by the
89/// very policy they describe.
90pub(super) fn check_policy_entries(policy: &DiagnosticPolicy, diagnostics: &mut Vec<Diagnostic>) {
91    for entry in &policy.entries {
92        match diag_catalog::lookup(&entry.code) {
93            None => {
94                diagnostics.push(Diagnostic::warning(
95                    "policy.unknown_code",
96                    format!(
97                        "diagnostics policy names '{}', which is not a diagnostic code this \
98                         engine emits; the entry has no effect",
99                        entry.code
100                    ),
101                    entry.source_span,
102                    Some(entry.code.clone()),
103                ));
104            }
105            Some(catalog_entry) => {
106                if !catalog_entry.is_governable() {
107                    // An always-Error code: only `allow`/`warn` are ineffective;
108                    // `deny` is a silent no-op (already Error).
109                    let verb_name = match entry.verb {
110                        PolicyVerb::Allow => "allow",
111                        PolicyVerb::Warn => "warn",
112                        // `deny` on an always-Error is a no-op (already Error) — do not flag.
113                        PolicyVerb::Deny => continue,
114                    };
115                    diagnostics.push(Diagnostic::warning(
116                        "policy.ineffective_on_error",
117                        format!(
118                            "diagnostics policy `{verb_name} \"{}\"` has no effect: '{}' is an \
119                             integrity Error and Error severity cannot be suppressed or \
120                             weakened",
121                            entry.code, entry.code
122                        ),
123                        entry.source_span,
124                        Some(entry.code.clone()),
125                    ));
126                }
127            }
128        }
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use crate::ast::policy::PolicyEntry;
136
137    fn policy(entries: Vec<(PolicyVerb, &str)>) -> DiagnosticPolicy {
138        DiagnosticPolicy {
139            entries: entries
140                .into_iter()
141                .map(|(verb, code)| PolicyEntry {
142                    verb,
143                    code: code.to_owned(),
144                    subjects: Vec::new(),
145                    source_span: None,
146                })
147                .collect(),
148        }
149    }
150
151    fn scoped_policy(entries: Vec<(PolicyVerb, &str, Vec<&str>)>) -> DiagnosticPolicy {
152        DiagnosticPolicy {
153            entries: entries
154                .into_iter()
155                .map(|(verb, code, subjects)| PolicyEntry {
156                    verb,
157                    code: code.to_owned(),
158                    subjects: subjects.into_iter().map(str::to_owned).collect(),
159                    source_span: None,
160                })
161                .collect(),
162        }
163    }
164
165    fn diag(code: &str, severity: Severity) -> Diagnostic {
166        Diagnostic::new(code, severity, "msg", None, None)
167    }
168
169    fn subject_diag(code: &str, severity: Severity, subject: &str) -> Diagnostic {
170        Diagnostic::new(code, severity, "msg", None, Some(subject.to_owned()))
171    }
172
173    #[test]
174    fn empty_policy_is_identity() {
175        let input = vec![diag("layout.off_canvas", Severity::Advisory)];
176        let out = apply_policy(input.clone(), &DiagnosticPolicy::default());
177        assert_eq!(out, input);
178    }
179
180    #[test]
181    fn allow_drops_advisory_but_not_error() {
182        let input = vec![
183            diag("layout.off_canvas", Severity::Advisory),
184            diag("id.duplicate", Severity::Error),
185        ];
186        let p = policy(vec![
187            (PolicyVerb::Allow, "layout.off_canvas"),
188            (PolicyVerb::Allow, "id.duplicate"),
189        ]);
190        let out = apply_policy(input, &p);
191        // Advisory dropped; Error survives unchanged.
192        assert_eq!(out.len(), 1);
193        assert_eq!(out[0].code, "id.duplicate");
194        assert_eq!(out[0].severity, Severity::Error);
195    }
196
197    #[test]
198    fn deny_elevates_to_error() {
199        let input = vec![diag("token.unused", Severity::Advisory)];
200        let p = policy(vec![(PolicyVerb::Deny, "token.unused")]);
201        let out = apply_policy(input, &p);
202        assert_eq!(out.len(), 1);
203        assert_eq!(out[0].severity, Severity::Error);
204    }
205
206    #[test]
207    fn warn_forces_warning_and_last_wins_over_deny() {
208        let input = vec![diag("node.unknown_property", Severity::Warning)];
209        // deny then warn → warn wins (last).
210        let p = policy(vec![
211            (PolicyVerb::Deny, "node.unknown_property"),
212            (PolicyVerb::Warn, "node.unknown_property"),
213        ]);
214        let out = apply_policy(input, &p);
215        assert_eq!(out[0].severity, Severity::Warning);
216    }
217
218    #[test]
219    fn scoped_allow_drops_only_matching_subject() {
220        let input = vec![
221            subject_diag("layout.off_canvas", Severity::Advisory, "bg.glow"),
222            subject_diag("layout.off_canvas", Severity::Advisory, "shape.1"),
223        ];
224        let p = scoped_policy(vec![(
225            PolicyVerb::Allow,
226            "layout.off_canvas",
227            vec!["bg.glow"],
228        )]);
229        let out = apply_policy(input, &p);
230        assert_eq!(out.len(), 1);
231        assert_eq!(out[0].subject_id.as_deref(), Some("shape.1"));
232    }
233
234    #[test]
235    fn unscoped_later_entry_overrides_scoped_entry_for_same_subject() {
236        let input = vec![subject_diag(
237            "layout.off_canvas",
238            Severity::Advisory,
239            "bg.glow",
240        )];
241        let p = DiagnosticPolicy {
242            entries: vec![
243                PolicyEntry {
244                    verb: PolicyVerb::Deny,
245                    code: "layout.off_canvas".to_owned(),
246                    subjects: vec!["bg.glow".to_owned()],
247                    source_span: None,
248                },
249                PolicyEntry {
250                    verb: PolicyVerb::Warn,
251                    code: "layout.off_canvas".to_owned(),
252                    subjects: Vec::new(),
253                    source_span: None,
254                },
255            ],
256        };
257        let out = apply_policy(input, &p);
258        assert_eq!(out[0].severity, Severity::Warning);
259    }
260
261    #[test]
262    fn unknown_code_is_flagged() {
263        let p = policy(vec![(PolicyVerb::Allow, "not.a_real_code")]);
264        let mut out = Vec::new();
265        check_policy_entries(&p, &mut out);
266        assert_eq!(out.len(), 1);
267        assert_eq!(out[0].code, "policy.unknown_code");
268    }
269
270    #[test]
271    fn allow_on_error_code_is_flagged_but_deny_is_silent() {
272        let p = policy(vec![
273            (PolicyVerb::Allow, "id.duplicate"),
274            (PolicyVerb::Deny, "id.duplicate"),
275        ]);
276        let mut out = Vec::new();
277        check_policy_entries(&p, &mut out);
278        // Only the `allow` entry is flagged; `deny` on an Error is a no-op.
279        assert_eq!(out.len(), 1);
280        assert_eq!(out[0].code, "policy.ineffective_on_error");
281    }
282}