cedar_policy_core/parser/
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::collections::{BTreeSet, HashMap, HashSet};
18use std::fmt::{self, Display, Write};
19use std::iter;
20use std::ops::{Deref, DerefMut};
21
22use either::Either;
23use lalrpop_util as lalr;
24use lazy_static::lazy_static;
25use miette::{Diagnostic, LabeledSpan, SourceSpan};
26use nonempty::NonEmpty;
27use smol_str::SmolStr;
28use thiserror::Error;
29
30use crate::ast::{self, ReservedNameError};
31use crate::parser::fmt::join_with_conjunction;
32use crate::parser::loc::Loc;
33use crate::parser::node::Node;
34use crate::parser::unescape::UnescapeError;
35
36use super::cst;
37
38pub(crate) type RawLocation = usize;
39pub(crate) type RawToken<'a> = lalr::lexer::Token<'a>;
40pub(crate) type RawUserError = Node<String>;
41
42pub(crate) type RawParseError<'a> = lalr::ParseError<RawLocation, RawToken<'a>, RawUserError>;
43pub(crate) type RawErrorRecovery<'a> = lalr::ErrorRecovery<RawLocation, RawToken<'a>, RawUserError>;
44
45type OwnedRawParseError = lalr::ParseError<RawLocation, String, RawUserError>;
46
47/// Errors that can occur when parsing Cedar policies or expressions.
48#[derive(Clone, Debug, Diagnostic, Error, PartialEq, Eq)]
49pub enum ParseError {
50    /// Error from the text -> CST parser
51    #[error(transparent)]
52    #[diagnostic(transparent)]
53    ToCST(#[from] ToCSTError),
54    /// Error from the CST -> AST transform
55    #[error(transparent)]
56    #[diagnostic(transparent)]
57    ToAST(#[from] ToASTError),
58}
59
60/// Errors possible from `Literal::from_str()`
61#[derive(Debug, Clone, PartialEq, Diagnostic, Error, Eq)]
62pub enum LiteralParseError {
63    /// Failed to parse the input
64    #[error(transparent)]
65    #[diagnostic(transparent)]
66    Parse(#[from] ParseErrors),
67    /// Parsed successfully as an expression, but failed to construct a literal
68    #[error("invalid literal: {0}")]
69    InvalidLiteral(ast::Expr),
70}
71
72/// Error from the CST -> AST transform
73#[derive(Debug, Error, Clone, PartialEq, Eq)]
74#[error("{kind}")]
75pub struct ToASTError {
76    kind: ToASTErrorKind,
77    loc: Loc,
78}
79
80// Construct `labels` and `source_code` based on the `loc` in this
81// struct; and everything else forwarded directly to `kind`.
82impl Diagnostic for ToASTError {
83    impl_diagnostic_from_source_loc_field!(loc);
84
85    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
86        self.kind.code()
87    }
88
89    fn severity(&self) -> Option<miette::Severity> {
90        self.kind.severity()
91    }
92
93    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
94        self.kind.help()
95    }
96
97    fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
98        self.kind.url()
99    }
100
101    fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
102        self.kind.diagnostic_source()
103    }
104}
105
106impl ToASTError {
107    /// Construct a new `ToASTError`.
108    pub fn new(kind: ToASTErrorKind, loc: Loc) -> Self {
109        Self { kind, loc }
110    }
111
112    /// Get the error kind.
113    pub fn kind(&self) -> &ToASTErrorKind {
114        &self.kind
115    }
116
117    pub(crate) fn source_loc(&self) -> &Loc {
118        &self.loc
119    }
120}
121
122const POLICY_SCOPE_HELP: &str =
123    "policy scopes must contain a `principal`, `action`, and `resource` element in that order";
124
125/// Details about a particular kind of `ToASTError`.
126//
127// This is NOT a publicly exported error type.
128#[derive(Debug, Diagnostic, Error, Clone, PartialEq, Eq)]
129#[non_exhaustive]
130pub enum ToASTErrorKind {
131    /// Returned when we attempt to parse a template with a conflicting id
132    #[error("a template with id `{0}` already exists in the policy set")]
133    DuplicateTemplateId(ast::PolicyID),
134    /// Returned when we attempt to parse a policy with a conflicting id
135    #[error("a policy with id `{0}` already exists in the policy set")]
136    DuplicatePolicyId(ast::PolicyID),
137    /// Returned when a template is encountered but a static policy is expected
138    #[error(transparent)]
139    #[diagnostic(transparent)]
140    ExpectedStaticPolicy(#[from] parse_errors::ExpectedStaticPolicy),
141    /// Returned when a static policy is encountered but a template is expected
142    #[error(transparent)]
143    #[diagnostic(transparent)]
144    ExpectedTemplate(#[from] parse_errors::ExpectedTemplate),
145    /// Returned when we attempt to parse a policy or template with duplicate or
146    /// conflicting annotations
147    #[error("duplicate annotation: @{0}")]
148    DuplicateAnnotation(ast::AnyId),
149    /// Returned when a policy contains template slots in a when/unless clause.
150    /// This is not currently supported; see [RFC 3](https://github.com/cedar-policy/rfcs/pull/3).
151    #[error(transparent)]
152    #[diagnostic(transparent)]
153    SlotsInConditionClause(#[from] parse_errors::SlotsInConditionClause),
154    /// Returned when a policy is missing one of the three required scope elements
155    /// (`principal`, `action`, and `resource`)
156    #[error("this policy is missing the `{0}` variable in the scope")]
157    #[diagnostic(help("{POLICY_SCOPE_HELP}"))]
158    MissingScopeVariable(ast::Var),
159    /// Returned when a policy has an extra scope element
160    #[error("this policy has an extra element in the scope: {0}")]
161    #[diagnostic(help("{POLICY_SCOPE_HELP}"))]
162    ExtraScopeElement(cst::VariableDef),
163    /// Returned when a policy uses a reserved keyword as an identifier.
164    #[error("this identifier is reserved and cannot be used: {0}")]
165    ReservedIdentifier(cst::Ident),
166    /// Returned when a policy contains an invalid identifier.
167    /// This error is not currently returned, but is here for future-proofing;
168    /// see [`cst::Ident::Invalid`].
169    #[error("invalid identifier: {0}")]
170    InvalidIdentifier(String),
171    /// Returned when a policy uses '=' as a binary operator.
172    /// '=' is not an operator in Cedar; we can suggest '==' instead.
173    #[error("'=' is not a valid operator in Cedar")]
174    #[diagnostic(help("try using '==' instead"))]
175    InvalidSingleEq,
176    /// Returned when a policy uses an effect keyword beyond `permit` or `forbid`
177    #[error("invalid policy effect: {0}")]
178    #[diagnostic(help("effect must be either `permit` or `forbid`"))]
179    InvalidEffect(cst::Ident),
180    /// Returned when a policy uses a condition keyword beyond `when` or `unless`
181    #[error("invalid policy condition: {0}")]
182    #[diagnostic(help("condition must be either `when` or `unless`"))]
183    InvalidCondition(cst::Ident),
184    /// Returned when a policy uses a variable in the scope beyond `principal`,
185    /// `action`, or `resource`
186    #[error("found an invalid variable in the policy scope: {0}")]
187    #[diagnostic(help("{POLICY_SCOPE_HELP}"))]
188    InvalidScopeVariable(cst::Ident),
189    /// Returned when a policy scope clause contains the wrong variable.
190    /// (`principal` must be in the first clause, etc...)
191    #[error("found the variable `{got}` where the variable `{expected}` must be used")]
192    #[diagnostic(help("{POLICY_SCOPE_HELP}"))]
193    IncorrectVariable {
194        /// The variable that is expected
195        expected: ast::Var,
196        /// The variable that was present
197        got: ast::Var,
198    },
199    /// Returned when a policy scope uses an operator not allowed in scopes
200    #[error("invalid operator in the policy scope: {0}")]
201    #[diagnostic(help("policy scope clauses can only use `==`, `in`, `is`, or `_ is _ in _`"))]
202    InvalidScopeOperator(cst::RelOp),
203    /// Returned when an action scope uses an operator not allowed in action scopes
204    /// (special case of `InvalidScopeOperator`)
205    #[error("invalid operator in the action scope: {0}")]
206    #[diagnostic(help("action scope clauses can only use `==` or `in`"))]
207    InvalidActionScopeOperator(cst::RelOp),
208    /// Returned when the action scope clause contains an `is`
209    #[error("`is` cannot appear in the action scope")]
210    #[diagnostic(help("try moving `action is ..` into a `when` condition"))]
211    IsInActionScope,
212    /// Returned when an `is` operator is used together with `==`
213    #[error("`is` cannot be used together with `==`")]
214    #[diagnostic(help("try using `_ is _ in _`"))]
215    IsWithEq,
216    /// Returned when an entity uid used as an action does not have the type `Action`
217    #[error(transparent)]
218    #[diagnostic(transparent)]
219    InvalidActionType(#[from] parse_errors::InvalidActionType),
220    /// Returned when a condition clause is empty
221    #[error("{}condition clause cannot be empty", match .0 { Some(ident) => format!("`{}` ", ident), None => "".to_string() })]
222    EmptyClause(Option<cst::Ident>),
223    /// Returned when membership chains do not resolve to an expression,
224    /// violating an internal invariant
225    #[error("internal invariant violated. Membership chain did not resolve to an expression")]
226    #[diagnostic(help("please file an issue at <https://github.com/cedar-policy/cedar/issues> including the text that failed to parse"))]
227    MembershipInvariantViolation,
228    /// Returned for a non-parse-able string literal
229    #[error("invalid string literal: {0}")]
230    InvalidString(String),
231    /// Returned when attempting to use an arbitrary variable name.
232    /// Cedar does not support arbitrary variables.
233    #[error("invalid variable: {0}")]
234    #[diagnostic(help("the valid Cedar variables are `principal`, `action`, `resource`, and `context`; did you mean to enclose `{0}` in quotes to make a string?"))]
235    ArbitraryVariable(SmolStr),
236    /// Returned when attempting to use an invalid attribute name
237    #[error("invalid attribute name: {0}")]
238    #[diagnostic(help("attribute names can either be identifiers or string literals"))]
239    InvalidAttribute(SmolStr),
240    /// Returned when attempting to use an attribute with a namespace
241    #[error("`{0}` cannot be used as an attribute as it contains a namespace")]
242    PathAsAttribute(String),
243    /// Returned when a policy attempts to call a method function-style
244    #[error("`{0}` is a method, not a function")]
245    #[diagnostic(help("use a method-style call `e.{0}(..)`"))]
246    FunctionCallOnMethod(ast::UnreservedId),
247    /// Returned when a policy attempts to call a function in the method style
248    #[error("`{0}` is a function, not a method")]
249    #[diagnostic(help("use a function-style call `{0}(..)`"))]
250    MethodCallOnFunction(ast::UnreservedId),
251    /// Returned when the right hand side of a `like` expression is not a constant pattern literal
252    #[error("right hand side of a `like` expression must be a pattern literal, but got `{0}`")]
253    InvalidPattern(String),
254    /// Returned when the right hand side of a `is` expression is not an entity type name
255    #[error("right hand side of an `is` expression must be an entity type name, but got `{0}`")]
256    #[diagnostic(help("try using `==` to test for equality"))]
257    InvalidIsType(String),
258    /// Returned when an unexpected node is in the policy scope
259    #[error("expected {expected}, found {got}")]
260    WrongNode {
261        /// What the expected AST node kind was
262        expected: &'static str,
263        /// What AST node was present in the policy source
264        got: String,
265        /// Optional free-form text with a suggestion for how to fix the problem
266        #[help]
267        suggestion: Option<String>,
268    },
269    /// Returned when a policy contains ambiguous ordering of operators.
270    /// This can be resolved by using parenthesis to make order explicit
271    #[error("multiple relational operators (>, ==, in, etc.) must be used with parentheses to make ordering explicit")]
272    AmbiguousOperators,
273    /// Returned when a policy uses the division operator (`/`), which is not supported
274    #[error("division is not supported")]
275    UnsupportedDivision,
276    /// Returned when a policy uses the remainder/modulo operator (`%`), which is not supported
277    #[error("remainder/modulo is not supported")]
278    UnsupportedModulo,
279    /// Any `ExpressionConstructionError` can also happen while converting CST to AST
280    #[error(transparent)]
281    #[diagnostic(transparent)]
282    ExpressionConstructionError(#[from] ast::ExpressionConstructionError),
283    /// Returned when a policy contains an integer literal that is out of range
284    #[error("integer literal `{0}` is too large")]
285    #[diagnostic(help("maximum allowed integer literal is `{}`", ast::InputInteger::MAX))]
286    IntegerLiteralTooLarge(u64),
287    /// Returned when a unary operator is chained more than 4 times in a row
288    #[error("too many occurrences of `{0}`")]
289    #[diagnostic(help("cannot chain more the 4 applications of a unary operator"))]
290    UnaryOpLimit(ast::UnaryOp),
291    /// Returned when a variable is called as a function, which is not allowed.
292    /// Functions are not first class values in Cedar
293    #[error("`{0}(...)` is not a valid function call")]
294    #[diagnostic(help("variables cannot be called as functions"))]
295    VariableCall(ast::Var),
296    /// Returned when a policy attempts to call a method on a value that has no methods
297    #[error("attempted to call `{0}.{1}(...)`, but `{0}` does not have any methods")]
298    NoMethods(ast::Name, ast::UnreservedId),
299    /// Returned when a policy attempts to call a method that does not exist
300    #[error("`{id}` is not a valid method")]
301    UnknownMethod {
302        /// The user-provided method id
303        id: ast::UnreservedId,
304        /// The hint to resolve the error
305        #[help]
306        hint: Option<String>,
307    },
308    /// Returned when a policy attempts to call a function that does not exist
309    #[error("`{id}` is not a valid function")]
310    UnknownFunction {
311        /// The user-provided function id
312        id: ast::Name,
313        /// The hint to resolve the error
314        #[help]
315        hint: Option<String>,
316    },
317    /// Returned when a policy attempts to write an entity literal
318    #[error("invalid entity literal: {0}")]
319    #[diagnostic(help("entity literals should have a form like `Namespace::User::\"alice\"`"))]
320    InvalidEntityLiteral(String),
321    /// Returned when an expression is the target of a function call.
322    /// Functions are not first class values in Cedar
323    #[error("function calls must be of the form `<name>(arg1, arg2, ...)`")]
324    ExpressionCall,
325    /// Returned when a policy attempts to access the fields of a value with no fields
326    #[error("invalid member access `{0}.{1}`, `{0}` has no fields or methods")]
327    InvalidAccess(ast::Name, SmolStr),
328    /// Returned when a policy attempts to index on a fields of a value with no fields
329    #[error("invalid indexing expression `{0}[\"{}\"]`, `{0}` has no fields", .1.escape_debug())]
330    InvalidIndex(ast::Name, SmolStr),
331    /// Returned when the contents of an indexing expression is not a string literal
332    #[error("the contents of an index expression must be a string literal")]
333    NonStringIndex,
334    /// Returned when a user attempts to use type-constraint `:` syntax. This
335    /// syntax was not adopted, but `is` can be used to write type constraints
336    /// in the policy scope.
337    #[error("type constraints using `:` are not supported")]
338    #[diagnostic(help("try using `is` instead"))]
339    TypeConstraints,
340    /// Returned when a string needs to be fully normalized
341    #[error("`{kind}` needs to be normalized (e.g., whitespace removed): {src}")]
342    #[diagnostic(help("the normalized form is `{normalized_src}`"))]
343    NonNormalizedString {
344        /// The kind of string we are expecting
345        kind: &'static str,
346        /// The source string passed in
347        src: String,
348        /// The normalized form of the string
349        normalized_src: String,
350    },
351    /// Returned when a CST node is empty during CST to AST/EST conversion.
352    /// This should have resulted in an error during the text to CST
353    /// conversion, which will terminate parsing. So it should be unreachable
354    /// in later stages.
355    #[error("internal invariant violated. Parsed data node should not be empty")]
356    #[diagnostic(help("please file an issue at <https://github.com/cedar-policy/cedar/issues> including the text that failed to parse"))]
357    EmptyNodeInvariantViolation,
358    /// Returned when a function or method is called with the wrong arity
359    #[error("call to `{name}` requires exactly {expected} argument{}, but got {got} argument{}", if .expected == &1 { "" } else { "s" }, if .got == &1 { "" } else { "s" })]
360    WrongArity {
361        /// Name of the function or method being called
362        name: &'static str,
363        /// The expected number of arguments
364        expected: usize,
365        /// The number of arguments present in source
366        got: usize,
367    },
368    /// Returned when a string contains invalid escapes
369    #[error(transparent)]
370    #[diagnostic(transparent)]
371    Unescape(#[from] UnescapeError),
372    /// Returned when a policy scope has incorrect entity uids or template slots
373    #[error(transparent)]
374    #[diagnostic(transparent)]
375    WrongEntityArgument(#[from] parse_errors::WrongEntityArgument),
376    /// Returned when a policy contains a template slot other than `?principal` or `?resource`
377    #[error("`{0}` is not a valid template slot")]
378    #[diagnostic(help("a template slot may only be `?principal` or `?resource`"))]
379    InvalidSlot(SmolStr),
380    /// Returned when an entity type contains a reserved namespace or typename (as of this writing, just `__cedar`)
381    #[error(transparent)]
382    #[diagnostic(transparent)]
383    ReservedNamespace(#[from] ReservedNameError),
384    /// Returned when a policy uses `_ in _ is _` instead of `_ is _ in _` in the policy scope
385    #[error("when `is` and `in` are used together, `is` must come first")]
386    #[diagnostic(help("try `_ is _ in _`"))]
387    InvertedIsIn,
388}
389
390impl ToASTErrorKind {
391    /// Constructor for the [`ToASTErrorKind::WrongNode`] error
392    pub fn wrong_node(
393        expected: &'static str,
394        got: impl Into<String>,
395        suggestion: Option<impl Into<String>>,
396    ) -> Self {
397        Self::WrongNode {
398            expected,
399            got: got.into(),
400            suggestion: suggestion.map(Into::into),
401        }
402    }
403
404    /// Constructor for the [`ToASTErrorKind::WrongArity`] error
405    pub fn wrong_arity(name: &'static str, expected: usize, got: usize) -> Self {
406        Self::WrongArity {
407            name,
408            expected,
409            got,
410        }
411    }
412
413    /// Constructor for the [`ToASTErrorKind::SlotsInConditionClause`] error
414    pub fn slots_in_condition_clause(slot: ast::Slot, clause_type: &'static str) -> Self {
415        parse_errors::SlotsInConditionClause { slot, clause_type }.into()
416    }
417
418    /// Constructor for the [`ToASTErrorKind::ExpectedStaticPolicy`] error
419    pub fn expected_static_policy(slot: ast::Slot) -> Self {
420        parse_errors::ExpectedStaticPolicy { slot }.into()
421    }
422
423    /// Constructor for the [`ToASTErrorKind::ExpectedTemplate`] error
424    pub fn expected_template() -> Self {
425        parse_errors::ExpectedTemplate::new().into()
426    }
427
428    /// Constructor for the [`ToASTErrorKind::WrongEntityArgument`] error when
429    /// one kind of entity argument was expected
430    pub fn wrong_entity_argument_one_expected(
431        expected: parse_errors::Ref,
432        got: parse_errors::Ref,
433    ) -> Self {
434        parse_errors::WrongEntityArgument {
435            expected: Either::Left(expected),
436            got,
437        }
438        .into()
439    }
440
441    /// Constructor for the [`ToASTErrorKind::WrongEntityArgument`] error when
442    /// one of two kinds of entity argument was expected
443    pub fn wrong_entity_argument_two_expected(
444        r1: parse_errors::Ref,
445        r2: parse_errors::Ref,
446        got: parse_errors::Ref,
447    ) -> Self {
448        let expected = Either::Right((r1, r2));
449        parse_errors::WrongEntityArgument { expected, got }.into()
450    }
451}
452
453/// Error subtypes for [`ToASTErrorKind`]
454pub mod parse_errors {
455
456    use std::sync::Arc;
457
458    use super::*;
459
460    /// Details about a `ExpectedStaticPolicy` error.
461    #[derive(Debug, Clone, Diagnostic, Error, PartialEq, Eq)]
462    #[error("expected a static policy, got a template containing the slot {}", slot.id)]
463    #[diagnostic(help("try removing the template slot(s) from this policy"))]
464    pub struct ExpectedStaticPolicy {
465        /// Slot that was found (which is not valid in a static policy)
466        pub(crate) slot: ast::Slot,
467    }
468
469    impl From<ast::UnexpectedSlotError> for ExpectedStaticPolicy {
470        fn from(err: ast::UnexpectedSlotError) -> Self {
471            match err {
472                ast::UnexpectedSlotError::FoundSlot(slot) => Self { slot },
473            }
474        }
475    }
476
477    /// Details about a `ExpectedTemplate` error.
478    #[derive(Debug, Clone, Diagnostic, Error, PartialEq, Eq)]
479    #[error("expected a template, got a static policy")]
480    #[diagnostic(help("a template should include slot(s) `?principal` or `?resource`"))]
481    pub struct ExpectedTemplate {
482        /// A private field, just so the public interface notes this as a
483        /// private-fields struct and not a empty-fields struct for semver
484        /// purposes (e.g., consumers cannot construct this type with
485        /// `ExpectedTemplate {}`)
486        _dummy: (),
487    }
488
489    impl ExpectedTemplate {
490        pub(crate) fn new() -> Self {
491            Self { _dummy: () }
492        }
493    }
494
495    /// Details about a `SlotsInConditionClause` error.
496    #[derive(Debug, Clone, Diagnostic, Error, PartialEq, Eq)]
497    #[error("found template slot {} in a `{clause_type}` clause", slot.id)]
498    #[diagnostic(help("slots are currently unsupported in `{clause_type}` clauses"))]
499    pub struct SlotsInConditionClause {
500        /// Slot that was found in a when/unless clause
501        pub(crate) slot: ast::Slot,
502        /// Clause type, e.g. "when" or "unless"
503        pub(crate) clause_type: &'static str,
504    }
505
506    /// Details about an `InvalidActionType` error.
507    #[derive(Debug, Clone, Diagnostic, Error, PartialEq, Eq)]
508    #[diagnostic(help("action entities must have type `Action`, optionally in a namespace"))]
509    pub struct InvalidActionType {
510        pub(crate) euids: NonEmpty<Arc<ast::EntityUID>>,
511    }
512
513    impl std::fmt::Display for InvalidActionType {
514        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
515            let subject = if self.euids.len() > 1 {
516                "entity uids"
517            } else {
518                "an entity uid"
519            };
520            write!(f, "expected {subject} with type `Action` but got ")?;
521            join_with_conjunction(f, "and", self.euids.iter(), |f, e| write!(f, "`{e}`"))
522        }
523    }
524
525    /// Details about an `WrongEntityArgument` error.
526    #[derive(Debug, Clone, Diagnostic, Error, PartialEq, Eq)]
527    #[error("expected {}, found {got}", match .expected { Either::Left(r) => r.to_string(), Either::Right((r1, r2)) => format!("{r1} or {r2}") })]
528    pub struct WrongEntityArgument {
529        /// What kinds of references the given scope clause required.
530        /// Some scope clauses require exactly one kind of reference, some require one of two
531        pub(crate) expected: Either<Ref, (Ref, Ref)>,
532        /// The kind of reference that was present in the policy
533        pub(crate) got: Ref,
534    }
535
536    /// The 3 kinds of literals that can be in a policy scope
537    #[derive(Debug, Clone, PartialEq, Eq)]
538    pub enum Ref {
539        /// A single entity uids
540        Single,
541        /// A list of entity uids
542        Set,
543        /// A template slot
544        Template,
545    }
546
547    impl std::fmt::Display for Ref {
548        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
549            match self {
550                Ref::Single => write!(f, "single entity uid"),
551                Ref::Template => write!(f, "template slot"),
552                Ref::Set => write!(f, "set of entity uids"),
553            }
554        }
555    }
556}
557
558/// Error from the text -> CST parser
559#[derive(Clone, Debug, Error, PartialEq, Eq)]
560pub struct ToCSTError {
561    err: OwnedRawParseError,
562}
563
564impl ToCSTError {
565    /// Extract a primary source span locating the error.
566    pub fn primary_source_span(&self) -> SourceSpan {
567        match &self.err {
568            OwnedRawParseError::InvalidToken { location } => SourceSpan::from(*location),
569            OwnedRawParseError::UnrecognizedEof { location, .. } => SourceSpan::from(*location),
570            OwnedRawParseError::UnrecognizedToken {
571                token: (token_start, _, token_end),
572                ..
573            } => SourceSpan::from(*token_start..*token_end),
574            OwnedRawParseError::ExtraToken {
575                token: (token_start, _, token_end),
576            } => SourceSpan::from(*token_start..*token_end),
577            OwnedRawParseError::User { error } => error.loc.span,
578        }
579    }
580
581    pub(crate) fn from_raw_parse_err(err: RawParseError<'_>) -> Self {
582        Self {
583            err: err.map_token(|token| token.to_string()),
584        }
585    }
586
587    pub(crate) fn from_raw_err_recovery(recovery: RawErrorRecovery<'_>) -> Self {
588        Self::from_raw_parse_err(recovery.error)
589    }
590}
591
592impl Display for ToCSTError {
593    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
594        match &self.err {
595            OwnedRawParseError::InvalidToken { .. } => write!(f, "invalid token"),
596            OwnedRawParseError::UnrecognizedEof { .. } => write!(f, "unexpected end of input"),
597            OwnedRawParseError::UnrecognizedToken {
598                token: (_, token, _),
599                ..
600            } => write!(f, "unexpected token `{token}`"),
601            OwnedRawParseError::ExtraToken {
602                token: (_, token, _),
603                ..
604            } => write!(f, "extra token `{token}`"),
605            OwnedRawParseError::User { error } => write!(f, "{error}"),
606        }
607    }
608}
609
610impl Diagnostic for ToCSTError {
611    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
612        let primary_source_span = self.primary_source_span();
613        let labeled_span = match &self.err {
614            OwnedRawParseError::InvalidToken { .. } => LabeledSpan::underline(primary_source_span),
615            OwnedRawParseError::UnrecognizedEof { expected, .. } => LabeledSpan::new_with_span(
616                expected_to_string(expected, &POLICY_TOKEN_CONFIG),
617                primary_source_span,
618            ),
619            OwnedRawParseError::UnrecognizedToken { expected, .. } => LabeledSpan::new_with_span(
620                expected_to_string(expected, &POLICY_TOKEN_CONFIG),
621                primary_source_span,
622            ),
623            OwnedRawParseError::ExtraToken { .. } => LabeledSpan::underline(primary_source_span),
624            OwnedRawParseError::User { .. } => LabeledSpan::underline(primary_source_span),
625        };
626        Some(Box::new(iter::once(labeled_span)))
627    }
628}
629
630/// Defines configurable rules for how tokens in an `UnrecognizedToken` or
631/// `UnrecognizedEof` error should be displayed to users.
632#[derive(Debug)]
633pub struct ExpectedTokenConfig {
634    /// Defines user-friendly names for tokens used by our parser. Keys are the
635    /// names of tokens as defined in the `.lalrpop` grammar file. A token may
636    /// be omitted from this map if the name is already friendly enough.
637    pub friendly_token_names: HashMap<&'static str, &'static str>,
638
639    /// Some tokens defined in our grammar always cause later processing to fail.
640    /// Our policy grammar defines a token for the mod operator `%`, but we
641    /// reject any CST that uses the operator. To reduce confusion we filter
642    /// these from the list of expected tokens in an error message.
643    pub impossible_tokens: HashSet<&'static str>,
644
645    /// Both our policy and schema grammar have a generic identifier token
646    /// and some more specific identifier tokens that we use to parse specific
647    /// constructs. It is very often not useful to explicitly list out all of
648    /// these special identifier because the parser really just wants any
649    /// generic identifier. That it would accept these does not give any
650    /// useful information.
651    pub special_identifier_tokens: HashSet<&'static str>,
652
653    /// If this token is expected, then the parser expected a generic identifier, so
654    /// we omit the specific identifiers favor of saying we expect an "identifier".
655    pub identifier_sentinel: &'static str,
656
657    /// Special identifiers that may be worth displaying even if the parser
658    /// wants a generic identifier. These can tokens will be parsed as something
659    /// other than an identifier when they occur as the first token in an
660    /// expression (or a type, in the case of the schema grammar).
661    pub first_set_identifier_tokens: HashSet<&'static str>,
662
663    /// If this token is expected, then the parser was looking to start parsing
664    /// an expression (or type, in the schema). We know that we should report the
665    /// tokens that aren't parsed as identifiers at the start of an expression.
666    pub first_set_sentinel: &'static str,
667}
668
669lazy_static! {
670    static ref POLICY_TOKEN_CONFIG: ExpectedTokenConfig = ExpectedTokenConfig {
671        friendly_token_names: HashMap::from([
672            ("TRUE", "`true`"),
673            ("FALSE", "`false`"),
674            ("IF", "`if`"),
675            ("PERMIT", "`permit`"),
676            ("FORBID", "`forbid`"),
677            ("WHEN", "`when`"),
678            ("UNLESS", "`unless`"),
679            ("IN", "`in`"),
680            ("HAS", "`has`"),
681            ("LIKE", "`like`"),
682            ("IS", "`is`"),
683            ("THEN", "`then`"),
684            ("ELSE", "`else`"),
685            ("PRINCIPAL", "`principal`"),
686            ("ACTION", "`action`"),
687            ("RESOURCE", "`resource`"),
688            ("CONTEXT", "`context`"),
689            ("PRINCIPAL_SLOT", "`?principal`"),
690            ("RESOURCE_SLOT", "`?resource`"),
691            ("IDENTIFIER", "identifier"),
692            ("NUMBER", "number"),
693            ("STRINGLIT", "string literal"),
694        ]),
695        impossible_tokens: HashSet::from(["\"=\"", "\"%\"", "\"/\"", "OTHER_SLOT"]),
696        special_identifier_tokens: HashSet::from([
697            "PERMIT",
698            "FORBID",
699            "WHEN",
700            "UNLESS",
701            "IN",
702            "HAS",
703            "LIKE",
704            "IS",
705            "THEN",
706            "ELSE",
707            "PRINCIPAL",
708            "ACTION",
709            "RESOURCE",
710            "CONTEXT",
711        ]),
712        identifier_sentinel: "IDENTIFIER",
713        first_set_identifier_tokens: HashSet::from(["TRUE", "FALSE", "IF"]),
714        first_set_sentinel: "\"!\"",
715    };
716}
717
718/// Format lalrpop expected error messages
719pub fn expected_to_string(expected: &[String], config: &ExpectedTokenConfig) -> Option<String> {
720    let mut expected = expected
721        .iter()
722        .filter(|e| !config.impossible_tokens.contains(e.as_str()))
723        .map(|e| e.as_str())
724        .collect::<BTreeSet<_>>();
725    if expected.contains(config.identifier_sentinel) {
726        for token in config.special_identifier_tokens.iter() {
727            expected.remove(*token);
728        }
729        if !expected.contains(config.first_set_sentinel) {
730            for token in config.first_set_identifier_tokens.iter() {
731                expected.remove(*token);
732            }
733        }
734    }
735    if expected.is_empty() {
736        return None;
737    }
738
739    let mut expected_string = "expected ".to_owned();
740    // PANIC SAFETY Shouldn't be `Err` since we're writing strings to a string
741    #[allow(clippy::expect_used)]
742    join_with_conjunction(
743        &mut expected_string,
744        "or",
745        expected,
746        |f, token| match config.friendly_token_names.get(token) {
747            Some(friendly_token_name) => write!(f, "{}", friendly_token_name),
748            None => write!(f, "{}", token.replace('"', "`")),
749        },
750    )
751    .expect("failed to format expected tokens");
752    Some(expected_string)
753}
754
755/// Represents one or more [`ParseError`]s encountered when parsing a policy or
756/// template.
757#[derive(Clone, Debug, PartialEq, Eq)]
758pub struct ParseErrors(NonEmpty<ParseError>);
759
760impl ParseErrors {
761    /// Construct a `ParseErrors` with a single element
762    pub(crate) fn singleton(err: impl Into<ParseError>) -> Self {
763        Self(NonEmpty::singleton(err.into()))
764    }
765
766    /// Construct a new `ParseErrors` with at least one element
767    pub(crate) fn new(first: ParseError, rest: impl IntoIterator<Item = ParseError>) -> Self {
768        Self(NonEmpty {
769            head: first,
770            tail: rest.into_iter().collect::<Vec<_>>(),
771        })
772    }
773
774    /// Construct a new `ParseErrors` from another `NonEmpty` type
775    pub(crate) fn new_from_nonempty(errs: NonEmpty<ParseError>) -> Self {
776        Self(errs)
777    }
778
779    pub(crate) fn from_iter(i: impl IntoIterator<Item = ParseError>) -> Option<Self> {
780        NonEmpty::collect(i).map(Self::new_from_nonempty)
781    }
782
783    /// Flatten a `Vec<ParseErrors>` into a single `ParseErrors`, returning
784    /// `None` if the input vector is empty.
785    pub(crate) fn flatten(v: Vec<ParseErrors>) -> Option<Self> {
786        let (first, rest) = v.split_first()?;
787        let mut first = first.clone();
788        rest.iter()
789            .for_each(|errs| first.extend(errs.iter().cloned()));
790        Some(first)
791    }
792
793    /// If there are any `Err`s in the input, this function will return a
794    /// combined version of all errors. Otherwise, it will return a vector of
795    /// all the `Ok` values.
796    pub(crate) fn transpose<T>(
797        i: impl IntoIterator<Item = Result<T, ParseErrors>>,
798    ) -> Result<Vec<T>, Self> {
799        let mut errs = vec![];
800        let oks: Vec<_> = i
801            .into_iter()
802            .filter_map(|r| r.map_err(|e| errs.push(e)).ok())
803            .collect();
804        if let Some(combined_errs) = Self::flatten(errs) {
805            Err(combined_errs)
806        } else {
807            Ok(oks)
808        }
809    }
810}
811
812impl Display for ParseErrors {
813    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
814        write!(f, "{}", self.first()) // intentionally showing only the first error; see #326
815    }
816}
817
818impl std::error::Error for ParseErrors {
819    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
820        self.first().source()
821    }
822
823    #[allow(deprecated)]
824    fn description(&self) -> &str {
825        self.first().description()
826    }
827
828    #[allow(deprecated)]
829    fn cause(&self) -> Option<&dyn std::error::Error> {
830        self.first().cause()
831    }
832}
833
834// Except for `.related()`, everything else is forwarded to the first error.
835// This ensures that users who only use `Display`, `.code()`, `.labels()` etc, still get rich
836// information for the first error, even if they don't realize there are multiple errors here.
837// See #326.
838impl Diagnostic for ParseErrors {
839    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
840        // the .related() on the first error, and then the 2nd through Nth errors (but not their own .related())
841        let mut errs = self.iter().map(|err| err as &dyn Diagnostic);
842        errs.next().map(move |first_err| match first_err.related() {
843            Some(first_err_related) => Box::new(first_err_related.chain(errs)),
844            None => Box::new(errs) as Box<dyn Iterator<Item = _>>,
845        })
846    }
847
848    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
849        self.first().code()
850    }
851
852    fn severity(&self) -> Option<miette::Severity> {
853        self.first().severity()
854    }
855
856    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
857        self.first().help()
858    }
859
860    fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
861        self.first().url()
862    }
863
864    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
865        self.first().source_code()
866    }
867
868    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
869        self.first().labels()
870    }
871
872    fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
873        self.first().diagnostic_source()
874    }
875}
876
877impl AsRef<NonEmpty<ParseError>> for ParseErrors {
878    fn as_ref(&self) -> &NonEmpty<ParseError> {
879        &self.0
880    }
881}
882
883impl AsMut<NonEmpty<ParseError>> for ParseErrors {
884    fn as_mut(&mut self) -> &mut NonEmpty<ParseError> {
885        &mut self.0
886    }
887}
888
889impl Deref for ParseErrors {
890    type Target = NonEmpty<ParseError>;
891
892    fn deref(&self) -> &Self::Target {
893        &self.0
894    }
895}
896
897impl DerefMut for ParseErrors {
898    fn deref_mut(&mut self) -> &mut Self::Target {
899        &mut self.0
900    }
901}
902
903impl<T: Into<ParseError>> From<T> for ParseErrors {
904    fn from(err: T) -> Self {
905        ParseErrors::singleton(err.into())
906    }
907}
908
909impl<T: Into<ParseError>> Extend<T> for ParseErrors {
910    fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
911        self.0.extend(iter.into_iter().map(Into::into))
912    }
913}
914
915impl IntoIterator for ParseErrors {
916    type Item = ParseError;
917    type IntoIter = iter::Chain<iter::Once<Self::Item>, std::vec::IntoIter<Self::Item>>;
918
919    fn into_iter(self) -> Self::IntoIter {
920        self.0.into_iter()
921    }
922}
923
924impl<'a> IntoIterator for &'a ParseErrors {
925    type Item = &'a ParseError;
926    type IntoIter = iter::Chain<iter::Once<Self::Item>, std::slice::Iter<'a, ParseError>>;
927
928    fn into_iter(self) -> Self::IntoIter {
929        iter::once(&self.head).chain(self.tail.iter())
930    }
931}