naga/
diagnostic_filter.rs

1//! [`DiagnosticFilter`]s and supporting functionality.
2
3#[cfg(feature = "wgsl-in")]
4use crate::Span;
5use crate::{Arena, Handle};
6#[cfg(feature = "arbitrary")]
7use arbitrary::Arbitrary;
8#[cfg(feature = "wgsl-in")]
9use indexmap::IndexMap;
10#[cfg(feature = "deserialize")]
11use serde::Deserialize;
12#[cfg(feature = "serialize")]
13use serde::Serialize;
14
15/// A severity set on a [`DiagnosticFilter`].
16///
17/// <https://www.w3.org/TR/WGSL/#diagnostic-severity>
18#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
19#[cfg_attr(feature = "serialize", derive(Serialize))]
20#[cfg_attr(feature = "deserialize", derive(Deserialize))]
21#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
22pub enum Severity {
23    Off,
24    Info,
25    Warning,
26    Error,
27}
28
29impl Severity {
30    /// Checks whether this severity is [`Self::Error`].
31    ///
32    /// Naga does not yet support diagnostic items at lesser severities than
33    /// [`Severity::Error`]. When this is implemented, this method should be deleted, and the
34    /// severity should be used directly for reporting diagnostics.
35    pub(crate) fn report_diag<E>(
36        self,
37        err: E,
38        log_handler: impl FnOnce(E, log::Level),
39    ) -> Result<(), E> {
40        let log_level = match self {
41            Severity::Off => return Ok(()),
42
43            // NOTE: These severities are not yet reported.
44            Severity::Info => log::Level::Info,
45            Severity::Warning => log::Level::Warn,
46
47            Severity::Error => return Err(err),
48        };
49        log_handler(err, log_level);
50        Ok(())
51    }
52}
53
54/// A filterable triggering rule in a [`DiagnosticFilter`].
55///
56/// <https://www.w3.org/TR/WGSL/#filterable-triggering-rules>
57#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
58#[cfg_attr(feature = "serialize", derive(Serialize))]
59#[cfg_attr(feature = "deserialize", derive(Deserialize))]
60#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
61pub enum FilterableTriggeringRule {
62    Standard(StandardFilterableTriggeringRule),
63    Unknown(Box<str>),
64    User(Box<[Box<str>; 2]>),
65}
66
67/// A filterable triggering rule in a [`DiagnosticFilter`].
68///
69/// <https://www.w3.org/TR/WGSL/#filterable-triggering-rules>
70#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
71#[cfg_attr(feature = "serialize", derive(Serialize))]
72#[cfg_attr(feature = "deserialize", derive(Deserialize))]
73#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
74pub enum StandardFilterableTriggeringRule {
75    DerivativeUniformity,
76}
77
78impl StandardFilterableTriggeringRule {
79    /// The default severity associated with this triggering rule.
80    ///
81    /// See <https://www.w3.org/TR/WGSL/#filterable-triggering-rules> for a table of default
82    /// severities.
83    pub(crate) const fn default_severity(self) -> Severity {
84        match self {
85            Self::DerivativeUniformity => Severity::Error,
86        }
87    }
88}
89
90/// A filtering rule that modifies how diagnostics are emitted for shaders.
91///
92/// <https://www.w3.org/TR/WGSL/#diagnostic-filter>
93#[derive(Clone, Debug)]
94#[cfg_attr(feature = "serialize", derive(Serialize))]
95#[cfg_attr(feature = "deserialize", derive(Deserialize))]
96#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
97pub struct DiagnosticFilter {
98    pub new_severity: Severity,
99    pub triggering_rule: FilterableTriggeringRule,
100}
101
102/// Determines whether [`DiagnosticFilterMap::add`] should consider full duplicates a conflict.
103///
104/// In WGSL, directive position does not consider this case a conflict, while attribute position
105/// does.
106#[cfg(feature = "wgsl-in")]
107pub(crate) enum ShouldConflictOnFullDuplicate {
108    /// Use this for attributes in WGSL.
109    Yes,
110    /// Use this for directives in WGSL.
111    No,
112}
113
114/// A map from diagnostic filters to their severity and span.
115///
116/// Front ends can use this to collect the set of filters applied to a
117/// particular language construct, and detect duplicate/conflicting filters.
118///
119/// For example, WGSL has global diagnostic filters that apply to the entire
120/// module, and diagnostic range filter attributes that apply to a specific
121/// function, statement, or other smaller construct. The set of filters applied
122/// to any given construct must not conflict, but they can be overridden by
123/// filters on other constructs nested within it. A front end can use a
124/// `DiagnosticFilterMap` to collect the filters applied to a single construct,
125/// using the [`add`] method's error checking to forbid conflicts.
126///
127/// For each filter it contains, a `DiagnosticFilterMap` records the requested
128/// severity, and the source span of the filter itself.
129///
130/// [`add`]: DiagnosticFilterMap::add
131#[derive(Clone, Debug, Default)]
132#[cfg(feature = "wgsl-in")]
133pub(crate) struct DiagnosticFilterMap(IndexMap<FilterableTriggeringRule, (Severity, Span)>);
134
135#[cfg(feature = "wgsl-in")]
136impl DiagnosticFilterMap {
137    pub(crate) fn new() -> Self {
138        Self::default()
139    }
140
141    /// Add the given `diagnostic_filter` parsed at the given `span` to this map.
142    pub(crate) fn add(
143        &mut self,
144        diagnostic_filter: DiagnosticFilter,
145        span: Span,
146        should_conflict_on_full_duplicate: ShouldConflictOnFullDuplicate,
147    ) -> Result<(), ConflictingDiagnosticRuleError> {
148        use indexmap::map::Entry;
149
150        let &mut Self(ref mut diagnostic_filters) = self;
151        let DiagnosticFilter {
152            new_severity,
153            triggering_rule,
154        } = diagnostic_filter;
155
156        match diagnostic_filters.entry(triggering_rule.clone()) {
157            Entry::Vacant(entry) => {
158                entry.insert((new_severity, span));
159            }
160            Entry::Occupied(entry) => {
161                let &(first_severity, first_span) = entry.get();
162                let should_conflict_on_full_duplicate = match should_conflict_on_full_duplicate {
163                    ShouldConflictOnFullDuplicate::Yes => true,
164                    ShouldConflictOnFullDuplicate::No => false,
165                };
166                if first_severity != new_severity || should_conflict_on_full_duplicate {
167                    return Err(ConflictingDiagnosticRuleError {
168                        triggering_rule_spans: [first_span, span],
169                    });
170                }
171            }
172        }
173        Ok(())
174    }
175
176    /// Were any rules specified?
177    pub(crate) fn is_empty(&self) -> bool {
178        let &Self(ref map) = self;
179        map.is_empty()
180    }
181
182    /// Returns the spans of all contained rules.
183    pub(crate) fn spans(&self) -> impl Iterator<Item = Span> + '_ {
184        let &Self(ref map) = self;
185        map.iter().map(|(_, &(_, span))| span)
186    }
187}
188
189#[cfg(feature = "wgsl-in")]
190impl IntoIterator for DiagnosticFilterMap {
191    type Item = (FilterableTriggeringRule, (Severity, Span));
192
193    type IntoIter = indexmap::map::IntoIter<FilterableTriggeringRule, (Severity, Span)>;
194
195    fn into_iter(self) -> Self::IntoIter {
196        let Self(this) = self;
197        this.into_iter()
198    }
199}
200
201/// An error returned by [`DiagnosticFilterMap::add`] when it encounters conflicting rules.
202#[cfg(feature = "wgsl-in")]
203#[derive(Clone, Debug)]
204pub(crate) struct ConflictingDiagnosticRuleError {
205    pub triggering_rule_spans: [Span; 2],
206}
207
208/// Represents a single parent-linking node in a tree of [`DiagnosticFilter`]s backed by a
209/// [`crate::Arena`].
210///
211/// A single element of a _tree_ of diagnostic filter rules stored in
212/// [`crate::Module::diagnostic_filters`]. When nodes are built by a front-end, module-applicable
213/// filter rules are chained together in runs based on parse site.  For instance, given the
214/// following:
215///
216/// - Module-applicable rules `a` and `b`.
217/// - Rules `c` and `d`, applicable to an entry point called `c_and_d_func`.
218/// - Rule `e`, applicable to an entry point called `e_func`.
219///
220/// The tree would be represented as follows:
221///
222/// ```text
223/// a <- b
224///      ^
225///      |- c <- d
226///      |
227///      \- e
228/// ```
229///
230/// ...where:
231///
232/// - `d` is the first leaf consulted by validation in `c_and_d_func`.
233/// - `e` is the first leaf consulted by validation in `e_func`.
234#[derive(Clone, Debug)]
235#[cfg_attr(feature = "serialize", derive(Serialize))]
236#[cfg_attr(feature = "deserialize", derive(Deserialize))]
237#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
238pub struct DiagnosticFilterNode {
239    pub inner: DiagnosticFilter,
240    pub parent: Option<Handle<DiagnosticFilterNode>>,
241}
242
243impl DiagnosticFilterNode {
244    /// Finds the most specific filter rule applicable to `triggering_rule` from the chain of
245    /// diagnostic filter rules in `arena`, starting with `node`, and returns its severity. If none
246    /// is found, return the value of [`StandardFilterableTriggeringRule::default_severity`].
247    ///
248    /// When `triggering_rule` is not applicable to this node, its parent is consulted recursively.
249    pub(crate) fn search(
250        node: Option<Handle<Self>>,
251        arena: &Arena<Self>,
252        triggering_rule: StandardFilterableTriggeringRule,
253    ) -> Severity {
254        let mut next = node;
255        while let Some(handle) = next {
256            let node = &arena[handle];
257            let &Self { ref inner, parent } = node;
258            let &DiagnosticFilter {
259                triggering_rule: ref rule,
260                new_severity,
261            } = inner;
262
263            if rule == &FilterableTriggeringRule::Standard(triggering_rule) {
264                return new_severity;
265            }
266
267            next = parent;
268        }
269        triggering_rule.default_severity()
270    }
271}