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