Skip to main content

cedar_policy_validator/human_schema/
err.rs

1/*
2 * Copyright Cedar Contributors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17use std::{
18    collections::{HashMap, HashSet},
19    fmt::Display,
20};
21
22use cedar_policy_core::parser::{
23    err::{expected_to_string, ExpectedTokenConfig},
24    unescape::UnescapeError,
25    Loc, Node,
26};
27use lalrpop_util as lalr;
28use lazy_static::lazy_static;
29use miette::{Diagnostic, LabeledSpan, SourceSpan};
30use nonempty::NonEmpty;
31use smol_str::SmolStr;
32use thiserror::Error;
33
34use super::ast::PR;
35
36#[derive(Debug, Clone, PartialEq, Eq, Error)]
37pub enum UserError {
38    #[error("An empty list was passed")]
39    EmptyList,
40    #[error("Invalid escape codes")]
41    StringEscape(NonEmpty<UnescapeError>),
42    #[error("`{0}` is a reserved identifier")]
43    ReservedIdentifierUsed(SmolStr),
44}
45
46pub(crate) type RawLocation = usize;
47pub(crate) type RawToken<'a> = lalr::lexer::Token<'a>;
48pub(crate) type RawUserError = Node<UserError>;
49
50pub(crate) type RawParseError<'a> = lalr::ParseError<RawLocation, RawToken<'a>, RawUserError>;
51pub(crate) type RawErrorRecovery<'a> = lalr::ErrorRecovery<RawLocation, RawToken<'a>, RawUserError>;
52
53type OwnedRawParseError = lalr::ParseError<RawLocation, String, RawUserError>;
54
55lazy_static! {
56    static ref SCHEMA_TOKEN_CONFIG: ExpectedTokenConfig = ExpectedTokenConfig {
57        friendly_token_names: HashMap::from([
58            ("IN", "`in`"),
59            ("PRINCIPAL", "`principal`"),
60            ("ACTION", "`action`"),
61            ("RESOURCE", "`resource`"),
62            ("CONTEXT", "`context`"),
63            ("STRINGLIT", "string literal"),
64            ("ENTITY", "`entity`"),
65            ("NAMESPACE", "`namespace`"),
66            ("TYPE", "`type`"),
67            ("SET", "`Set`"),
68            ("IDENTIFIER", "identifier"),
69        ]),
70        impossible_tokens: HashSet::new(),
71        special_identifier_tokens: HashSet::from([
72            "NAMESPACE",
73            "ENTITY",
74            "IN",
75            "TYPE",
76            "APPLIESTO",
77            "PRINCIPAL",
78            "ACTION",
79            "RESOURCE",
80            "CONTEXT",
81            "ATTRIBUTES",
82            "LONG",
83            "STRING",
84            "BOOL",
85        ]),
86        identifier_sentinel: "IDENTIFIER",
87        first_set_identifier_tokens: HashSet::from(["SET"]),
88        first_set_sentinel: "\"{\"",
89    };
90}
91
92/// For errors during parsing
93#[derive(Clone, Debug, PartialEq, Eq)]
94pub struct ParseError {
95    /// Error generated by lalrpop
96    err: OwnedRawParseError,
97}
98
99impl From<RawParseError<'_>> for ParseError {
100    fn from(err: RawParseError<'_>) -> Self {
101        Self {
102            err: err.map_token(|token| token.to_string()),
103        }
104    }
105}
106
107impl From<RawErrorRecovery<'_>> for ParseError {
108    fn from(recovery: RawErrorRecovery<'_>) -> Self {
109        recovery.error.into()
110    }
111}
112
113impl ParseError {
114    /// Extract a primary source span locating the error.
115    pub fn primary_source_span(&self) -> SourceSpan {
116        let Self { err } = self;
117        match err {
118            OwnedRawParseError::InvalidToken { location } => SourceSpan::from(*location),
119            OwnedRawParseError::UnrecognizedEof { location, .. } => SourceSpan::from(*location),
120            OwnedRawParseError::UnrecognizedToken {
121                token: (token_start, _, token_end),
122                ..
123            } => SourceSpan::from(*token_start..*token_end),
124            OwnedRawParseError::ExtraToken {
125                token: (token_start, _, token_end),
126            } => SourceSpan::from(*token_start..*token_end),
127            OwnedRawParseError::User { error } => error.loc.span,
128        }
129    }
130}
131
132impl Display for ParseError {
133    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134        let Self { err } = self;
135        match err {
136            OwnedRawParseError::InvalidToken { .. } => write!(f, "invalid token"),
137            OwnedRawParseError::UnrecognizedEof { .. } => write!(f, "unexpected end of input"),
138            OwnedRawParseError::UnrecognizedToken {
139                token: (_, token, _),
140                ..
141            } => write!(f, "unexpected token `{token}`"),
142            OwnedRawParseError::ExtraToken {
143                token: (_, token, _),
144                ..
145            } => write!(f, "extra token `{token}`"),
146            OwnedRawParseError::User {
147                error: Node { node, .. },
148            } => write!(f, "{node}"),
149        }
150    }
151}
152
153impl std::error::Error for ParseError {}
154
155impl Diagnostic for ParseError {
156    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
157        let primary_source_span = self.primary_source_span();
158        let Self { err } = self;
159        let labeled_span = match err {
160            OwnedRawParseError::InvalidToken { .. } => LabeledSpan::underline(primary_source_span),
161            OwnedRawParseError::UnrecognizedEof { expected, .. } => LabeledSpan::new_with_span(
162                expected_to_string(expected, &SCHEMA_TOKEN_CONFIG),
163                primary_source_span,
164            ),
165            OwnedRawParseError::UnrecognizedToken { expected, .. } => LabeledSpan::new_with_span(
166                expected_to_string(expected, &SCHEMA_TOKEN_CONFIG),
167                primary_source_span,
168            ),
169            OwnedRawParseError::ExtraToken { .. } => LabeledSpan::underline(primary_source_span),
170            OwnedRawParseError::User { .. } => LabeledSpan::underline(primary_source_span),
171        };
172        Some(Box::new(std::iter::once(labeled_span)))
173    }
174}
175
176/// Multiple parse errors.
177#[derive(Clone, Debug, PartialEq, Eq)]
178pub struct ParseErrors(Box<NonEmpty<ParseError>>);
179
180impl ParseErrors {
181    pub fn new(first: ParseError, rest: impl IntoIterator<Item = ParseError>) -> Self {
182        let mut nv = NonEmpty::singleton(first);
183        let mut v = rest.into_iter().collect::<Vec<_>>();
184        nv.append(&mut v);
185        Self(Box::new(nv))
186    }
187
188    pub fn from_iter(i: impl IntoIterator<Item = ParseError>) -> Option<Self> {
189        let v = i.into_iter().collect::<Vec<_>>();
190        Some(Self(Box::new(NonEmpty::from_vec(v)?)))
191    }
192
193    pub fn iter(&self) -> impl Iterator<Item = &ParseError> {
194        self.0.iter()
195    }
196}
197
198impl Display for ParseErrors {
199    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200        write!(f, "{}", self.0.first())
201    }
202}
203
204impl std::error::Error for ParseErrors {
205    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
206        std::error::Error::source(self.0.first())
207    }
208}
209
210// Except for `.related()`, everything else is forwarded to the first error, if it is present.
211// This ensures that users who only use `Display`, `.code()`, `.labels()` etc, still get rich
212// information for the first error, even if they don't realize there are multiple errors here.
213// See cedar-policy/cedar#326.
214impl Diagnostic for ParseErrors {
215    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
216        // the .related() on the first error, and then the 2nd through Nth errors (but not their own .related())
217        let mut errs = self.iter().map(|err| err as &dyn Diagnostic);
218        errs.next().map(move |first_err| match first_err.related() {
219            Some(first_err_related) => Box::new(first_err_related.chain(errs)),
220            None => Box::new(errs) as Box<dyn Iterator<Item = _>>,
221        })
222    }
223
224    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
225        Diagnostic::code(self.0.first())
226    }
227
228    fn severity(&self) -> Option<miette::Severity> {
229        Diagnostic::severity(self.0.first())
230    }
231
232    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
233        Diagnostic::help(self.0.first())
234    }
235
236    fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
237        Diagnostic::url(self.0.first())
238    }
239
240    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
241        Diagnostic::source_code(self.0.first())
242    }
243
244    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
245        Diagnostic::labels(self.0.first())
246    }
247
248    fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
249        Diagnostic::diagnostic_source(self.0.first())
250    }
251}
252
253#[derive(Debug, Clone, PartialEq, Eq)]
254pub struct ToJsonSchemaErrors(NonEmpty<ToJsonSchemaError>);
255
256impl ToJsonSchemaErrors {
257    pub fn new(errs: NonEmpty<ToJsonSchemaError>) -> Self {
258        Self(errs)
259    }
260
261    pub fn iter(&self) -> impl Iterator<Item = &ToJsonSchemaError> {
262        self.0.iter()
263    }
264}
265
266impl IntoIterator for ToJsonSchemaErrors {
267    type Item = ToJsonSchemaError;
268    type IntoIter = <NonEmpty<ToJsonSchemaError> as IntoIterator>::IntoIter;
269
270    fn into_iter(self) -> Self::IntoIter {
271        self.0.into_iter()
272    }
273}
274
275impl From<ToJsonSchemaError> for ToJsonSchemaErrors {
276    fn from(value: ToJsonSchemaError) -> Self {
277        Self(NonEmpty::singleton(value))
278    }
279}
280
281impl Display for ToJsonSchemaErrors {
282    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283        write!(f, "{}", self.0.first()) // intentionally showing only the first error; see #326 for discussion on a similar error type
284    }
285}
286
287impl std::error::Error for ToJsonSchemaErrors {
288    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
289        self.0.first().source()
290    }
291
292    #[allow(deprecated)]
293    fn description(&self) -> &str {
294        self.0.first().description()
295    }
296
297    #[allow(deprecated)]
298    fn cause(&self) -> Option<&dyn std::error::Error> {
299        self.0.first().cause()
300    }
301}
302
303// Except for `.related()`, everything else is forwarded to the first error, if it is present.
304// This ensures that users who only use `Display`, `.code()`, `.labels()` etc, still get rich
305// information for the first error, even if they don't realize there are multiple errors here.
306// See #326 for discussion on a similar error type.
307impl Diagnostic for ToJsonSchemaErrors {
308    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
309        // the .related() on the first error, and then the 2nd through Nth errors (but not their own .related())
310        let mut errs = self.iter().map(|err| err as &dyn Diagnostic);
311        errs.next().map(move |first_err| match first_err.related() {
312            Some(first_err_related) => Box::new(first_err_related.chain(errs)),
313            None => Box::new(errs) as Box<dyn Iterator<Item = _>>,
314        })
315    }
316
317    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
318        self.0.first().code()
319    }
320
321    fn severity(&self) -> Option<miette::Severity> {
322        self.0.first().severity()
323    }
324
325    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
326        self.0.first().help()
327    }
328
329    fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
330        self.0.first().url()
331    }
332
333    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
334        self.0.first().source_code()
335    }
336
337    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
338        self.0.first().labels()
339    }
340
341    fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
342        self.0.first().diagnostic_source()
343    }
344}
345
346/// For errors during schema format conversion
347#[derive(Clone, Debug, Error, PartialEq, Eq)]
348pub enum ToJsonSchemaError {
349    /// Error raised when there are duplicate keys
350    #[error("Duplicate keys: `{key}`")]
351    DuplicateKeys { key: SmolStr, loc1: Loc, loc2: Loc },
352    /// Error raised when there are duplicate declarations
353    #[error("Duplicate declarations: `{decl}`")]
354    DuplicateDeclarations { decl: SmolStr, loc1: Loc, loc2: Loc },
355    #[error("Duplicate context declaration. Action may have at most one context declaration")]
356    DuplicateContext { loc1: Loc, loc2: Loc },
357    #[error("Duplicate {kind} decleration. Action may have at most once {kind} declaration")]
358    DuplicatePR { kind: PR, loc1: Loc, loc2: Loc },
359
360    /// Error raised when there are duplicate namespace IDs
361    #[error("Duplicate namespace IDs: `{namespace_id}`")]
362    DuplicateNameSpaces {
363        namespace_id: SmolStr,
364        loc1: Option<Loc>,
365        loc2: Option<Loc>,
366    },
367    /// Invalid type name
368    #[error("Unknown type name: `{}`", .0.node)]
369    UnknownTypeName(Node<SmolStr>),
370    #[error("Use reserved namespace `__cedar`")]
371    UseReservedNamespace(Loc),
372}
373
374impl ToJsonSchemaError {
375    pub fn duplicate_keys(key: SmolStr, loc1: Loc, loc2: Loc) -> Self {
376        Self::DuplicateKeys { key, loc1, loc2 }
377    }
378    pub fn duplicate_decls(decl: SmolStr, loc1: Loc, loc2: Loc) -> Self {
379        Self::DuplicateDeclarations { decl, loc1, loc2 }
380    }
381    pub fn duplicate_namespace(namespace_id: SmolStr, loc1: Loc, loc2: Loc) -> Self {
382        Self::DuplicateNameSpaces {
383            namespace_id,
384            loc1: Some(loc1),
385            loc2: Some(loc2),
386        }
387    }
388}
389
390impl Diagnostic for ToJsonSchemaError {
391    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
392        match self {
393            ToJsonSchemaError::DuplicateDeclarations { loc1, loc2, .. }
394            | ToJsonSchemaError::DuplicateContext { loc1, loc2 }
395            | ToJsonSchemaError::DuplicatePR { loc1, loc2, .. }
396            | ToJsonSchemaError::DuplicateKeys { loc1, loc2, .. } => Some(Box::new(
397                vec![
398                    LabeledSpan::underline(loc1.span),
399                    LabeledSpan::underline(loc2.span),
400                ]
401                .into_iter(),
402            )),
403            ToJsonSchemaError::DuplicateNameSpaces { loc1, loc2, .. } => {
404                Some(Box::new([loc1, loc2].into_iter().filter_map(|loc| {
405                    Some(LabeledSpan::underline(loc.as_ref()?.span))
406                })))
407            }
408            ToJsonSchemaError::UnknownTypeName(node) => Some(Box::new(std::iter::once(
409                LabeledSpan::underline(node.loc.span),
410            ))),
411            ToJsonSchemaError::UseReservedNamespace(loc) => {
412                Some(Box::new(std::iter::once(LabeledSpan::underline(loc.span))))
413            }
414        }
415    }
416}
417
418#[derive(Debug, Clone, Error, Diagnostic)]
419#[diagnostic(severity(warning))]
420pub enum SchemaWarning {
421    #[error("The name `{name}` shadows a builtin Cedar name. You'll have to refer to the builtin as `__cedar::{name}`.")]
422    ShadowsBuiltin { name: SmolStr, loc: Loc },
423    #[error("The common type name {name} shadows an entity name")]
424    ShadowsEntity {
425        name: SmolStr,
426        entity_loc: Loc,
427        common_loc: Loc,
428    },
429    #[error("The namespace {name} uses a name that will be reserved in the future. All namespaces beginning with `__` will be reserved in a future version.")]
430    UsesBuiltinNamespace { name: SmolStr, loc: Loc },
431}