cedar_policy_validator/cedar_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    iter::{Chain, Once},
21    sync::Arc,
22    vec,
23};
24
25use cedar_policy_core::{
26    ast::AnyId,
27    impl_diagnostic_from_source_loc_field, impl_diagnostic_from_two_source_loc_fields,
28    impl_diagnostic_from_two_source_loc_opt_fields,
29    parser::{
30        err::{expected_to_string, ExpectedTokenConfig},
31        unescape::UnescapeError,
32        Loc, Node,
33    },
34};
35use lalrpop_util as lalr;
36use lazy_static::lazy_static;
37use miette::{Diagnostic, LabeledSpan, SourceSpan};
38use nonempty::NonEmpty;
39use smol_str::{SmolStr, ToSmolStr};
40use thiserror::Error;
41
42use super::ast::PR;
43
44#[derive(Debug, Clone, PartialEq, Eq, Error)]
45pub enum UserError {
46    #[error("An empty list was passed")]
47    EmptyList(Node<()>),
48    #[error("Invalid escape codes")]
49    StringEscape(Node<NonEmpty<UnescapeError>>),
50    #[error("`{0}` is a reserved identifier")]
51    ReservedIdentifierUsed(Node<SmolStr>),
52    #[error("duplicate annotations: `{}`", .0)]
53    DuplicateAnnotations(AnyId, Node<()>, Node<()>),
54}
55
56impl UserError {
57    // Extract a primary source span locating the error.
58    pub(crate) fn primary_source_span(&self) -> SourceSpan {
59        match self {
60            Self::EmptyList(n) => n.loc.span,
61            Self::StringEscape(n) => n.loc.span,
62            Self::ReservedIdentifierUsed(n) => n.loc.span,
63            // use the first occurrence as the primary source span
64            Self::DuplicateAnnotations(_, n, _) => n.loc.span,
65        }
66    }
67}
68
69pub(crate) type RawLocation = usize;
70pub(crate) type RawToken<'a> = lalr::lexer::Token<'a>;
71
72pub(crate) type RawParseError<'a> = lalr::ParseError<RawLocation, RawToken<'a>, UserError>;
73pub(crate) type RawErrorRecovery<'a> = lalr::ErrorRecovery<RawLocation, RawToken<'a>, UserError>;
74
75type OwnedRawParseError = lalr::ParseError<RawLocation, String, UserError>;
76
77lazy_static! {
78    static ref SCHEMA_TOKEN_CONFIG: ExpectedTokenConfig = ExpectedTokenConfig {
79        friendly_token_names: HashMap::from([
80            ("IN", "`in`"),
81            ("PRINCIPAL", "`principal`"),
82            ("ACTION", "`action`"),
83            ("RESOURCE", "`resource`"),
84            ("CONTEXT", "`context`"),
85            ("STRINGLIT", "string literal"),
86            ("ENTITY", "`entity`"),
87            ("NAMESPACE", "`namespace`"),
88            ("TYPE", "`type`"),
89            ("SET", "`Set`"),
90            ("IDENTIFIER", "identifier"),
91            ("TAGS", "`tags`"),
92            ("ENUM", "`enum`"),
93        ]),
94        impossible_tokens: HashSet::new(),
95        special_identifier_tokens: HashSet::from([
96            "NAMESPACE",
97            "ENTITY",
98            "IN",
99            "TYPE",
100            "APPLIESTO",
101            "PRINCIPAL",
102            "ACTION",
103            "RESOURCE",
104            "CONTEXT",
105            "ATTRIBUTES",
106            "TAGS",
107            "LONG",
108            "STRING",
109            "BOOL",
110            "ENUM",
111        ]),
112        identifier_sentinel: "IDENTIFIER",
113        first_set_identifier_tokens: HashSet::from(["SET"]),
114        first_set_sentinel: "\"{\"",
115    };
116}
117
118/// For errors during parsing
119#[derive(Clone, Debug, PartialEq, Eq)]
120pub struct ParseError {
121    /// Error generated by lalrpop
122    pub(crate) err: OwnedRawParseError,
123    /// Source code
124    src: Arc<str>,
125}
126
127impl ParseError {
128    pub(crate) fn from_raw_parse_error(err: RawParseError<'_>, src: Arc<str>) -> Self {
129        Self {
130            err: err.map_token(|token| token.to_string()),
131            src,
132        }
133    }
134
135    pub(crate) fn from_raw_error_recovery(recovery: RawErrorRecovery<'_>, src: Arc<str>) -> Self {
136        Self::from_raw_parse_error(recovery.error, src)
137    }
138}
139
140impl ParseError {
141    /// Extract a primary source span locating the error.
142    pub fn primary_source_span(&self) -> SourceSpan {
143        match &self.err {
144            OwnedRawParseError::InvalidToken { location } => SourceSpan::from(*location),
145            OwnedRawParseError::UnrecognizedEof { location, .. } => SourceSpan::from(*location),
146            OwnedRawParseError::UnrecognizedToken {
147                token: (token_start, _, token_end),
148                ..
149            } => SourceSpan::from(*token_start..*token_end),
150            OwnedRawParseError::ExtraToken {
151                token: (token_start, _, token_end),
152            } => SourceSpan::from(*token_start..*token_end),
153            OwnedRawParseError::User { error } => error.primary_source_span(),
154        }
155    }
156}
157
158impl Display for ParseError {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        let Self { err, .. } = self;
161        match err {
162            OwnedRawParseError::InvalidToken { .. } => write!(f, "invalid token"),
163            OwnedRawParseError::UnrecognizedEof { .. } => write!(f, "unexpected end of input"),
164            OwnedRawParseError::UnrecognizedToken {
165                token: (_, token, _),
166                ..
167            } => write!(f, "unexpected token `{token}`"),
168            OwnedRawParseError::ExtraToken {
169                token: (_, token, _),
170                ..
171            } => write!(f, "extra token `{token}`"),
172            OwnedRawParseError::User { error } => write!(f, "{error}"),
173        }
174    }
175}
176
177impl std::error::Error for ParseError {}
178
179impl Diagnostic for ParseError {
180    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
181        Some(&self.src as &dyn miette::SourceCode)
182    }
183
184    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
185        let primary_source_span = self.primary_source_span();
186        match &self.err {
187            OwnedRawParseError::InvalidToken { .. } => Some(Box::new(std::iter::once(
188                LabeledSpan::underline(primary_source_span),
189            ))),
190            OwnedRawParseError::UnrecognizedEof { expected, .. } => {
191                Some(Box::new(std::iter::once(LabeledSpan::new_with_span(
192                    expected_to_string(expected, &SCHEMA_TOKEN_CONFIG),
193                    primary_source_span,
194                ))))
195            }
196            OwnedRawParseError::UnrecognizedToken { expected, .. } => {
197                Some(Box::new(std::iter::once(LabeledSpan::new_with_span(
198                    expected_to_string(expected, &SCHEMA_TOKEN_CONFIG),
199                    primary_source_span,
200                ))))
201            }
202            OwnedRawParseError::ExtraToken { .. } => Some(Box::new(std::iter::once(
203                LabeledSpan::underline(primary_source_span),
204            ))),
205            OwnedRawParseError::User {
206                error: UserError::DuplicateAnnotations(_, n1, n2),
207            } => Some(Box::new(
208                std::iter::once(n1.loc.span)
209                    .chain(std::iter::once(n2.loc.span))
210                    .map(LabeledSpan::underline),
211            )),
212            OwnedRawParseError::User { .. } => Some(Box::new(std::iter::once(
213                LabeledSpan::underline(primary_source_span),
214            ))),
215        }
216    }
217}
218
219/// Multiple parse errors.
220#[derive(Clone, Debug, PartialEq, Eq)]
221pub struct ParseErrors(pub(crate) Box<NonEmpty<ParseError>>);
222
223impl ParseErrors {
224    pub fn new(first: ParseError, tail: impl IntoIterator<Item = ParseError>) -> Self {
225        Self(Box::new(NonEmpty {
226            head: first,
227            tail: tail.into_iter().collect(),
228        }))
229    }
230
231    pub fn from_iter(i: impl IntoIterator<Item = ParseError>) -> Option<Self> {
232        let v = i.into_iter().collect::<Vec<_>>();
233        Some(Self(Box::new(NonEmpty::from_vec(v)?)))
234    }
235
236    /// Borrowed Iterator over reported errors
237    pub fn iter(&self) -> impl Iterator<Item = &ParseError> {
238        self.0.iter()
239    }
240}
241
242impl Display for ParseErrors {
243    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244        write!(f, "{}", self.0.first())
245    }
246}
247
248impl IntoIterator for ParseErrors {
249    type Item = ParseError;
250    type IntoIter = Chain<Once<ParseError>, vec::IntoIter<ParseError>>;
251
252    fn into_iter(self) -> Self::IntoIter {
253        self.0.into_iter()
254    }
255}
256
257impl std::error::Error for ParseErrors {
258    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
259        std::error::Error::source(self.0.first())
260    }
261}
262
263// Except for `.related()`, everything else is forwarded to the first error, if it is present.
264// This ensures that users who only use `Display`, `.code()`, `.labels()` etc, still get rich
265// information for the first error, even if they don't realize there are multiple errors here.
266// See cedar-policy/cedar#326.
267impl Diagnostic for ParseErrors {
268    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
269        // the .related() on the first error, and then the 2nd through Nth errors (but not their own .related())
270        let mut errs = self.iter().map(|err| err as &dyn Diagnostic);
271        errs.next().map(move |first_err| match first_err.related() {
272            Some(first_err_related) => Box::new(first_err_related.chain(errs)),
273            None => Box::new(errs) as Box<dyn Iterator<Item = _>>,
274        })
275    }
276
277    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
278        Diagnostic::code(self.0.first())
279    }
280
281    fn severity(&self) -> Option<miette::Severity> {
282        Diagnostic::severity(self.0.first())
283    }
284
285    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
286        Diagnostic::help(self.0.first())
287    }
288
289    fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
290        Diagnostic::url(self.0.first())
291    }
292
293    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
294        Diagnostic::source_code(self.0.first())
295    }
296
297    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
298        Diagnostic::labels(self.0.first())
299    }
300
301    fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
302        Diagnostic::diagnostic_source(self.0.first())
303    }
304}
305
306/// Non-empty collection of [`ToJsonSchemaError`]
307// WARNING: This type is publicly exported from [`cedar-policy`]
308#[derive(Debug, Clone, PartialEq, Eq)]
309pub struct ToJsonSchemaErrors(NonEmpty<ToJsonSchemaError>);
310
311impl ToJsonSchemaErrors {
312    /// Constructor. Guaranteed to have at least one error by construction.
313    pub fn new(errs: NonEmpty<ToJsonSchemaError>) -> Self {
314        Self(errs)
315    }
316
317    /// (Borrowed) iterator
318    pub fn iter(&self) -> impl Iterator<Item = &ToJsonSchemaError> {
319        self.0.iter()
320    }
321}
322
323impl IntoIterator for ToJsonSchemaErrors {
324    type Item = ToJsonSchemaError;
325    type IntoIter = <NonEmpty<ToJsonSchemaError> as IntoIterator>::IntoIter;
326
327    fn into_iter(self) -> Self::IntoIter {
328        self.0.into_iter()
329    }
330}
331
332impl From<ToJsonSchemaError> for ToJsonSchemaErrors {
333    fn from(value: ToJsonSchemaError) -> Self {
334        Self(NonEmpty::singleton(value))
335    }
336}
337
338impl Display for ToJsonSchemaErrors {
339    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
340        write!(f, "{}", self.0.first()) // intentionally showing only the first error; see #326 for discussion on a similar error type
341    }
342}
343
344impl std::error::Error for ToJsonSchemaErrors {
345    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
346        self.0.first().source()
347    }
348
349    #[allow(deprecated)]
350    fn description(&self) -> &str {
351        self.0.first().description()
352    }
353
354    #[allow(deprecated)]
355    fn cause(&self) -> Option<&dyn std::error::Error> {
356        self.0.first().cause()
357    }
358}
359
360// Except for `.related()`, everything else is forwarded to the first error, if it is present.
361// This ensures that users who only use `Display`, `.code()`, `.labels()` etc, still get rich
362// information for the first error, even if they don't realize there are multiple errors here.
363// See #326 for discussion on a similar error type.
364impl Diagnostic for ToJsonSchemaErrors {
365    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
366        // the .related() on the first error, and then the 2nd through Nth errors (but not their own .related())
367        let mut errs = self.iter().map(|err| err as &dyn Diagnostic);
368        errs.next().map(move |first_err| match first_err.related() {
369            Some(first_err_related) => Box::new(first_err_related.chain(errs)),
370            None => Box::new(errs) as Box<dyn Iterator<Item = _>>,
371        })
372    }
373
374    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
375        self.0.first().code()
376    }
377
378    fn severity(&self) -> Option<miette::Severity> {
379        self.0.first().severity()
380    }
381
382    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
383        self.0.first().help()
384    }
385
386    fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
387        self.0.first().url()
388    }
389
390    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
391        self.0.first().source_code()
392    }
393
394    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
395        self.0.first().labels()
396    }
397
398    fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
399        self.0.first().diagnostic_source()
400    }
401}
402
403// WARNING: This error type is publicly exported in `cedar-policy`, so it is part of the public interface
404/// For errors during schema format conversion
405#[derive(Clone, Debug, Error, PartialEq, Eq, Diagnostic)]
406pub enum ToJsonSchemaError {
407    /// Error raised when there are duplicate declarations
408    #[error(transparent)]
409    #[diagnostic(transparent)]
410    DuplicateDeclarations(#[from] DuplicateDeclarations),
411    /// Error raised when an action has multiple context declarations
412    #[error(transparent)]
413    #[diagnostic(transparent)]
414    DuplicateContext(#[from] DuplicateContext),
415    /// Error raised when a `principal` or `resource` is declared multiple times
416    #[error(transparent)]
417    #[diagnostic(transparent)]
418    DuplicatePrincipalOrResource(#[from] DuplicatePrincipalOrResource),
419    /// Error raised when an action does not define either `principal` or `resource`
420    #[error(transparent)]
421    #[diagnostic(transparent)]
422    NoPrincipalOrResource(#[from] NoPrincipalOrResource),
423    /// Error raised when there are duplicate namespace IDs
424    #[error(transparent)]
425    #[diagnostic(transparent)]
426    DuplicateNamespaces(#[from] DuplicateNamespace),
427    /// Error raised when a type name is unknown
428    #[error(transparent)]
429    #[diagnostic(transparent)]
430    UnknownTypeName(#[from] UnknownTypeName),
431    /// Invalid type name
432    #[error(transparent)]
433    #[diagnostic(transparent)]
434    ReservedName(#[from] ReservedName),
435    /// Use reserved schema keywords
436    #[error(transparent)]
437    #[diagnostic(transparent)]
438    ReservedSchemaKeyword(#[from] ReservedSchemaKeyword),
439}
440
441impl ToJsonSchemaError {
442    pub(crate) fn duplicate_context(name: &impl ToSmolStr, loc1: Loc, loc2: Loc) -> Self {
443        Self::DuplicateContext(DuplicateContext {
444            name: name.to_smolstr(),
445            loc1,
446            loc2,
447        })
448    }
449
450    pub(crate) fn duplicate_decls(decl: &impl ToSmolStr, loc1: Loc, loc2: Loc) -> Self {
451        Self::DuplicateDeclarations(DuplicateDeclarations {
452            decl: decl.to_smolstr(),
453            loc1,
454            loc2,
455        })
456    }
457
458    pub(crate) fn duplicate_namespace(
459        namespace_id: &impl ToSmolStr,
460        loc1: Option<Loc>,
461        loc2: Option<Loc>,
462    ) -> Self {
463        Self::DuplicateNamespaces(DuplicateNamespace {
464            namespace_id: namespace_id.to_smolstr(),
465            loc1,
466            loc2,
467        })
468    }
469
470    pub(crate) fn duplicate_principal(name: &impl ToSmolStr, loc1: Loc, loc2: Loc) -> Self {
471        Self::DuplicatePrincipalOrResource(DuplicatePrincipalOrResource {
472            name: name.to_smolstr(),
473            kind: PR::Principal,
474            loc1,
475            loc2,
476        })
477    }
478
479    pub(crate) fn duplicate_resource(name: &impl ToSmolStr, loc1: Loc, loc2: Loc) -> Self {
480        Self::DuplicatePrincipalOrResource(DuplicatePrincipalOrResource {
481            name: name.to_smolstr(),
482            kind: PR::Resource,
483            loc1,
484            loc2,
485        })
486    }
487
488    pub(crate) fn no_principal(name: &impl ToSmolStr, name_loc: Loc) -> Self {
489        Self::NoPrincipalOrResource(NoPrincipalOrResource {
490            kind: PR::Principal,
491            name: name.to_smolstr(),
492            missing_or_empty: MissingOrEmpty::Missing,
493            name_loc,
494        })
495    }
496
497    pub(crate) fn no_resource(name: &impl ToSmolStr, name_loc: Loc) -> Self {
498        Self::NoPrincipalOrResource(NoPrincipalOrResource {
499            kind: PR::Resource,
500            name: name.to_smolstr(),
501            missing_or_empty: MissingOrEmpty::Missing,
502            name_loc,
503        })
504    }
505
506    pub(crate) fn empty_principal(name: &impl ToSmolStr, name_loc: Loc, loc: Loc) -> Self {
507        Self::NoPrincipalOrResource(NoPrincipalOrResource {
508            kind: PR::Principal,
509            name: name.to_smolstr(),
510            missing_or_empty: MissingOrEmpty::Empty { loc },
511            name_loc,
512        })
513    }
514
515    pub(crate) fn empty_resource(name: &impl ToSmolStr, name_loc: Loc, loc: Loc) -> Self {
516        Self::NoPrincipalOrResource(NoPrincipalOrResource {
517            kind: PR::Resource,
518            name: name.to_smolstr(),
519            missing_or_empty: MissingOrEmpty::Empty { loc },
520            name_loc,
521        })
522    }
523
524    pub(crate) fn reserved_name(name: &impl ToSmolStr, loc: Loc) -> Self {
525        Self::ReservedName(ReservedName {
526            name: name.to_smolstr(),
527            loc,
528        })
529    }
530
531    pub(crate) fn reserved_keyword(keyword: &impl ToSmolStr, loc: Loc) -> Self {
532        Self::ReservedSchemaKeyword(ReservedSchemaKeyword {
533            keyword: keyword.to_smolstr(),
534            loc,
535        })
536    }
537}
538
539#[derive(Debug, Clone, PartialEq, Eq, Error)]
540#[error("this uses a reserved schema keyword: `{keyword}`")]
541pub struct ReservedSchemaKeyword {
542    keyword: SmolStr,
543    loc: Loc,
544}
545
546impl Diagnostic for ReservedSchemaKeyword {
547    impl_diagnostic_from_source_loc_field!(loc);
548
549    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
550        Some(Box::new("Keywords such as `entity`, `extension`, `set` and `record` cannot be used as common type names"))
551    }
552}
553
554#[derive(Debug, Clone, PartialEq, Eq, Error)]
555#[error("use of the reserved `__cedar` namespace")]
556pub struct ReservedName {
557    name: SmolStr,
558    loc: Loc,
559}
560
561impl Diagnostic for ReservedName {
562    impl_diagnostic_from_source_loc_field!(loc);
563
564    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
565        Some(Box::new(
566            "Names containing `__cedar` (for example: `__cedar::A`, `A::__cedar`, or `A::__cedar::B`) are reserved",
567        ))
568    }
569}
570
571#[derive(Debug, Clone, PartialEq, Eq, Error)]
572#[error("unknown type name: `{name}`")]
573pub struct UnknownTypeName {
574    name: SmolStr,
575    loc: Loc,
576}
577
578impl Diagnostic for UnknownTypeName {
579    impl_diagnostic_from_source_loc_field!(loc);
580
581    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
582        let msg = format!(
583            "Did you mean to define `{}` as an entity type or common type?",
584            self.name
585        );
586        Some(Box::new(msg))
587    }
588}
589
590#[derive(Debug, Clone, PartialEq, Eq, Error)]
591#[error("duplicate `{kind}` declaration in action `{name}`")]
592pub struct DuplicatePrincipalOrResource {
593    name: SmolStr,
594    kind: PR,
595    loc1: Loc,
596    loc2: Loc,
597}
598
599impl Diagnostic for DuplicatePrincipalOrResource {
600    impl_diagnostic_from_two_source_loc_fields!(loc1, loc2);
601
602    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
603        let msg = format!("Actions may only have a single {kind} declaration, but a {kind} declaration may specify a list of entity types like `{kind}: [X, Y, Z]`", kind=self.kind);
604        Some(Box::new(msg))
605    }
606}
607
608#[derive(Debug, Clone, PartialEq, Eq, Error)]
609#[error("duplicate context declaration in action `{name}`")]
610pub struct DuplicateContext {
611    name: SmolStr,
612    loc1: Loc,
613    loc2: Loc,
614}
615
616impl Diagnostic for DuplicateContext {
617    impl_diagnostic_from_two_source_loc_fields!(loc1, loc2);
618
619    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
620        Some(Box::new(
621            "Try either deleting one of the declarations, or merging into a single declaration",
622        ))
623    }
624}
625#[derive(Debug, Clone, PartialEq, Eq, Error)]
626#[error("`{decl}` is declared twice")]
627pub struct DuplicateDeclarations {
628    decl: SmolStr,
629    loc1: Loc,
630    loc2: Loc,
631}
632
633impl Diagnostic for DuplicateDeclarations {
634    impl_diagnostic_from_two_source_loc_fields!(loc1, loc2);
635}
636
637#[derive(Debug, Clone, PartialEq, Eq, Error)]
638#[error("{}", match .missing_or_empty {
639    MissingOrEmpty::Missing => format!("missing `{kind}` declaration for `{name}`"),
640    MissingOrEmpty::Empty { .. } => format!("for action `{name}`, `{kind}` is `[]`, which is invalid")
641})]
642pub struct NoPrincipalOrResource {
643    kind: PR,
644    name: SmolStr,
645    missing_or_empty: MissingOrEmpty,
646    /// Loc of the action name
647    name_loc: Loc,
648}
649
650#[derive(Debug, Clone, PartialEq, Eq)]
651enum MissingOrEmpty {
652    /// The declaration was entirely missing
653    Missing,
654    /// The declaration was present but defined as `[]`
655    Empty {
656        /// `Loc` of the declaration
657        loc: Loc,
658    },
659}
660
661pub const NO_PR_HELP_MSG: &str =
662    "Every action must define both `principal` and `resource` targets, and the `principal` and `resource` lists must not be `[]`.";
663
664impl Diagnostic for NoPrincipalOrResource {
665    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
666        Some(&self.name_loc.src as &dyn miette::SourceCode)
667    }
668
669    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
670        match &self.missing_or_empty {
671            MissingOrEmpty::Missing => {
672                // just the action name
673                Some(Box::new(std::iter::once(miette::LabeledSpan::underline(
674                    self.name_loc.span,
675                ))))
676            }
677            MissingOrEmpty::Empty { loc } => {
678                // also underline the bad declaration
679                let action_name = miette::LabeledSpan::new_with_span(
680                    Some("for this action".into()),
681                    self.name_loc.span,
682                );
683                let decl =
684                    miette::LabeledSpan::new_with_span(Some("must not be `[]`".into()), loc.span);
685                Some(Box::new([action_name, decl].into_iter()))
686            }
687        }
688    }
689
690    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
691        Some(Box::new(NO_PR_HELP_MSG))
692    }
693}
694
695#[derive(Debug, Clone, Error, PartialEq, Eq)]
696#[error("duplicate namespace id: `{namespace_id}`")]
697pub struct DuplicateNamespace {
698    namespace_id: SmolStr,
699    // `Loc`s are optional here as the implicit empty namespace has no location
700    loc1: Option<Loc>,
701    loc2: Option<Loc>,
702}
703
704impl Diagnostic for DuplicateNamespace {
705    impl_diagnostic_from_two_source_loc_opt_fields!(loc1, loc2);
706}
707
708/// Error subtypes for [`SchemaWarning`]
709pub mod schema_warnings {
710    use cedar_policy_core::{impl_diagnostic_from_source_loc_field, parser::Loc};
711    use miette::Diagnostic;
712    use smol_str::SmolStr;
713    use thiserror::Error;
714
715    /// Warning when a builtin Cedar name is shadowed
716    //
717    // CAUTION: this type is publicly exported in `cedar-policy`.
718    // Don't make fields `pub`, don't make breaking changes, and use caution
719    // when adding public methods.
720    #[derive(Eq, PartialEq, Debug, Clone, Error)]
721    #[error("The name `{name}` shadows a builtin Cedar name. You'll have to refer to the builtin as `__cedar::{name}`.")]
722    pub struct ShadowsBuiltinWarning {
723        pub(crate) name: SmolStr,
724        pub(crate) loc: Loc,
725    }
726
727    impl Diagnostic for ShadowsBuiltinWarning {
728        impl_diagnostic_from_source_loc_field!(loc);
729
730        fn severity(&self) -> Option<miette::Severity> {
731            Some(miette::Severity::Warning)
732        }
733    }
734
735    /// Warning when an entity name is shadowed by a common type name
736    //
737    // CAUTION: this type is publicly exported in `cedar-policy`.
738    // Don't make fields `pub`, don't make breaking changes, and use caution
739    // when adding public methods.
740    #[derive(Eq, PartialEq, Debug, Clone, Error)]
741    #[error("The common type name {name} shadows an entity name")]
742    pub struct ShadowsEntityWarning {
743        pub(crate) name: SmolStr,
744        pub(crate) entity_loc: Loc,
745        pub(crate) common_loc: Loc,
746    }
747
748    impl Diagnostic for ShadowsEntityWarning {
749        fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
750            Some(Box::new(
751                std::iter::once(&self.entity_loc)
752                    .chain(std::iter::once(&self.common_loc))
753                    .map(miette::LabeledSpan::underline),
754            ))
755        }
756
757        fn source_code(&self) -> Option<&dyn miette::SourceCode> {
758            // just have to pick one; we assume `entity_loc` and `common_loc`
759            // have the same source code.
760            // if that isn't true we'll have a confusing underline.
761            Some(&self.entity_loc.src as _)
762        }
763
764        fn severity(&self) -> Option<miette::Severity> {
765            Some(miette::Severity::Warning)
766        }
767    }
768}
769
770/// Warning when constructing a schema
771//
772// CAUTION: this type is publicly exported in `cedar-policy`.
773// Don't make fields `pub`, don't make breaking changes, and use caution
774// when adding public methods.
775#[derive(Eq, PartialEq, Debug, Clone, Error, Diagnostic)]
776#[non_exhaustive]
777pub enum SchemaWarning {
778    /// Warning when a declaration shadows a builtin type
779    #[error(transparent)]
780    #[diagnostic(transparent)]
781    ShadowsBuiltin(#[from] schema_warnings::ShadowsBuiltinWarning),
782    /// Warning when a declaration shadows an entity type
783    #[error(transparent)]
784    #[diagnostic(transparent)]
785    ShadowsEntity(#[from] schema_warnings::ShadowsEntityWarning),
786}