Skip to main content

cedar_policy_core/parser/
cst_to_ast.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
17//! Conversions from CST to AST
18//!
19//! This module contains functions to convert Nodes containing CST items into
20//! AST items. It works with the parser CST output, where all nodes are optional.
21//!
22//! An important goal of the transformation is to provide as many errors as
23//! possible to expedite development cycles. To this end, many of the functions
24//! in this file will continue processing the input, even after an error has
25//! been detected. To combine errors before returning a result, we use a
26//! `flatten_tuple_N` helper function.
27
28// Throughout this module parameters to functions are references to CSTs or
29// owned AST items. This allows the most flexibility and least copying of data.
30// CSTs are almost entirely rewritten to ASTs, so we keep those values intact
31// and only clone the identifiers inside. ASTs here are temporary values until
32// the data passes out of the module, so we deconstruct them freely in the few
33// cases where there is a secondary conversion. This prevents any further
34// cloning.
35
36use super::err::{parse_errors, ParseError, ParseErrors, ToASTError, ToASTErrorKind};
37use super::node::Node;
38use super::unescape::{to_pattern, to_unescaped_string};
39use super::util::{flatten_tuple_2, flatten_tuple_3, flatten_tuple_4};
40use super::{cst, Loc};
41#[cfg(feature = "tolerant-ast")]
42use crate::ast::expr_allows_errors::ExprWithErrsBuilder;
43use crate::ast::{
44    self, ActionConstraint, Integer, PatternElem, PolicySetError, PrincipalConstraint,
45    PrincipalOrResourceConstraint, ResourceConstraint, UnreservedId, UnwrapInfallible,
46};
47use crate::expr_builder::{ExprBuilder, ExprBuilderInfallibleBuild};
48use crate::extensions::ExtStyles;
49use itertools::{Either, Itertools};
50use nonempty::nonempty;
51use nonempty::NonEmpty;
52use smol_str::{format_smolstr, SmolStr, ToSmolStr};
53use std::cmp::Ordering;
54use std::collections::BTreeMap;
55use std::mem;
56use std::sync::Arc;
57
58/// Defines the function `cst::Expr::to_ref_or_refs` and other similar functions
59/// for converting CST expressions into one or multiple entity UIDS. Used to
60/// extract entity uids from expressions that appear in the policy scope.
61mod to_ref_or_refs;
62use to_ref_or_refs::OneOrMultipleRefs;
63
64const INVALID_SNIPPET: &str = "<invalid>";
65
66/// Type alias for convenience
67type Result<T> = std::result::Result<T, ParseErrors>;
68
69impl Node<Option<cst::Policies>> {
70    /// Iterate over the `Policy` nodes in this `cst::Policies`, with
71    /// corresponding generated `PolicyID`s
72    pub fn with_generated_policyids(
73        &self,
74    ) -> Result<impl Iterator<Item = (ast::PolicyID, &Node<Option<cst::Policy>>)>> {
75        let policies = self.try_as_inner()?;
76
77        Ok(policies.0.iter().enumerate().map(|(count, node)| {
78            (
79                ast::PolicyID::from_smolstr(format_smolstr!("policy{count}")),
80                node,
81            )
82        }))
83    }
84
85    /// convert `cst::Policies` to `ast::PolicySet`
86    pub fn to_policyset(&self) -> Result<ast::PolicySet> {
87        let mut pset = ast::PolicySet::new();
88        let mut all_errs: Vec<ParseErrors> = vec![];
89        // Caution: `parser::parse_policyset_and_also_return_policy_text()`
90        // depends on this function returning a policy set with `PolicyID`s as
91        // generated by `with_generated_policyids()` to maintain an invariant.
92        for (policy_id, policy) in self.with_generated_policyids()? {
93            // policy may have convert error
94            match policy.to_policy_or_template(policy_id) {
95                Ok(Either::Right(template)) => {
96                    if let Err(e) = pset.add_template(template) {
97                        match e {
98                            PolicySetError::Occupied { id } => all_errs.push(
99                                self.to_ast_err(ToASTErrorKind::DuplicateTemplateId(id))
100                                    .into(),
101                            ),
102                        };
103                    }
104                }
105                Ok(Either::Left(static_policy)) => {
106                    if let Err(e) = pset.add_static(static_policy) {
107                        match e {
108                            PolicySetError::Occupied { id } => all_errs.push(
109                                self.to_ast_err(ToASTErrorKind::DuplicatePolicyId(id))
110                                    .into(),
111                            ),
112                        };
113                    }
114                }
115                Err(errs) => {
116                    all_errs.push(errs);
117                }
118            };
119        }
120
121        // fail on any error
122        if let Some(errs) = ParseErrors::flatten(all_errs) {
123            Err(errs)
124        } else {
125            Ok(pset)
126        }
127    }
128
129    /// convert `cst::Policies` to `ast::PolicySet`
130    #[cfg(feature = "tolerant-ast")]
131    pub fn to_policyset_tolerant(&self) -> Result<ast::PolicySet> {
132        let mut pset = ast::PolicySet::new();
133        let mut all_errs: Vec<ParseErrors> = vec![];
134        // Caution: `parser::parse_policyset_and_also_return_policy_text()`
135        // depends on this function returning a policy set with `PolicyID`s as
136        // generated by `with_generated_policyids()` to maintain an invariant.
137        for (policy_id, policy) in self.with_generated_policyids()? {
138            // policy may have convert error
139            match policy.to_policy_or_template_tolerant(policy_id) {
140                Ok(Either::Right(template)) => {
141                    if let Err(e) = pset.add_template(template) {
142                        match e {
143                            PolicySetError::Occupied { id } => all_errs.push(
144                                self.to_ast_err(ToASTErrorKind::DuplicateTemplateId(id))
145                                    .into(),
146                            ),
147                        };
148                    }
149                }
150                Ok(Either::Left(static_policy)) => {
151                    if let Err(e) = pset.add_static(static_policy) {
152                        match e {
153                            PolicySetError::Occupied { id } => all_errs.push(
154                                self.to_ast_err(ToASTErrorKind::DuplicatePolicyId(id))
155                                    .into(),
156                            ),
157                        };
158                    }
159                }
160                Err(errs) => {
161                    all_errs.push(errs);
162                }
163            };
164        }
165
166        // fail on any error
167        if let Some(errs) = ParseErrors::flatten(all_errs) {
168            Err(errs)
169        } else {
170            Ok(pset)
171        }
172    }
173}
174
175impl Node<Option<cst::Policy>> {
176    /// Convert `cst::Policy` to `ast::Template`. Works for static policies as
177    /// well, which will become templates with 0 slots
178    pub fn to_template(&self, id: ast::PolicyID) -> Result<ast::Template> {
179        self.to_policy_template(id)
180    }
181
182    /// Convert `cst::Policy` to `ast::Template`. Works for static policies as
183    /// well, which will become templates with 0 slots
184    #[cfg(feature = "tolerant-ast")]
185    pub fn to_template_tolerant(&self, id: ast::PolicyID) -> Result<ast::Template> {
186        self.to_policy_template_tolerant(id)
187    }
188
189    /// Convert `cst::Policy` to an AST `StaticPolicy` or `Template`
190    pub fn to_policy_or_template(
191        &self,
192        id: ast::PolicyID,
193    ) -> Result<Either<ast::StaticPolicy, ast::Template>> {
194        let t = self.to_policy_template(id)?;
195        if t.slots().count() == 0 {
196            #[expect(clippy::expect_used, reason = "A `Template` with no slots will successfully convert to a `StaticPolicy`")]
197            let p = ast::StaticPolicy::try_from(t).expect("internal invariant violation: a template with no slots should be a valid static policy");
198            Ok(Either::Left(p))
199        } else {
200            Ok(Either::Right(t))
201        }
202    }
203
204    /// Convert `cst::Policy` to an AST `StaticPolicy` or `Template`
205    #[cfg(feature = "tolerant-ast")]
206    pub fn to_policy_or_template_tolerant(
207        &self,
208        id: ast::PolicyID,
209    ) -> Result<Either<ast::StaticPolicy, ast::Template>> {
210        let t = self.to_policy_template_tolerant(id)?;
211        if t.slots().count() == 0 {
212            #[expect(clippy::expect_used, reason = "A `Template` with no slots will successfully convert to a `StaticPolicy`")]
213            let p = ast::StaticPolicy::try_from(t).expect("internal invariant violation: a template with no slots should be a valid static policy");
214            Ok(Either::Left(p))
215        } else {
216            Ok(Either::Right(t))
217        }
218    }
219
220    /// Convert `cst::Policy` to an AST `StaticPolicy`. (Will fail if the CST is for a template)
221    pub fn to_policy(&self, id: ast::PolicyID) -> Result<ast::StaticPolicy> {
222        let maybe_template = self.to_policy_template(id);
223        let maybe_policy = maybe_template.map(ast::StaticPolicy::try_from);
224        match maybe_policy {
225            // Successfully parsed a static policy
226            Ok(Ok(p)) => Ok(p),
227            // The source parsed as a template, but not a static policy
228            Ok(Err(ast::UnexpectedSlotError::FoundSlot(slot))) => Err(ToASTError::new(
229                ToASTErrorKind::expected_static_policy(slot.clone()),
230                slot.loc.or_else(|| self.loc.clone()),
231            )
232            .into()),
233            // The source failed to parse completely. If the parse errors include
234            // `SlotsInConditionClause` also add an `ExpectedStaticPolicy` error.
235            Err(mut errs) => {
236                let new_errs = errs
237                    .iter()
238                    .filter_map(|err| match err {
239                        ParseError::ToAST(err) => match err.kind() {
240                            ToASTErrorKind::SlotsInConditionClause(inner) => Some(ToASTError::new(
241                                ToASTErrorKind::expected_static_policy(inner.slot.clone()),
242                                err.source_loc().cloned(),
243                            )),
244                            _ => None,
245                        },
246                        _ => None,
247                    })
248                    .collect::<Vec<_>>();
249                errs.extend(new_errs);
250                Err(errs)
251            }
252        }
253    }
254
255    /// Convert `cst::Policy` to `ast::Template`. Works for static policies as
256    /// well, which will become templates with 0 slots
257    pub fn to_policy_template(&self, id: ast::PolicyID) -> Result<ast::Template> {
258        let policy = self.try_as_inner()?;
259        #[cfg_attr(
260            not(feature = "tolerant-ast"),
261            expect(
262                clippy::infallible_destructuring_match,
263                reason = "this is not a destructuring match when `tolerant-ast` is enabled"
264            )
265        )]
266        let policy = match policy {
267            cst::Policy::Policy(policy_impl) => policy_impl,
268            #[cfg(feature = "tolerant-ast")]
269            cst::Policy::PolicyError => {
270                // This will only happen if we use a 'tolerant' parser, otherwise errors should be caught
271                // during parsing to CST
272                return Err(ParseErrors::singleton(ToASTError::new(
273                    ToASTErrorKind::CSTErrorNode,
274                    self.loc.clone(),
275                )));
276            }
277        };
278
279        // convert effect
280        let maybe_effect = policy.effect.to_effect();
281
282        // convert annotations
283        let maybe_annotations = policy.get_ast_annotations(|value, loc| {
284            ast::Annotation::with_optional_value(value, loc.cloned())
285        });
286
287        // convert scope
288        let maybe_scope = policy.extract_scope();
289
290        // convert conditions
291        let maybe_conds = ParseErrors::transpose(policy.conds.iter().map(|c| {
292            let (e, is_when) = c.to_expr::<ast::ExprBuilder<()>>()?;
293
294            let slot_errs = e.slots().map(|slot| {
295                ToASTError::new(
296                    ToASTErrorKind::slots_in_condition_clause(
297                        slot.clone(),
298                        if is_when { "when" } else { "unless" },
299                    ),
300                    slot.loc.or_else(|| c.loc.clone()),
301                )
302                .into()
303            });
304            match ParseErrors::from_iter(slot_errs) {
305                Some(errs) => Err(errs),
306                None => Ok(e),
307            }
308        }));
309
310        let (effect, annotations, (principal, action, resource), conds) =
311            flatten_tuple_4(maybe_effect, maybe_annotations, maybe_scope, maybe_conds)?;
312        Ok(construct_template_policy(
313            id,
314            annotations.into(),
315            effect,
316            principal,
317            action,
318            resource,
319            conds,
320            self.loc(),
321        ))
322    }
323
324    /// Convert `cst::Policy` to an AST `StaticPolicy`. (Will fail if the CST is for a template)
325    /// NOTE: This function allows partial parsing and can produce AST Error nodes
326    /// These cannot be evaluated
327    /// Should ONLY be used to examine a partially constructed AST from invalid Cedar
328    #[cfg(feature = "tolerant-ast")]
329    pub fn to_policy_tolerant(&self, id: ast::PolicyID) -> Result<ast::StaticPolicy> {
330        let maybe_template = self.to_policy_template_tolerant(id);
331        let maybe_policy = maybe_template.map(ast::StaticPolicy::try_from);
332        match maybe_policy {
333            // Successfully parsed a static policy
334            Ok(Ok(p)) => Ok(p),
335            // The source parsed as a template, but not a static policy
336            Ok(Err(ast::UnexpectedSlotError::FoundSlot(slot))) => Err(ToASTError::new(
337                ToASTErrorKind::expected_static_policy(slot.clone()),
338                slot.loc.or_else(|| self.loc.clone()),
339            )
340            .into()),
341            // The source failed to parse completely. If the parse errors include
342            // `SlotsInConditionClause` also add an `ExpectedStaticPolicy` error.
343            Err(mut errs) => {
344                let new_errs = errs
345                    .iter()
346                    .filter_map(|err| match err {
347                        ParseError::ToAST(err) => match err.kind() {
348                            ToASTErrorKind::SlotsInConditionClause(inner) => Some(ToASTError::new(
349                                ToASTErrorKind::expected_static_policy(inner.slot.clone()),
350                                err.source_loc().cloned(),
351                            )),
352                            _ => None,
353                        },
354                        _ => None,
355                    })
356                    .collect::<Vec<_>>();
357                errs.extend(new_errs);
358                Err(errs)
359            }
360        }
361    }
362
363    /// Convert `cst::Policy` to `ast::Template`. Works for static policies as
364    /// well, which will become templates with 0 slots
365    /// NOTE: This function allows partial parsing and can produce AST Error nodes
366    /// These cannot be evaluated
367    /// Should ONLY be used to examine a partially constructed AST from invalid Cedar
368    #[cfg(feature = "tolerant-ast")]
369    pub fn to_policy_template_tolerant(&self, id: ast::PolicyID) -> Result<ast::Template> {
370        let policy = self.try_as_inner()?;
371        let policy = match policy {
372            cst::Policy::Policy(policy_impl) => policy_impl,
373            cst::Policy::PolicyError => {
374                return Ok(ast::Template::error(id, self.loc.clone()));
375            }
376        };
377        // convert effect
378        let maybe_effect = policy.effect.to_effect();
379
380        // convert annotations
381        let maybe_annotations = policy.get_ast_annotations(|value, loc| {
382            ast::Annotation::with_optional_value(value, loc.cloned())
383        });
384
385        // convert scope
386        let maybe_scope = policy.extract_scope_tolerant_ast();
387
388        // convert conditions
389        let maybe_conds = ParseErrors::transpose(policy.conds.iter().map(|c| {
390            let (e, is_when) = c.to_expr::<ExprWithErrsBuilder<()>>()?;
391            let slot_errs = e.slots().map(|slot| {
392                ToASTError::new(
393                    ToASTErrorKind::slots_in_condition_clause(
394                        slot.clone(),
395                        if is_when { "when" } else { "unless" },
396                    ),
397                    slot.loc.or_else(|| c.loc.clone()),
398                )
399                .into()
400            });
401            match ParseErrors::from_iter(slot_errs) {
402                Some(errs) => Err(errs),
403                None => Ok(e),
404            }
405        }));
406
407        let (effect, annotations, (principal, action, resource), conds) =
408            flatten_tuple_4(maybe_effect, maybe_annotations, maybe_scope, maybe_conds)?;
409        Ok(construct_template_policy(
410            id,
411            annotations.into(),
412            effect,
413            principal,
414            action,
415            resource,
416            conds,
417            self.loc.as_ref(),
418        ))
419    }
420}
421
422impl cst::PolicyImpl {
423    /// Get the scope constraints from the `cst::Policy`
424    pub fn extract_scope(
425        &self,
426    ) -> Result<(PrincipalConstraint, ActionConstraint, ResourceConstraint)> {
427        // Tracks where the last variable in the scope ended. We'll point to
428        // this position to indicate where to fill in vars if we're missing one.
429        let mut end_of_last_var = self.effect.loc.as_ref().map(|loc| loc.end());
430
431        let mut vars = self.variables.iter();
432        let maybe_principal = if let Some(scope1) = vars.next() {
433            end_of_last_var = scope1.loc.as_ref().map(|loc| loc.end()).or(end_of_last_var);
434            scope1.to_principal_constraint(TolerantAstSetting::NotTolerant)
435        } else {
436            let effect_span = self
437                .effect
438                .loc
439                .as_ref()
440                .and_then(|loc| end_of_last_var.map(|end| loc.span(end)));
441            Err(ToASTError::new(
442                ToASTErrorKind::MissingScopeVariable(ast::Var::Principal),
443                effect_span,
444            )
445            .into())
446        };
447        let maybe_action = if let Some(scope2) = vars.next() {
448            end_of_last_var = scope2.loc.as_ref().map(|loc| loc.end()).or(end_of_last_var);
449            scope2.to_action_constraint(TolerantAstSetting::NotTolerant)
450        } else {
451            let effect_span = self
452                .effect
453                .loc
454                .as_ref()
455                .and_then(|loc| end_of_last_var.map(|end| loc.span(end)));
456            Err(ToASTError::new(
457                ToASTErrorKind::MissingScopeVariable(ast::Var::Action),
458                effect_span,
459            )
460            .into())
461        };
462        let maybe_resource = if let Some(scope3) = vars.next() {
463            scope3.to_resource_constraint(TolerantAstSetting::NotTolerant)
464        } else {
465            let effect_span = self
466                .effect
467                .loc
468                .as_ref()
469                .and_then(|loc| end_of_last_var.map(|end| loc.span(end)));
470            Err(ToASTError::new(
471                ToASTErrorKind::MissingScopeVariable(ast::Var::Resource),
472                effect_span,
473            )
474            .into())
475        };
476
477        let maybe_extra_vars = if let Some(errs) = ParseErrors::from_iter(
478            // Add each of the extra constraints to the error list
479            vars.map(|extra_var| {
480                extra_var
481                    .try_as_inner()
482                    .map(|def| {
483                        extra_var
484                            .to_ast_err(ToASTErrorKind::ExtraScopeElement(Box::new(def.clone())))
485                    })
486                    .unwrap_or_else(|e| e)
487                    .into()
488            }),
489        ) {
490            Err(errs)
491        } else {
492            Ok(())
493        };
494        let (principal, action, resource, _) = flatten_tuple_4(
495            maybe_principal,
496            maybe_action,
497            maybe_resource,
498            maybe_extra_vars,
499        )?;
500        Ok((principal, action, resource))
501    }
502
503    /// Get the scope constraints from the `cst::Policy`
504    #[cfg(feature = "tolerant-ast")]
505    pub fn extract_scope_tolerant_ast(
506        &self,
507    ) -> Result<(PrincipalConstraint, ActionConstraint, ResourceConstraint)> {
508        // Tracks where the last variable in the scope ended. We'll point to
509        // this position to indicate where to fill in vars if we're missing one.
510        let mut end_of_last_var = self.effect.loc.as_ref().map(|loc| loc.end());
511
512        let mut vars = self.variables.iter();
513        let maybe_principal = if let Some(scope1) = vars.next() {
514            end_of_last_var = scope1.loc.as_ref().map(|loc| loc.end()).or(end_of_last_var);
515            scope1.to_principal_constraint(TolerantAstSetting::Tolerant)
516        } else {
517            let effect_span = self
518                .effect
519                .loc
520                .as_ref()
521                .and_then(|loc| end_of_last_var.map(|end| loc.span(end)));
522            Err(ToASTError::new(
523                ToASTErrorKind::MissingScopeVariable(ast::Var::Principal),
524                effect_span,
525            )
526            .into())
527        };
528        let maybe_action = if let Some(scope2) = vars.next() {
529            end_of_last_var = scope2.loc.as_ref().map(|loc| loc.end()).or(end_of_last_var);
530            scope2.to_action_constraint(TolerantAstSetting::Tolerant)
531        } else {
532            let effect_span = self
533                .effect
534                .loc
535                .as_ref()
536                .and_then(|loc| end_of_last_var.map(|end| loc.span(end)));
537            Err(ToASTError::new(
538                ToASTErrorKind::MissingScopeVariable(ast::Var::Action),
539                effect_span,
540            )
541            .into())
542        };
543        let maybe_resource = if let Some(scope3) = vars.next() {
544            scope3.to_resource_constraint(TolerantAstSetting::Tolerant)
545        } else {
546            let effect_span = self
547                .effect
548                .loc
549                .as_ref()
550                .and_then(|loc| end_of_last_var.map(|end| loc.span(end)));
551            Err(ToASTError::new(
552                ToASTErrorKind::MissingScopeVariable(ast::Var::Resource),
553                effect_span,
554            )
555            .into())
556        };
557
558        let maybe_extra_vars = if let Some(errs) = ParseErrors::from_iter(
559            // Add each of the extra constraints to the error list
560            vars.map(|extra_var| {
561                extra_var
562                    .try_as_inner()
563                    .map(|def| {
564                        extra_var
565                            .to_ast_err(ToASTErrorKind::ExtraScopeElement(Box::new(def.clone())))
566                    })
567                    .unwrap_or_else(|e| e)
568                    .into()
569            }),
570        ) {
571            Err(errs)
572        } else {
573            Ok(())
574        };
575        let (principal, action, resource, _) = flatten_tuple_4(
576            maybe_principal,
577            maybe_action,
578            maybe_resource,
579            maybe_extra_vars,
580        )?;
581        Ok((principal, action, resource))
582    }
583
584    /// Get annotations from the `cst::Policy`
585    pub fn get_ast_annotations<T>(
586        &self,
587        annotation_constructor: impl Fn(Option<SmolStr>, Option<&Loc>) -> T,
588    ) -> Result<BTreeMap<ast::AnyId, T>> {
589        let mut annotations = BTreeMap::new();
590        let mut all_errs: Vec<ParseErrors> = vec![];
591        for node in self.annotations.iter() {
592            match node.to_kv_pair(&annotation_constructor) {
593                Ok((k, v)) => {
594                    use std::collections::btree_map::Entry;
595                    match annotations.entry(k) {
596                        Entry::Occupied(oentry) => {
597                            all_errs.push(
598                                ToASTError::new(
599                                    ToASTErrorKind::DuplicateAnnotation(oentry.key().clone()),
600                                    node.loc.clone(),
601                                )
602                                .into(),
603                            );
604                        }
605                        Entry::Vacant(ventry) => {
606                            ventry.insert(v);
607                        }
608                    }
609                }
610                Err(errs) => {
611                    all_errs.push(errs);
612                }
613            }
614        }
615        match ParseErrors::flatten(all_errs) {
616            Some(errs) => Err(errs),
617            None => Ok(annotations),
618        }
619    }
620}
621
622impl Node<Option<cst::Annotation>> {
623    /// Get the (k, v) pair for the annotation. Critically, this checks validity
624    /// for the strings and does unescaping
625    pub fn to_kv_pair<T>(
626        &self,
627        annotation_constructor: impl Fn(Option<SmolStr>, Option<&Loc>) -> T,
628    ) -> Result<(ast::AnyId, T)> {
629        let anno = self.try_as_inner()?;
630
631        let maybe_key = anno.key.to_any_ident();
632        let maybe_value = anno
633            .value
634            .as_ref()
635            .map(|a| {
636                a.as_valid_string().and_then(|s| {
637                    to_unescaped_string(s).map_err(|unescape_errs| {
638                        ParseErrors::new_from_nonempty(
639                            unescape_errs.map(|e| self.to_ast_err(e).into()),
640                        )
641                    })
642                })
643            })
644            .transpose();
645
646        let (k, v) = flatten_tuple_2(maybe_key, maybe_value)?;
647        Ok((k, annotation_constructor(v, self.loc.as_ref())))
648    }
649}
650
651impl Node<Option<cst::Ident>> {
652    /// Convert `cst::Ident` to `ast::UnreservedId`. Fails for reserved or invalid identifiers
653    pub(crate) fn to_unreserved_ident(&self) -> Result<ast::UnreservedId> {
654        self.to_valid_ident()
655            .and_then(|id| id.try_into().map_err(|err| self.to_ast_err(err).into()))
656    }
657    /// Convert `cst::Ident` to `ast::Id`. Fails for reserved or invalid identifiers
658    pub fn to_valid_ident(&self) -> Result<ast::Id> {
659        let ident = self.try_as_inner()?;
660
661        match ident {
662            cst::Ident::If
663            | cst::Ident::True
664            | cst::Ident::False
665            | cst::Ident::Then
666            | cst::Ident::Else
667            | cst::Ident::In
668            | cst::Ident::Is
669            | cst::Ident::Has
670            | cst::Ident::Like => Err(self
671                .to_ast_err(ToASTErrorKind::ReservedIdentifier(ident.clone()))
672                .into()),
673            cst::Ident::Invalid(i) => Err(self
674                .to_ast_err(ToASTErrorKind::InvalidIdentifier(i.clone()))
675                .into()),
676            cst::Ident::Ident(i) => Ok(ast::Id::new_unchecked(i.clone())),
677            _ => Ok(ast::Id::new_unchecked(ident.to_smolstr())),
678        }
679    }
680
681    /// Convert [`cst::Ident`] to [`ast::AnyId`]. This method does not fail for
682    /// reserved identifiers; see notes on [`ast::AnyId`].
683    /// (It does fail for invalid identifiers, but there are no invalid
684    /// identifiers at the time of this writing; see notes on
685    /// [`cst::Ident::Invalid`])
686    pub fn to_any_ident(&self) -> Result<ast::AnyId> {
687        let ident = self.try_as_inner()?;
688
689        match ident {
690            cst::Ident::Invalid(i) => Err(self
691                .to_ast_err(ToASTErrorKind::InvalidIdentifier(i.clone()))
692                .into()),
693            cst::Ident::Ident(i) => Ok(ast::AnyId::new_unchecked(i.clone())),
694            _ => Ok(ast::AnyId::new_unchecked(ident.to_smolstr())),
695        }
696    }
697
698    pub(crate) fn to_effect(&self) -> Result<ast::Effect> {
699        let effect = self.try_as_inner()?;
700
701        match effect {
702            cst::Ident::Permit => Ok(ast::Effect::Permit),
703            cst::Ident::Forbid => Ok(ast::Effect::Forbid),
704            _ => Err(self
705                .to_ast_err(ToASTErrorKind::InvalidEffect(effect.clone()))
706                .into()),
707        }
708    }
709
710    /// Returns `Ok(true)` if the condition is "when" and `Ok(false)` if the
711    /// condition is "unless"
712    pub(crate) fn to_cond_is_when(&self) -> Result<bool> {
713        let cond = self.try_as_inner()?;
714
715        match cond {
716            cst::Ident::When => Ok(true),
717            cst::Ident::Unless => Ok(false),
718            _ => Err(self
719                .to_ast_err(ToASTErrorKind::InvalidCondition(cond.clone()))
720                .into()),
721        }
722    }
723
724    fn to_var(&self) -> Result<ast::Var> {
725        let ident = self.try_as_inner()?;
726
727        match ident {
728            cst::Ident::Principal => Ok(ast::Var::Principal),
729            cst::Ident::Action => Ok(ast::Var::Action),
730            cst::Ident::Resource => Ok(ast::Var::Resource),
731            ident => Err(self
732                .to_ast_err(ToASTErrorKind::InvalidScopeVariable(ident.clone()))
733                .into()),
734        }
735    }
736}
737
738impl ast::UnreservedId {
739    fn to_meth<Build: ExprBuilderInfallibleBuild>(
740        &self,
741        e: Build::Expr,
742        args: Vec<Build::Expr>,
743        loc: Option<&Loc>,
744    ) -> Result<Build::Expr> {
745        let builder = Build::new().with_maybe_source_loc(loc);
746        match self.as_ref() {
747            "contains" => extract_single_argument(args.into_iter(), "contains", loc)
748                .map(|arg| builder.contains(e, arg)),
749            "containsAll" => extract_single_argument(args.into_iter(), "containsAll", loc)
750                .map(|arg| builder.contains_all(e, arg)),
751            "containsAny" => extract_single_argument(args.into_iter(), "containsAny", loc)
752                .map(|arg| builder.contains_any(e, arg)),
753            "isEmpty" => {
754                require_zero_arguments(&args.into_iter(), "isEmpty", loc)?;
755                Ok(builder.is_empty(e))
756            }
757            "getTag" => extract_single_argument(args.into_iter(), "getTag", loc)
758                .map(|arg| builder.get_tag(e, arg)),
759            "hasTag" => extract_single_argument(args.into_iter(), "hasTag", loc)
760                .map(|arg| builder.has_tag(e, arg)),
761            _ => {
762                if ExtStyles::is_method(self) {
763                    let args = NonEmpty {
764                        head: e,
765                        tail: args,
766                    };
767                    Ok(builder
768                        .call_extension_fn(ast::Name::unqualified_name(self.clone()), args)
769                        .unwrap_infallible())
770                } else {
771                    let unqual_name = ast::Name::unqualified_name(self.clone());
772                    if ExtStyles::is_function(&unqual_name) {
773                        Err(ToASTError::new(
774                            ToASTErrorKind::MethodCallOnFunction(unqual_name.basename()),
775                            loc.cloned(),
776                        )
777                        .into())
778                    } else {
779                        convert_expr_error_to_parse_error::<Build>(
780                            ToASTError::new(
781                                ToASTErrorKind::UnknownMethod {
782                                    id: self.clone(),
783                                    hint: ExtStyles::suggest_method(self),
784                                },
785                                loc.cloned(),
786                            )
787                            .into(),
788                            loc,
789                        )
790                    }
791                }
792            }
793        }
794    }
795}
796
797/// Return the single argument in `args` iterator, or return a wrong arity error
798/// if the iterator has 0 elements or more than 1 element.
799fn extract_single_argument<T>(
800    args: impl ExactSizeIterator<Item = T>,
801    fn_name: &'static str,
802    loc: Option<&Loc>,
803) -> Result<T> {
804    args.exactly_one().map_err(|args| {
805        ParseErrors::singleton(ToASTError::new(
806            ToASTErrorKind::wrong_arity(fn_name, 1, args.len()),
807            loc.cloned(),
808        ))
809    })
810}
811
812/// Return a wrong arity error if the iterator has any elements.
813fn require_zero_arguments<T>(
814    args: &impl ExactSizeIterator<Item = T>,
815    fn_name: &'static str,
816    loc: Option<&Loc>,
817) -> Result<()> {
818    match args.len() {
819        0 => Ok(()),
820        n => Err(ParseErrors::singleton(ToASTError::new(
821            ToASTErrorKind::wrong_arity(fn_name, 0, n),
822            loc.cloned(),
823        ))),
824    }
825}
826
827#[derive(Debug)]
828enum PrincipalOrResource {
829    Principal(PrincipalConstraint),
830    Resource(ResourceConstraint),
831}
832
833#[derive(Debug, Clone, Copy)]
834enum TolerantAstSetting {
835    NotTolerant,
836    #[cfg(feature = "tolerant-ast")]
837    Tolerant,
838}
839
840impl Node<Option<cst::VariableDef>> {
841    fn to_principal_constraint(
842        &self,
843        tolerant_setting: TolerantAstSetting,
844    ) -> Result<PrincipalConstraint> {
845        match self.to_principal_or_resource_constraint(ast::Var::Principal, tolerant_setting)? {
846            PrincipalOrResource::Principal(p) => Ok(p),
847            PrincipalOrResource::Resource(_) => Err(self
848                .to_ast_err(ToASTErrorKind::IncorrectVariable {
849                    expected: ast::Var::Principal,
850                    got: ast::Var::Resource,
851                })
852                .into()),
853        }
854    }
855
856    fn to_resource_constraint(
857        &self,
858        tolerant_setting: TolerantAstSetting,
859    ) -> Result<ResourceConstraint> {
860        match self.to_principal_or_resource_constraint(ast::Var::Resource, tolerant_setting)? {
861            PrincipalOrResource::Principal(_) => Err(self
862                .to_ast_err(ToASTErrorKind::IncorrectVariable {
863                    expected: ast::Var::Resource,
864                    got: ast::Var::Principal,
865                })
866                .into()),
867            PrincipalOrResource::Resource(r) => Ok(r),
868        }
869    }
870
871    fn to_principal_or_resource_constraint(
872        &self,
873        expected: ast::Var,
874        tolerant_ast: TolerantAstSetting,
875    ) -> Result<PrincipalOrResource> {
876        let vardef = self.try_as_inner()?;
877        let var = vardef.variable.to_var()?;
878
879        if let Some(unused_typename) = vardef.unused_type_name.as_ref() {
880            unused_typename.to_type_constraint::<ast::ExprBuilder<()>>()?;
881        }
882
883        let c = if let Some((op, rel_expr)) = &vardef.ineq {
884            // special check for the syntax `_ in _ is _`
885            if op == &cst::RelOp::In {
886                if let Ok(expr) = rel_expr.to_expr::<ast::ExprBuilder<()>>() {
887                    if matches!(expr.expr_kind(), ast::ExprKind::Is { .. }) {
888                        return Err(self.to_ast_err(ToASTErrorKind::InvertedIsIn).into());
889                    }
890                }
891            }
892            let eref = match tolerant_ast {
893                TolerantAstSetting::NotTolerant => rel_expr.to_ref_or_slot(var)?,
894                #[cfg(feature = "tolerant-ast")]
895                TolerantAstSetting::Tolerant => rel_expr.to_ref_or_slot_tolerant_ast(var)?,
896            };
897            match (op, &vardef.entity_type) {
898                (cst::RelOp::Eq, None) => Ok(PrincipalOrResourceConstraint::Eq(eref)),
899                (cst::RelOp::Eq, Some(_)) => Err(self.to_ast_err(ToASTErrorKind::IsWithEq)),
900                (cst::RelOp::In, None) => Ok(PrincipalOrResourceConstraint::In(eref)),
901                (cst::RelOp::In, Some(entity_type)) => {
902                    match entity_type
903                        .to_expr_or_special::<ast::ExprBuilder<()>>()?
904                        .into_entity_type()
905                    {
906                        Ok(et) => Ok(PrincipalOrResourceConstraint::IsIn(Arc::new(et), eref)),
907                        Err(eos) => Err(eos.to_ast_err(ToASTErrorKind::InvalidIsType {
908                            lhs: var.to_string(),
909                            rhs: eos
910                                .loc()
911                                .map(|loc| loc.snippet().unwrap_or(INVALID_SNIPPET))
912                                .unwrap_or(INVALID_SNIPPET)
913                                .to_string(),
914                        })),
915                    }
916                }
917                (cst::RelOp::InvalidSingleEq, _) => {
918                    Err(self.to_ast_err(ToASTErrorKind::InvalidSingleEq))
919                }
920                (op, _) => Err(self.to_ast_err(ToASTErrorKind::InvalidScopeOperator(*op))),
921            }
922        } else if let Some(entity_type) = &vardef.entity_type {
923            match entity_type
924                .to_expr_or_special::<ast::ExprBuilder<()>>()?
925                .into_entity_type()
926            {
927                Ok(et) => Ok(PrincipalOrResourceConstraint::Is(Arc::new(et))),
928                Err(eos) => Err(eos.to_ast_err(ToASTErrorKind::InvalidIsType {
929                    lhs: var.to_string(),
930                    rhs: eos
931                        .loc()
932                        .map(|loc| loc.snippet().unwrap_or(INVALID_SNIPPET))
933                        .unwrap_or(INVALID_SNIPPET)
934                        .to_string(),
935                })),
936            }
937        } else {
938            Ok(PrincipalOrResourceConstraint::Any)
939        }?;
940        match var {
941            ast::Var::Principal => Ok(PrincipalOrResource::Principal(PrincipalConstraint::new(c))),
942            ast::Var::Resource => Ok(PrincipalOrResource::Resource(ResourceConstraint::new(c))),
943            got => Err(self
944                .to_ast_err(ToASTErrorKind::IncorrectVariable { expected, got })
945                .into()),
946        }
947    }
948
949    fn to_action_constraint(
950        &self,
951        tolerant_setting: TolerantAstSetting,
952    ) -> Result<ast::ActionConstraint> {
953        let vardef = self.try_as_inner()?;
954
955        match vardef.variable.to_var() {
956            Ok(ast::Var::Action) => Ok(()),
957            Ok(got) => Err(self
958                .to_ast_err(ToASTErrorKind::IncorrectVariable {
959                    expected: ast::Var::Action,
960                    got,
961                })
962                .into()),
963            Err(errs) => Err(errs),
964        }?;
965
966        if let Some(typename) = vardef.unused_type_name.as_ref() {
967            typename.to_type_constraint::<ast::ExprBuilder<()>>()?;
968        }
969
970        if vardef.entity_type.is_some() {
971            return Err(self.to_ast_err(ToASTErrorKind::IsInActionScope).into());
972        }
973
974        if let Some((op, rel_expr)) = &vardef.ineq {
975            let action_constraint = match op {
976                cst::RelOp::In => {
977                    // special check for the syntax `_ in _ is _`
978                    if let Ok(expr) = rel_expr.to_expr::<ast::ExprBuilder<()>>() {
979                        if matches!(expr.expr_kind(), ast::ExprKind::Is { .. }) {
980                            return Err(self.to_ast_err(ToASTErrorKind::IsInActionScope).into());
981                        }
982                    }
983                    let one_or_multiple_refs = match tolerant_setting {
984                        TolerantAstSetting::NotTolerant => rel_expr.to_refs(ast::Var::Action)?,
985                        #[cfg(feature = "tolerant-ast")]
986                        TolerantAstSetting::Tolerant => {
987                            rel_expr.to_refs_tolerant_ast(ast::Var::Action)?
988                        }
989                    };
990                    match one_or_multiple_refs {
991                        OneOrMultipleRefs::Single(single_ref) => {
992                            Ok(ActionConstraint::is_in([single_ref]))
993                        }
994                        OneOrMultipleRefs::Multiple(refs) => Ok(ActionConstraint::is_in(refs)),
995                    }
996                }
997                cst::RelOp::Eq => {
998                    let single_ref = match tolerant_setting {
999                        TolerantAstSetting::NotTolerant => rel_expr.to_ref(ast::Var::Action)?,
1000                        #[cfg(feature = "tolerant-ast")]
1001                        TolerantAstSetting::Tolerant => {
1002                            rel_expr.to_ref_tolerant_ast(ast::Var::Action)?
1003                        }
1004                    };
1005                    Ok(ActionConstraint::is_eq(single_ref))
1006                }
1007                cst::RelOp::InvalidSingleEq => {
1008                    Err(self.to_ast_err(ToASTErrorKind::InvalidSingleEq))
1009                }
1010                op => Err(self.to_ast_err(ToASTErrorKind::InvalidActionScopeOperator(*op))),
1011            }?;
1012
1013            match tolerant_setting {
1014                TolerantAstSetting::NotTolerant => action_constraint
1015                    .contains_only_action_types()
1016                    .map_err(|non_action_euids| {
1017                        rel_expr
1018                            .to_ast_err(parse_errors::InvalidActionType {
1019                                euids: non_action_euids,
1020                            })
1021                            .into()
1022                    }),
1023                #[cfg(feature = "tolerant-ast")]
1024                TolerantAstSetting::Tolerant => {
1025                    let action_constraint_res = action_constraint.contains_only_action_types();
1026                    // With 'tolerant-ast' feature enabled, we store invalid action constraints as an ErrorConstraint
1027                    Ok(action_constraint_res.unwrap_or(ActionConstraint::ErrorConstraint))
1028                }
1029            }
1030        } else {
1031            Ok(ActionConstraint::Any)
1032        }
1033    }
1034}
1035
1036impl Node<Option<cst::Cond>> {
1037    /// to expr. Also returns, for informational purposes, a `bool` which is
1038    /// `true` if the cond is a `when` clause, `false` if it is an `unless`
1039    /// clause. (The returned `expr` is already adjusted for this, the `bool` is
1040    /// for information only.)
1041    fn to_expr<Build: ExprBuilderInfallibleBuild>(&self) -> Result<(Build::Expr, bool)> {
1042        let cond = self.try_as_inner()?;
1043        let is_when = cond.cond.to_cond_is_when()?;
1044
1045        let maybe_expr = match &cond.expr {
1046            Some(expr) => expr.to_expr::<Build>(),
1047            None => {
1048                let ident = match cond.cond.as_inner() {
1049                    Some(ident) => ident.clone(),
1050                    None => {
1051                        // `cond.cond.to_cond_is_when()` returned with `Ok`,
1052                        // so `cond.cond.as_inner()` must have been `Ok`
1053                        // inside that function call, making this unreachable.
1054                        if is_when {
1055                            cst::Ident::Ident("when".into())
1056                        } else {
1057                            cst::Ident::Ident("unless".into())
1058                        }
1059                    }
1060                };
1061                convert_expr_error_to_parse_error::<Build>(
1062                    self.to_ast_err(ToASTErrorKind::EmptyClause(Some(ident)))
1063                        .into(),
1064                    self.loc.as_ref(),
1065                )
1066            }
1067        };
1068
1069        maybe_expr.map(|e| {
1070            if is_when {
1071                (e, true)
1072            } else {
1073                (
1074                    Build::new().with_maybe_source_loc(self.loc.as_ref()).not(e),
1075                    false,
1076                )
1077            }
1078        })
1079    }
1080}
1081
1082impl Node<Option<cst::Str>> {
1083    pub(crate) fn as_valid_string(&self) -> Result<&SmolStr> {
1084        let id = self.try_as_inner()?;
1085
1086        match id {
1087            cst::Str::String(s) => Ok(s),
1088            // at time of comment, all strings are valid
1089            cst::Str::Invalid(s) => Err(self
1090                .to_ast_err(ToASTErrorKind::InvalidString(s.to_string()))
1091                .into()),
1092        }
1093    }
1094}
1095
1096#[cfg(feature = "tolerant-ast")]
1097fn build_ast_error_node_if_possible<Build: ExprBuilderInfallibleBuild>(
1098    error: ParseErrors,
1099    loc: Option<&Loc>,
1100) -> Result<Build::Expr> {
1101    let res = Build::new().with_maybe_source_loc(loc).error(error.clone());
1102    match res {
1103        Ok(r) => Ok(r),
1104        Err(_) => Err(error),
1105    }
1106}
1107
1108/// Since InfallibleExprBuilder ErrorType can be Infallible or ParseErrors, if we get an error from building the node pass the ParseErrors along
1109#[cfg_attr(
1110    not(feature = "tolerant-ast"),
1111    expect(
1112        unused_variables,
1113        reason = "loc argument unused unless feature is enabled"
1114    )
1115)]
1116fn convert_expr_error_to_parse_error<Build: ExprBuilderInfallibleBuild>(
1117    error: ParseErrors,
1118    loc: Option<&Loc>,
1119) -> Result<Build::Expr> {
1120    #[cfg(feature = "tolerant-ast")]
1121    return build_ast_error_node_if_possible::<Build>(error, loc);
1122    #[cfg(not(feature = "tolerant-ast"))]
1123    Err(error)
1124}
1125
1126/// Result type of conversion when we expect an Expr, Var, Name, or String.
1127///
1128/// During conversion it is useful to keep track of expression that may be used
1129/// as function names, record names, or record attributes. This prevents parsing these
1130/// terms to a general Expr expression and then immediately unwrapping them.
1131#[derive(Debug)]
1132pub(crate) enum ExprOrSpecial<'a, Expr> {
1133    /// Any expression except a variable, name, string literal, or boolean literal
1134    Expr { expr: Expr, loc: Option<Loc> },
1135    /// Variables, which act as expressions or names
1136    Var { var: ast::Var, loc: Option<Loc> },
1137    /// Name that isn't an expr and couldn't be converted to var
1138    Name { name: ast::Name, loc: Option<Loc> },
1139    /// String literal, not yet unescaped
1140    /// Must be processed with to_unescaped_string or to_pattern before inclusion in the AST
1141    StrLit { lit: &'a SmolStr, loc: Option<Loc> },
1142    /// A boolean literal
1143    BoolLit { val: bool, loc: Option<Loc> },
1144}
1145
1146impl<Expr> ExprOrSpecial<'_, Expr>
1147where
1148    Expr: std::fmt::Display,
1149{
1150    fn loc(&self) -> Option<&Loc> {
1151        match self {
1152            Self::Expr { loc, .. } => loc.as_ref(),
1153            Self::Var { loc, .. } => loc.as_ref(),
1154            Self::Name { loc, .. } => loc.as_ref(),
1155            Self::StrLit { loc, .. } => loc.as_ref(),
1156            Self::BoolLit { loc, .. } => loc.as_ref(),
1157        }
1158    }
1159
1160    fn to_ast_err(&self, kind: impl Into<ToASTErrorKind>) -> ToASTError {
1161        ToASTError::new(kind.into(), self.loc().cloned())
1162    }
1163
1164    fn into_expr<Build: ExprBuilderInfallibleBuild<Expr = Expr>>(self) -> Result<Expr> {
1165        match self {
1166            Self::Expr { expr, .. } => Ok(expr),
1167            Self::Var { var, loc } => Ok(Build::new().with_maybe_source_loc(loc.as_ref()).var(var)),
1168            Self::Name { name, loc } => convert_expr_error_to_parse_error::<Build>(
1169                ToASTError::new(
1170                    ToASTErrorKind::ArbitraryVariable(name.to_string().into()),
1171                    loc.clone(),
1172                )
1173                .into(),
1174                loc.as_ref(),
1175            ),
1176            Self::StrLit { lit, loc } => {
1177                match to_unescaped_string(lit) {
1178                    Ok(s) => Ok(Build::new().with_maybe_source_loc(loc.as_ref()).val(s)),
1179                    Err(escape_errs) => Err(ParseErrors::new_from_nonempty(escape_errs.map(|e| {
1180                        ToASTError::new(ToASTErrorKind::Unescape(e), loc.clone()).into()
1181                    }))),
1182                }
1183            }
1184            Self::BoolLit { val, loc } => {
1185                Ok(Build::new().with_maybe_source_loc(loc.as_ref()).val(val))
1186            }
1187        }
1188    }
1189
1190    /// Variables, names (with no prefixes), and string literals can all be used as record attributes
1191    pub(crate) fn into_valid_attr(self) -> Result<SmolStr> {
1192        match self {
1193            Self::Var { var, .. } => Ok(construct_string_from_var(var)),
1194            Self::Name { name, loc } => name.into_valid_attr(loc),
1195            Self::StrLit { lit, loc } => to_unescaped_string(lit).map_err(|escape_errs| {
1196                ParseErrors::new_from_nonempty(
1197                    escape_errs
1198                        .map(|e| ToASTError::new(ToASTErrorKind::Unescape(e), loc.clone()).into()),
1199                )
1200            }),
1201            Self::Expr { expr, loc } => Err(ToASTError::new(
1202                ToASTErrorKind::InvalidAttribute(expr.to_string().into()),
1203                loc,
1204            )
1205            .into()),
1206            Self::BoolLit { val, loc } => Err(ToASTError::new(
1207                ToASTErrorKind::ReservedIdentifier(if val {
1208                    cst::Ident::True
1209                } else {
1210                    cst::Ident::False
1211                }),
1212                loc,
1213            )
1214            .into()),
1215        }
1216    }
1217
1218    pub(crate) fn into_pattern(self) -> Result<Vec<PatternElem>> {
1219        match &self {
1220            Self::StrLit { lit, .. } => to_pattern(lit).map_err(|escape_errs| {
1221                ParseErrors::new_from_nonempty(
1222                    escape_errs.map(|e| self.to_ast_err(ToASTErrorKind::Unescape(e)).into()),
1223                )
1224            }),
1225            Self::Var { var, .. } => Err(self
1226                .to_ast_err(ToASTErrorKind::InvalidPattern(var.to_string()))
1227                .into()),
1228            Self::Name { name, .. } => Err(self
1229                .to_ast_err(ToASTErrorKind::InvalidPattern(name.to_string()))
1230                .into()),
1231            Self::Expr { expr, .. } => Err(self
1232                .to_ast_err(ToASTErrorKind::InvalidPattern(expr.to_string()))
1233                .into()),
1234            Self::BoolLit { val, .. } => Err(self
1235                .to_ast_err(ToASTErrorKind::InvalidPattern(val.to_string()))
1236                .into()),
1237        }
1238    }
1239    /// to string literal
1240    fn into_string_literal(self) -> Result<SmolStr> {
1241        match &self {
1242            Self::StrLit { lit, .. } => to_unescaped_string(lit).map_err(|escape_errs| {
1243                ParseErrors::new_from_nonempty(
1244                    escape_errs.map(|e| self.to_ast_err(ToASTErrorKind::Unescape(e)).into()),
1245                )
1246            }),
1247            Self::Var { var, .. } => Err(self
1248                .to_ast_err(ToASTErrorKind::InvalidString(var.to_string()))
1249                .into()),
1250            Self::Name { name, .. } => Err(self
1251                .to_ast_err(ToASTErrorKind::InvalidString(name.to_string()))
1252                .into()),
1253            Self::Expr { expr, .. } => Err(self
1254                .to_ast_err(ToASTErrorKind::InvalidString(expr.to_string()))
1255                .into()),
1256            Self::BoolLit { val, .. } => Err(self
1257                .to_ast_err(ToASTErrorKind::InvalidString(val.to_string()))
1258                .into()),
1259        }
1260    }
1261
1262    /// Returns `Err` if `self` is not an `ast::EntityType`. The `Err` will give you the `self` reference back
1263    fn into_entity_type(self) -> std::result::Result<ast::EntityType, Self> {
1264        self.into_name().map(ast::EntityType::from)
1265    }
1266
1267    /// Returns `Err` if `self` is not an `ast::Name`. The `Err` will give you the `self` reference back
1268    fn into_name(self) -> std::result::Result<ast::Name, Self> {
1269        match self {
1270            Self::Var { var, .. } => Ok(ast::Name::unqualified_name(var.into())),
1271            Self::Name { name, .. } => Ok(name),
1272            _ => Err(self),
1273        }
1274    }
1275}
1276
1277impl Node<Option<cst::Expr>> {
1278    /// convert `cst::Expr` to `ast::Expr`
1279    pub fn to_expr<Build: ExprBuilderInfallibleBuild>(&self) -> Result<Build::Expr> {
1280        self.to_expr_or_special::<Build>()?.into_expr::<Build>()
1281    }
1282    pub(crate) fn to_expr_or_special<Build: ExprBuilderInfallibleBuild>(
1283        &self,
1284    ) -> Result<ExprOrSpecial<'_, Build::Expr>> {
1285        let expr_opt = self.try_as_inner()?;
1286
1287        #[cfg_attr(
1288            not(feature = "tolerant-ast"),
1289            expect(
1290                clippy::infallible_destructuring_match,
1291                reason = "this is not a destructuring match when `tolerant-ast` is enabled"
1292            )
1293        )]
1294        let expr = match expr_opt {
1295            cst::Expr::Expr(expr_impl) => expr_impl,
1296            #[cfg(feature = "tolerant-ast")]
1297            cst::Expr::ErrorExpr => {
1298                let e = ToASTError::new(ToASTErrorKind::CSTErrorNode, self.loc.clone());
1299                return Ok(ExprOrSpecial::Expr {
1300                    expr: convert_expr_error_to_parse_error::<Build>(e.into(), self.loc.as_ref())?,
1301                    loc: self.loc.clone(),
1302                });
1303            }
1304        };
1305
1306        match &*expr.expr {
1307            cst::ExprData::Or(or) => or.to_expr_or_special::<Build>(),
1308            cst::ExprData::If(i, t, e) => {
1309                let maybe_guard = i.to_expr::<Build>();
1310                let maybe_then = t.to_expr::<Build>();
1311                let maybe_else = e.to_expr::<Build>();
1312
1313                let (i, t, e) = flatten_tuple_3(maybe_guard, maybe_then, maybe_else)?;
1314                Ok(ExprOrSpecial::Expr {
1315                    expr: Build::new()
1316                        .with_maybe_source_loc(self.loc.as_ref())
1317                        .ite(i, t, e),
1318                    loc: self.loc.clone(),
1319                })
1320            }
1321        }
1322    }
1323}
1324
1325impl Node<Option<cst::Or>> {
1326    fn to_expr_or_special<Build: ExprBuilderInfallibleBuild>(
1327        &self,
1328    ) -> Result<ExprOrSpecial<'_, Build::Expr>> {
1329        let or = self.try_as_inner()?;
1330
1331        let maybe_first = or.initial.to_expr_or_special::<Build>();
1332        let maybe_rest = ParseErrors::transpose(or.extended.iter().map(|i| i.to_expr::<Build>()));
1333
1334        let (first, rest) = flatten_tuple_2(maybe_first, maybe_rest)?;
1335        if rest.is_empty() {
1336            // This case is required so the "special" expression variants are
1337            // not converted into a plain `ExprOrSpecial::Expr`.
1338            Ok(first)
1339        } else {
1340            first.into_expr::<Build>().map(|first| ExprOrSpecial::Expr {
1341                expr: Build::new()
1342                    .with_maybe_source_loc(self.loc.as_ref())
1343                    .or_nary(first, rest),
1344                loc: self.loc.clone(),
1345            })
1346        }
1347    }
1348}
1349
1350impl Node<Option<cst::And>> {
1351    pub(crate) fn to_expr<Build: ExprBuilderInfallibleBuild>(&self) -> Result<Build::Expr> {
1352        self.to_expr_or_special::<Build>()?.into_expr::<Build>()
1353    }
1354    fn to_expr_or_special<Build: ExprBuilderInfallibleBuild>(
1355        &self,
1356    ) -> Result<ExprOrSpecial<'_, Build::Expr>> {
1357        let and = self.try_as_inner()?;
1358
1359        let maybe_first = and.initial.to_expr_or_special::<Build>();
1360        let maybe_rest = ParseErrors::transpose(and.extended.iter().map(|i| i.to_expr::<Build>()));
1361
1362        let (first, rest) = flatten_tuple_2(maybe_first, maybe_rest)?;
1363        if rest.is_empty() {
1364            // This case is required so the "special" expression variants are
1365            // not converted into a plain `ExprOrSpecial::Expr`.
1366            Ok(first)
1367        } else {
1368            first.into_expr::<Build>().map(|first| ExprOrSpecial::Expr {
1369                expr: Build::new()
1370                    .with_maybe_source_loc(self.loc.as_ref())
1371                    .and_naryl(first, rest),
1372                loc: self.loc.clone(),
1373            })
1374        }
1375    }
1376}
1377
1378impl Node<Option<cst::Relation>> {
1379    fn to_expr<Build: ExprBuilderInfallibleBuild>(&self) -> Result<Build::Expr> {
1380        self.to_expr_or_special::<Build>()?.into_expr::<Build>()
1381    }
1382    fn to_expr_or_special<Build: ExprBuilderInfallibleBuild>(
1383        &self,
1384    ) -> Result<ExprOrSpecial<'_, Build::Expr>> {
1385        let rel = self.try_as_inner()?;
1386
1387        match rel {
1388            cst::Relation::Common { initial, extended } => {
1389                let maybe_first = initial.to_expr_or_special::<Build>();
1390                let maybe_rest = ParseErrors::transpose(
1391                    extended
1392                        .iter()
1393                        .map(|(op, i)| i.to_expr::<Build>().map(|e| (op, e))),
1394                );
1395                let maybe_extra_elmts = if extended.len() > 1 {
1396                    Err(self.to_ast_err(ToASTErrorKind::AmbiguousOperators).into())
1397                } else {
1398                    Ok(())
1399                };
1400                let (first, rest, _) = flatten_tuple_3(maybe_first, maybe_rest, maybe_extra_elmts)?;
1401                let mut rest = rest.into_iter();
1402                let second = rest.next();
1403                match second {
1404                    None => Ok(first),
1405                    Some((&op, second)) => first.into_expr::<Build>().and_then(|first| {
1406                        Ok(ExprOrSpecial::Expr {
1407                            expr: construct_expr_rel::<Build>(first, op, second, self.loc.clone())?,
1408                            loc: self.loc.clone(),
1409                        })
1410                    }),
1411                }
1412            }
1413            cst::Relation::Has { target, field } => {
1414                let maybe_target = target.to_expr::<Build>();
1415                let maybe_fields = Ok(match field.to_has_rhs::<Build>()? {
1416                    Either::Left(s) => nonempty![s],
1417                    Either::Right(ids) => ids.map(|id| id.into_smolstr()),
1418                });
1419                let (target, fields) = flatten_tuple_2(maybe_target, maybe_fields)?;
1420                Ok(ExprOrSpecial::Expr {
1421                    expr: Build::new()
1422                        .with_maybe_source_loc(self.loc.as_ref())
1423                        .extended_has_attr(target, fields),
1424                    loc: self.loc.clone(),
1425                })
1426            }
1427            cst::Relation::Like { target, pattern } => {
1428                let maybe_target = target.to_expr::<Build>();
1429                let maybe_pattern = pattern.to_expr_or_special::<Build>()?.into_pattern();
1430                let (target, pattern) = flatten_tuple_2(maybe_target, maybe_pattern)?;
1431                Ok(ExprOrSpecial::Expr {
1432                    expr: Build::new()
1433                        .with_maybe_source_loc(self.loc.as_ref())
1434                        .like(target, pattern.into()),
1435                    loc: self.loc.clone(),
1436                })
1437            }
1438            cst::Relation::IsIn {
1439                target,
1440                entity_type,
1441                in_entity,
1442            } => {
1443                let maybe_target = target.to_expr::<Build>();
1444                let maybe_entity_type = entity_type
1445                    .to_expr_or_special::<Build>()?
1446                    .into_entity_type()
1447                    .map_err(|eos| {
1448                        eos.to_ast_err(ToASTErrorKind::InvalidIsType {
1449                            lhs: maybe_target
1450                                .as_ref()
1451                                .map(|expr| expr.to_string())
1452                                .unwrap_or_else(|_| "..".to_string()),
1453                            rhs: eos
1454                                .loc()
1455                                .map(|loc| loc.snippet().unwrap_or(INVALID_SNIPPET))
1456                                .unwrap_or(INVALID_SNIPPET)
1457                                .to_string(),
1458                        })
1459                        .into()
1460                    });
1461                let (t, n) = flatten_tuple_2(maybe_target, maybe_entity_type)?;
1462                match in_entity {
1463                    Some(in_entity) => {
1464                        let in_expr = in_entity.to_expr::<Build>()?;
1465                        Ok(ExprOrSpecial::Expr {
1466                            expr: Build::new()
1467                                .with_maybe_source_loc(self.loc.as_ref())
1468                                .is_in_entity_type(t, n, in_expr),
1469                            loc: self.loc.clone(),
1470                        })
1471                    }
1472                    None => Ok(ExprOrSpecial::Expr {
1473                        expr: Build::new()
1474                            .with_maybe_source_loc(self.loc.as_ref())
1475                            .is_entity_type(t, n),
1476                        loc: self.loc.clone(),
1477                    }),
1478                }
1479            }
1480        }
1481    }
1482}
1483
1484impl Node<Option<cst::Add>> {
1485    fn to_expr<Build: ExprBuilderInfallibleBuild>(&self) -> Result<Build::Expr> {
1486        self.to_expr_or_special::<Build>()?.into_expr::<Build>()
1487    }
1488
1489    // Peel the grammar onion until we see valid RHS
1490    // This function is added to implement RFC 62 (extended `has` operator).
1491    // We could modify existing code instead of having this function. However,
1492    // the former requires adding a weird variant to `ExprOrSpecial` to
1493    // accommodate a sequence of identifiers as RHS, which greatly complicates
1494    // the conversion from CSTs to `ExprOrSpecial`. Hence, this function is
1495    // added to directly tackle the CST to AST conversion for the has operator,
1496    // This design choice should be noninvasive to existing CST to AST logic,
1497    // despite producing deadcode.
1498    pub(crate) fn to_has_rhs<Build: ExprBuilderInfallibleBuild>(
1499        &self,
1500    ) -> Result<Either<SmolStr, NonEmpty<UnreservedId>>> {
1501        let inner @ cst::Add { initial, extended } = self.try_as_inner()?;
1502        let err = |loc| {
1503            ToASTError::new(ToASTErrorKind::InvalidHasRHS(inner.to_string().into()), loc).into()
1504        };
1505        let construct_attrs =
1506            |first, rest: &[Node<Option<cst::MemAccess>>]| -> Result<NonEmpty<UnreservedId>> {
1507                let mut acc = nonempty![first];
1508                rest.iter().try_for_each(|ma_node| {
1509                    let ma = ma_node.try_as_inner()?;
1510                    match ma {
1511                        cst::MemAccess::Field(id) => {
1512                            acc.push(id.to_unreserved_ident()?);
1513                            Ok(())
1514                        }
1515                        _ => Err(err(ma_node.loc.clone())),
1516                    }
1517                })?;
1518                Ok(acc)
1519            };
1520        if !extended.is_empty() {
1521            return Err(err(self.loc.clone()));
1522        }
1523        let cst::Mult { initial, extended } = initial.try_as_inner()?;
1524        if !extended.is_empty() {
1525            return Err(err(self.loc.clone()));
1526        }
1527        if let cst::Unary {
1528            op: None,
1529            item: item_node,
1530        } = initial.try_as_inner()?
1531        {
1532            let cst::Member { item, access } = item_node.try_as_inner()?;
1533            // Among successful conversion from `Primary` to `ExprOrSpecial`,
1534            // an `Ident` or `Str` becomes `ExprOrSpecial::StrLit`,
1535            // `ExprOrSpecial::Var`, and `ExprOrSpecial::Name`. Other
1536            // syntactical variants become `ExprOrSpecial::Expr`.
1537            match item.try_as_inner()? {
1538                cst::Primary::EList(_)
1539                | cst::Primary::Expr(_)
1540                | cst::Primary::RInits(_)
1541                | cst::Primary::Ref(_)
1542                | cst::Primary::Slot(_) => Err(err(item.loc.clone())),
1543                cst::Primary::Literal(_) | cst::Primary::Name(_) => {
1544                    let item = item.to_expr_or_special::<Build>()?;
1545                    match (item, access.as_slice()) {
1546                        (ExprOrSpecial::StrLit { lit, loc }, []) => Ok(Either::Left(
1547                            to_unescaped_string(lit).map_err(|escape_errs| {
1548                                ParseErrors::new_from_nonempty(escape_errs.map(|e| {
1549                                    ToASTError::new(ToASTErrorKind::Unescape(e), loc.clone()).into()
1550                                }))
1551                            })?,
1552                        )),
1553                        (ExprOrSpecial::Var { var, .. }, rest) => {
1554                            #[expect(
1555                                clippy::unwrap_used,
1556                                reason = "any variable should be a valid identifier"
1557                            )]
1558                            let first = construct_string_from_var(var).parse().unwrap();
1559                            Ok(Either::Right(construct_attrs(first, rest)?))
1560                        }
1561                        (ExprOrSpecial::Name { name, loc }, rest) => {
1562                            if name.is_unqualified() {
1563                                let first = name.basename();
1564
1565                                Ok(Either::Right(construct_attrs(first, rest)?))
1566                            } else {
1567                                Err(ToASTError::new(
1568                                    ToASTErrorKind::PathAsAttribute(inner.to_string()),
1569                                    loc,
1570                                )
1571                                .into())
1572                            }
1573                        }
1574                        // Attempt to return a precise error message for RHS like `true.<...>` and `false.<...>`
1575                        (ExprOrSpecial::BoolLit { val, loc }, _) => Err(ToASTError::new(
1576                            ToASTErrorKind::ReservedIdentifier(if val {
1577                                cst::Ident::True
1578                            } else {
1579                                cst::Ident::False
1580                            }),
1581                            loc,
1582                        )
1583                        .into()),
1584                        (ExprOrSpecial::Expr { loc, .. }, _) => Err(err(loc)),
1585                        _ => Err(err(self.loc.clone())),
1586                    }
1587                }
1588            }
1589        } else {
1590            Err(err(self.loc.clone()))
1591        }
1592    }
1593
1594    pub(crate) fn to_expr_or_special<Build: ExprBuilderInfallibleBuild>(
1595        &self,
1596    ) -> Result<ExprOrSpecial<'_, Build::Expr>> {
1597        let add = self.try_as_inner()?;
1598
1599        let maybe_first = add.initial.to_expr_or_special::<Build>();
1600        let maybe_rest = ParseErrors::transpose(
1601            add.extended
1602                .iter()
1603                .map(|&(op, ref i)| i.to_expr::<Build>().map(|e| (op, e))),
1604        );
1605        let (first, rest) = flatten_tuple_2(maybe_first, maybe_rest)?;
1606        if !rest.is_empty() {
1607            // in this case, `first` must be an expr, we should check for errors there as well
1608            let first = first.into_expr::<Build>()?;
1609            Ok(ExprOrSpecial::Expr {
1610                expr: Build::new()
1611                    .with_maybe_source_loc(self.loc.as_ref())
1612                    .add_nary(first, rest),
1613                loc: self.loc.clone(),
1614            })
1615        } else {
1616            Ok(first)
1617        }
1618    }
1619}
1620
1621impl Node<Option<cst::Mult>> {
1622    fn to_expr<Build: ExprBuilderInfallibleBuild>(&self) -> Result<Build::Expr> {
1623        self.to_expr_or_special::<Build>()?.into_expr::<Build>()
1624    }
1625    fn to_expr_or_special<Build: ExprBuilderInfallibleBuild>(
1626        &self,
1627    ) -> Result<ExprOrSpecial<'_, Build::Expr>> {
1628        let mult = self.try_as_inner()?;
1629
1630        let maybe_first = mult.initial.to_expr_or_special::<Build>();
1631        let maybe_rest = ParseErrors::transpose(mult.extended.iter().map(|&(op, ref i)| {
1632            i.to_expr::<Build>().and_then(|e| match op {
1633                cst::MultOp::Times => Ok(e),
1634                cst::MultOp::Divide => {
1635                    Err(self.to_ast_err(ToASTErrorKind::UnsupportedDivision).into())
1636                }
1637                cst::MultOp::Mod => Err(self.to_ast_err(ToASTErrorKind::UnsupportedModulo).into()),
1638            })
1639        }));
1640
1641        let (first, rest) = flatten_tuple_2(maybe_first, maybe_rest)?;
1642        if !rest.is_empty() {
1643            // in this case, `first` must be an expr, we should check for errors there as well
1644            let first = first.into_expr::<Build>()?;
1645            Ok(ExprOrSpecial::Expr {
1646                expr: Build::new()
1647                    .with_maybe_source_loc(self.loc.as_ref())
1648                    .mul_nary(first, rest),
1649                loc: self.loc.clone(),
1650            })
1651        } else {
1652            Ok(first)
1653        }
1654    }
1655}
1656
1657impl Node<Option<cst::Unary>> {
1658    fn to_expr<Build: ExprBuilderInfallibleBuild>(&self) -> Result<Build::Expr> {
1659        self.to_expr_or_special::<Build>()?.into_expr::<Build>()
1660    }
1661    fn to_expr_or_special<Build: ExprBuilderInfallibleBuild>(
1662        &self,
1663    ) -> Result<ExprOrSpecial<'_, Build::Expr>> {
1664        let unary = self.try_as_inner()?;
1665
1666        match unary.op {
1667            None => unary.item.to_expr_or_special::<Build>(),
1668            Some(cst::NegOp::Bang(n)) => {
1669                (0..n).fold(unary.item.to_expr_or_special::<Build>(), |inner, _| {
1670                    inner
1671                        .and_then(|e| e.into_expr::<Build>())
1672                        .map(|expr| ExprOrSpecial::Expr {
1673                            expr: Build::new()
1674                                .with_maybe_source_loc(self.loc.as_ref())
1675                                .not(expr),
1676                            loc: self.loc.clone(),
1677                        })
1678                })
1679            }
1680            Some(cst::NegOp::Dash(0)) => unary.item.to_expr_or_special::<Build>(),
1681            Some(cst::NegOp::Dash(c)) => {
1682                // Test if there is a negative numeric literal.
1683                // A negative numeric literal should match regex pattern
1684                // `-\d+` which is parsed into a `Unary(_, Member(Primary(Literal(Num(_))), []))`.
1685                // Given a successful match, the number of negation operations
1686                // decreases by one.
1687                let (last, rc) = if let Some(cst::Literal::Num(n)) = unary.item.to_lit() {
1688                    match n.cmp(&(i64::MAX as u64 + 1)) {
1689                        Ordering::Equal => (
1690                            Ok(Build::new()
1691                                .with_maybe_source_loc(unary.item.loc.as_ref())
1692                                .val(i64::MIN)),
1693                            c - 1,
1694                        ),
1695                        Ordering::Less => (
1696                            Ok(Build::new()
1697                                .with_maybe_source_loc(unary.item.loc.as_ref())
1698                                .val(-(*n as i64))),
1699                            c - 1,
1700                        ),
1701                        Ordering::Greater => (
1702                            Err(self
1703                                .to_ast_err(ToASTErrorKind::IntegerLiteralTooLarge(*n))
1704                                .into()),
1705                            0,
1706                        ),
1707                    }
1708                } else {
1709                    // If the operand is not a CST literal, convert it into
1710                    // an expression.
1711                    (
1712                        unary
1713                            .item
1714                            .to_expr_or_special::<Build>()
1715                            .and_then(|i| i.into_expr::<Build>()),
1716                        c,
1717                    )
1718                };
1719                // Fold the expression into a series of negation operations.
1720                (0..rc)
1721                    .fold(last, |r, _| {
1722                        r.map(|e| Build::new().with_maybe_source_loc(self.loc.as_ref()).neg(e))
1723                    })
1724                    .map(|expr| ExprOrSpecial::Expr {
1725                        expr,
1726                        loc: self.loc.clone(),
1727                    })
1728            }
1729            Some(cst::NegOp::OverBang) => Err(self
1730                .to_ast_err(ToASTErrorKind::UnaryOpLimit(ast::UnaryOp::Not))
1731                .into()),
1732            Some(cst::NegOp::OverDash) => Err(self
1733                .to_ast_err(ToASTErrorKind::UnaryOpLimit(ast::UnaryOp::Neg))
1734                .into()),
1735        }
1736    }
1737}
1738
1739/// Temporary converted data, mirroring `cst::MemAccess`
1740enum AstAccessor<Expr> {
1741    Field(ast::UnreservedId),
1742    Call(Vec<Expr>),
1743    Index(SmolStr),
1744}
1745
1746impl Node<Option<cst::Member>> {
1747    /// Try to convert `cst::Member` into a `cst::Literal`, i.e.
1748    /// match `Member(Primary(Literal(_), []))`.
1749    /// It does not match the `Expr` arm of `Primary`, which means expressions
1750    /// like `(1)` are not considered as literals on the CST level.
1751    pub fn to_lit(&self) -> Option<&cst::Literal> {
1752        let m = self.as_ref().node.as_ref()?;
1753        if !m.access.is_empty() {
1754            return None;
1755        }
1756        match m.item.as_ref().node.as_ref()? {
1757            cst::Primary::Literal(lit) => lit.as_ref().node.as_ref(),
1758            _ => None,
1759        }
1760    }
1761
1762    /// Construct an attribute access or method call on an expression. This also
1763    /// handles function calls, but a function call of an arbitrary expression
1764    /// is always an error.
1765    ///
1766    /// The input `head` is an arbitrary expression, while `next` and `tail` are
1767    /// togther a non-empty list of accesses applied to that expression.
1768    ///
1769    /// Returns a tuple where the first element is the expression built for the
1770    /// `next` access applied to `head`, and the second element is the new tail of
1771    /// acessors. In most cases, `tail` is returned unmodified, but in the method
1772    /// call case we need to pull off the `Call` element containing the arguments.
1773    #[expect(clippy::type_complexity, reason = "judged readable enough")]
1774    fn build_expr_accessor<'a, Build: ExprBuilderInfallibleBuild>(
1775        &self,
1776        head: Build::Expr,
1777        next: &mut AstAccessor<Build::Expr>,
1778        tail: &'a mut [AstAccessor<Build::Expr>],
1779    ) -> Result<(Build::Expr, &'a mut [AstAccessor<Build::Expr>])> {
1780        use AstAccessor::*;
1781        match (next, tail) {
1782            // trying to "call" an expression as a function like `(1 + 1)("foo")`. Always an error.
1783            (Call(_), _) => Err(self.to_ast_err(ToASTErrorKind::ExpressionCall).into()),
1784
1785            // method call on arbitrary expression like `[].contains(1)`
1786            (Field(id), [Call(args), rest @ ..]) => {
1787                // move the expr and args out of the slice
1788                let args = std::mem::take(args);
1789                // move the id out of the slice as well, to avoid cloning the internal string
1790                let id = mem::replace(id, ast::UnreservedId::empty());
1791                Ok((id.to_meth::<Build>(head, args, self.loc.as_ref())?, rest))
1792            }
1793
1794            // field of arbitrary expr like `(principal.foo).bar`
1795            (Field(id), rest) => {
1796                let id = mem::replace(id, ast::UnreservedId::empty());
1797                Ok((
1798                    Build::new()
1799                        .with_maybe_source_loc(self.loc.as_ref())
1800                        .get_attr(head, id.into_smolstr()),
1801                    rest,
1802                ))
1803            }
1804
1805            // index into arbitrary expr like `(principal.foo)["bar"]`
1806            (Index(i), rest) => {
1807                let i = mem::take(i);
1808                Ok((
1809                    Build::new()
1810                        .with_maybe_source_loc(self.loc.as_ref())
1811                        .get_attr(head, i),
1812                    rest,
1813                ))
1814            }
1815        }
1816    }
1817
1818    fn to_expr_or_special<Build: ExprBuilderInfallibleBuild>(
1819        &self,
1820    ) -> Result<ExprOrSpecial<'_, Build::Expr>> {
1821        let mem = self.try_as_inner()?;
1822
1823        let maybe_prim = mem.item.to_expr_or_special::<Build>();
1824        let maybe_accessors =
1825            ParseErrors::transpose(mem.access.iter().map(|a| a.to_access::<Build>()));
1826
1827        // Return errors in case parsing failed for any element
1828        let (prim, mut accessors) = flatten_tuple_2(maybe_prim, maybe_accessors)?;
1829
1830        let (mut head, mut tail) = {
1831            use AstAccessor::*;
1832            use ExprOrSpecial::*;
1833            match (prim, accessors.as_mut_slice()) {
1834                // no accessors, return head immediately.
1835                (prim, []) => return Ok(prim),
1836
1837                // Any access on an arbitrary expression (or string or boolean
1838                // literal). We will handle the possibility of multiple chained
1839                // accesses on this expression in the loop at the end of this
1840                // function.
1841                (prim @ (Expr { .. } | StrLit { .. } | BoolLit { .. }), [next, rest @ ..]) => {
1842                    self.build_expr_accessor::<Build>(prim.into_expr::<Build>()?, next, rest)?
1843                }
1844
1845                // function call
1846                (Name { name, .. }, [Call(args), rest @ ..]) => {
1847                    // move the vec out of the slice, we won't use the slice after
1848                    let args = std::mem::take(args);
1849                    (name.into_func::<Build>(args, self.loc.clone())?, rest)
1850                }
1851                // variable function call - error
1852                (Var { var, .. }, [Call(_), ..]) => {
1853                    return Err(self.to_ast_err(ToASTErrorKind::VariableCall(var)).into());
1854                }
1855
1856                // method call on name - error
1857                (Name { name, .. }, [Field(f), Call(_), ..]) => {
1858                    return Err(self
1859                        .to_ast_err(ToASTErrorKind::NoMethods(name, f.clone()))
1860                        .into());
1861                }
1862                // method call on variable
1863                (Var { var, loc: var_loc }, [Field(id), Call(args), rest @ ..]) => {
1864                    let args = std::mem::take(args);
1865                    // move the id out of the slice as well, to avoid cloning the internal string
1866                    let id = mem::replace(id, ast::UnreservedId::empty());
1867                    (
1868                        id.to_meth::<Build>(
1869                            Build::new()
1870                                .with_maybe_source_loc(var_loc.as_ref())
1871                                .var(var),
1872                            args,
1873                            self.loc.as_ref(),
1874                        )?,
1875                        rest,
1876                    )
1877                }
1878
1879                // attribute access on a variable
1880                (Var { var, loc: var_loc }, [Field(i), rest @ ..]) => {
1881                    let id = mem::replace(i, ast::UnreservedId::empty());
1882                    (
1883                        Build::new()
1884                            .with_maybe_source_loc(self.loc.as_ref())
1885                            .get_attr(
1886                                Build::new()
1887                                    .with_maybe_source_loc(var_loc.as_ref())
1888                                    .var(var),
1889                                id.into_smolstr(),
1890                            ),
1891                        rest,
1892                    )
1893                }
1894                // attribute access on an arbitrary name - error
1895                (Name { name, .. }, [Field(f), ..]) => {
1896                    return Err(self
1897                        .to_ast_err(ToASTErrorKind::InvalidAccess {
1898                            lhs: name,
1899                            field: f.clone().into_smolstr(),
1900                        })
1901                        .into());
1902                }
1903                // index style attribute access on an arbitrary name - error
1904                (Name { name, .. }, [Index(i), ..]) => {
1905                    return Err(self
1906                        .to_ast_err(ToASTErrorKind::InvalidIndex {
1907                            lhs: name,
1908                            field: i.clone(),
1909                        })
1910                        .into());
1911                }
1912
1913                // index style attribute access on a variable
1914                (Var { var, loc: var_loc }, [Index(i), rest @ ..]) => {
1915                    let i = mem::take(i);
1916                    (
1917                        Build::new()
1918                            .with_maybe_source_loc(self.loc.as_ref())
1919                            .get_attr(
1920                                Build::new()
1921                                    .with_maybe_source_loc(var_loc.as_ref())
1922                                    .var(var),
1923                                i,
1924                            ),
1925                        rest,
1926                    )
1927                }
1928            }
1929        };
1930
1931        // After processing the first element, we know that `head` is always an
1932        // expression, so we repeatedly apply `build_expr_access` on head
1933        // without need to consider the other cases until we've consumed the
1934        // list of accesses.
1935        while let [next, rest @ ..] = tail {
1936            (head, tail) = self.build_expr_accessor::<Build>(head, next, rest)?;
1937        }
1938        Ok(ExprOrSpecial::Expr {
1939            expr: head,
1940            loc: self.loc.clone(),
1941        })
1942    }
1943}
1944
1945impl Node<Option<cst::MemAccess>> {
1946    fn to_access<Build: ExprBuilderInfallibleBuild>(&self) -> Result<AstAccessor<Build::Expr>> {
1947        let acc = self.try_as_inner()?;
1948
1949        match acc {
1950            cst::MemAccess::Field(i) => {
1951                let maybe_ident = i.to_unreserved_ident();
1952                maybe_ident.map(AstAccessor::Field)
1953            }
1954            cst::MemAccess::Call(args) => {
1955                let maybe_args = ParseErrors::transpose(args.iter().map(|e| e.to_expr::<Build>()));
1956                maybe_args.map(AstAccessor::Call)
1957            }
1958            cst::MemAccess::Index(index) => {
1959                let maybe_index = index.to_expr_or_special::<Build>()?.into_string_literal();
1960                maybe_index.map(AstAccessor::Index)
1961            }
1962        }
1963    }
1964}
1965
1966impl Node<Option<cst::Primary>> {
1967    pub(crate) fn to_expr<Build: ExprBuilderInfallibleBuild>(&self) -> Result<Build::Expr> {
1968        self.to_expr_or_special::<Build>()?.into_expr::<Build>()
1969    }
1970    fn to_expr_or_special<Build: ExprBuilderInfallibleBuild>(
1971        &self,
1972    ) -> Result<ExprOrSpecial<'_, Build::Expr>> {
1973        let prim = self.try_as_inner()?;
1974
1975        match prim {
1976            cst::Primary::Literal(lit) => lit.to_expr_or_special::<Build>(),
1977            cst::Primary::Ref(r) => r.to_expr::<Build>().map(|expr| ExprOrSpecial::Expr {
1978                expr,
1979                loc: r.loc.clone(),
1980            }),
1981            cst::Primary::Slot(s) => {
1982                s.clone()
1983                    .into_expr::<Build>()
1984                    .map(|expr| ExprOrSpecial::Expr {
1985                        expr,
1986                        loc: s.loc.clone(),
1987                    })
1988            }
1989            cst::Primary::Name(n) => {
1990                // ignore errors in the case where `n` isn't a var - we'll get them elsewhere
1991                if let Some(var) = n.maybe_to_var() {
1992                    Ok(ExprOrSpecial::Var {
1993                        var,
1994                        loc: self.loc.clone(),
1995                    })
1996                } else {
1997                    n.to_internal_name().and_then(|name| match name.try_into() {
1998                        Ok(name) => Ok(ExprOrSpecial::Name {
1999                            name,
2000                            loc: self.loc.clone(),
2001                        }),
2002                        Err(err) => Err(ParseErrors::singleton(err)),
2003                    })
2004                }
2005            }
2006            cst::Primary::Expr(e) => e.to_expr::<Build>().map(|expr| ExprOrSpecial::Expr {
2007                expr,
2008                loc: e.loc.clone(),
2009            }),
2010            cst::Primary::EList(es) => {
2011                let maybe_list = ParseErrors::transpose(es.iter().map(|e| e.to_expr::<Build>()));
2012                maybe_list.map(|list| ExprOrSpecial::Expr {
2013                    expr: Build::new()
2014                        .with_maybe_source_loc(self.loc.as_ref())
2015                        .set(list),
2016                    loc: self.loc.clone(),
2017                })
2018            }
2019            cst::Primary::RInits(is) => {
2020                let rec = ParseErrors::transpose(is.iter().map(|i| i.to_init::<Build>()))?;
2021                let expr = Build::new()
2022                    .with_maybe_source_loc(self.loc.as_ref())
2023                    .record(rec)
2024                    .map_err(|e| {
2025                        Into::<ParseErrors>::into(ToASTError::new(e.into(), self.loc.clone()))
2026                    })?;
2027                Ok(ExprOrSpecial::Expr {
2028                    expr,
2029                    loc: self.loc.clone(),
2030                })
2031            }
2032        }
2033    }
2034
2035    /// convert `cst::Primary` representing a string literal to a `SmolStr`.
2036    pub fn to_string_literal<Build: ExprBuilderInfallibleBuild>(&self) -> Result<SmolStr> {
2037        let prim = self.try_as_inner()?;
2038
2039        match prim {
2040            cst::Primary::Literal(lit) => lit.to_expr_or_special::<Build>()?.into_string_literal(),
2041            _ => Err(self
2042                .to_ast_err(ToASTErrorKind::InvalidString(prim.to_string()))
2043                .into()),
2044        }
2045    }
2046}
2047
2048impl Node<Option<cst::Slot>> {
2049    fn into_expr<Build: ExprBuilderInfallibleBuild>(self) -> Result<Build::Expr> {
2050        match self.try_as_inner()?.try_into() {
2051            Ok(slot_id) => Ok(Build::new()
2052                .with_maybe_source_loc(self.loc.as_ref())
2053                .slot(slot_id)),
2054            Err(e) => Err(self.to_ast_err(e).into()),
2055        }
2056    }
2057}
2058
2059impl TryFrom<&cst::Slot> for ast::SlotId {
2060    type Error = ToASTErrorKind;
2061
2062    fn try_from(slot: &cst::Slot) -> std::result::Result<Self, Self::Error> {
2063        match slot {
2064            cst::Slot::Principal => Ok(ast::SlotId::principal()),
2065            cst::Slot::Resource => Ok(ast::SlotId::resource()),
2066            cst::Slot::Other(slot) => Err(ToASTErrorKind::InvalidSlot(slot.clone())),
2067        }
2068    }
2069}
2070
2071impl From<ast::SlotId> for cst::Slot {
2072    fn from(slot: ast::SlotId) -> cst::Slot {
2073        match slot {
2074            ast::SlotId(ast::ValidSlotId::Principal) => cst::Slot::Principal,
2075            ast::SlotId(ast::ValidSlotId::Resource) => cst::Slot::Resource,
2076        }
2077    }
2078}
2079
2080impl Node<Option<cst::Name>> {
2081    /// Build type constraints
2082    fn to_type_constraint<Build: ExprBuilderInfallibleBuild>(&self) -> Result<Build::Expr> {
2083        match self.as_inner() {
2084            Some(_) => Err(self.to_ast_err(ToASTErrorKind::TypeConstraints).into()),
2085            None => Ok(Build::new()
2086                .with_maybe_source_loc(self.loc.as_ref())
2087                .val(true)),
2088        }
2089    }
2090
2091    pub(crate) fn to_name(&self) -> Result<ast::Name> {
2092        self.to_internal_name()
2093            .and_then(|n| n.try_into().map_err(ParseErrors::singleton))
2094    }
2095
2096    pub(crate) fn to_internal_name(&self) -> Result<ast::InternalName> {
2097        let name = self.try_as_inner()?;
2098
2099        let maybe_path = ParseErrors::transpose(name.path.iter().map(|i| i.to_valid_ident()));
2100        let maybe_name = name.name.to_valid_ident();
2101
2102        // computation and error generation is complete, so fail or construct
2103        let (name, path) = flatten_tuple_2(maybe_name, maybe_path)?;
2104        Ok(construct_name(path, name, self.loc.clone()))
2105    }
2106
2107    // Errors from this function are ignored (because they are detected elsewhere)
2108    // so it's fine to return an `Option` instead of a `Result`.
2109    fn maybe_to_var(&self) -> Option<ast::Var> {
2110        let name = self.as_inner()?;
2111        let ident = if name.path.is_empty() {
2112            name.name.as_inner()
2113        } else {
2114            // The path should be empty for a variable
2115            None
2116        }?;
2117
2118        match ident {
2119            cst::Ident::Principal => Some(ast::Var::Principal),
2120            cst::Ident::Action => Some(ast::Var::Action),
2121            cst::Ident::Resource => Some(ast::Var::Resource),
2122            cst::Ident::Context => Some(ast::Var::Context),
2123            _ => None,
2124        }
2125    }
2126}
2127
2128impl ast::Name {
2129    /// Convert the `Name` into a `String` attribute, which fails if it had any namespaces
2130    fn into_valid_attr(self, loc: Option<Loc>) -> Result<SmolStr> {
2131        if !self.0.path.is_empty() {
2132            Err(ToASTError::new(ToASTErrorKind::PathAsAttribute(self.to_string()), loc).into())
2133        } else {
2134            Ok(self.0.id.into_smolstr())
2135        }
2136    }
2137
2138    fn into_func<Build: ExprBuilderInfallibleBuild>(
2139        self,
2140        args: Vec<Build::Expr>,
2141        loc: Option<Loc>,
2142    ) -> Result<Build::Expr> {
2143        // error on standard methods
2144        if self.0.path.is_empty() {
2145            let id = self.basename();
2146            if ExtStyles::is_method(&id)
2147                || matches!(
2148                    id.as_ref(),
2149                    "contains" | "containsAll" | "containsAny" | "isEmpty" | "getTag" | "hasTag"
2150                )
2151            {
2152                return Err(ToASTError::new(
2153                    ToASTErrorKind::FunctionCallOnMethod(self.basename()),
2154                    loc,
2155                )
2156                .into());
2157            }
2158        }
2159        if ExtStyles::is_function(&self) {
2160            Ok(Build::new()
2161                .with_maybe_source_loc(loc.as_ref())
2162                .call_extension_fn(self, args)
2163                .unwrap_infallible())
2164        } else {
2165            let hint = ExtStyles::suggest_function(&self);
2166            Err(ToASTError::new(ToASTErrorKind::UnknownFunction { id: self, hint }, loc).into())
2167        }
2168    }
2169}
2170
2171impl Node<Option<cst::Ref>> {
2172    /// convert `cst::Ref` to `ast::EntityUID`
2173    pub fn to_ref(&self) -> Result<ast::EntityUID> {
2174        let refr = self.try_as_inner()?;
2175
2176        match refr {
2177            cst::Ref::Uid { path, eid } => {
2178                let maybe_path = path.to_name().map(ast::EntityType::from);
2179                let maybe_eid = eid.as_valid_string().and_then(|s| {
2180                    to_unescaped_string(s).map_err(|escape_errs| {
2181                        ParseErrors::new_from_nonempty(
2182                            escape_errs
2183                                .map(|e| self.to_ast_err(ToASTErrorKind::Unescape(e)).into()),
2184                        )
2185                    })
2186                });
2187
2188                let (p, e) = flatten_tuple_2(maybe_path, maybe_eid)?;
2189                Ok({
2190                    let loc = self.loc.clone();
2191                    ast::EntityUID::from_components(p, ast::Eid::new(e), loc)
2192                })
2193            }
2194            r @ cst::Ref::Ref { .. } => Err(self
2195                .to_ast_err(ToASTErrorKind::InvalidEntityLiteral(r.to_string()))
2196                .into()),
2197        }
2198    }
2199    fn to_expr<Build: ExprBuilderInfallibleBuild>(&self) -> Result<Build::Expr> {
2200        self.to_ref().map(|euid| {
2201            Build::new()
2202                .with_maybe_source_loc(self.loc.as_ref())
2203                .val(euid)
2204        })
2205    }
2206}
2207
2208impl Node<Option<cst::Literal>> {
2209    fn to_expr_or_special<Build: ExprBuilderInfallibleBuild>(
2210        &self,
2211    ) -> Result<ExprOrSpecial<'_, Build::Expr>> {
2212        let lit = self.try_as_inner()?;
2213
2214        match lit {
2215            cst::Literal::True => Ok(ExprOrSpecial::BoolLit {
2216                val: true,
2217                loc: self.loc.clone(),
2218            }),
2219            cst::Literal::False => Ok(ExprOrSpecial::BoolLit {
2220                val: false,
2221                loc: self.loc.clone(),
2222            }),
2223            cst::Literal::Num(n) => match Integer::try_from(*n) {
2224                Ok(i) => Ok(ExprOrSpecial::Expr {
2225                    expr: Build::new().with_maybe_source_loc(self.loc.as_ref()).val(i),
2226                    loc: self.loc.clone(),
2227                }),
2228                Err(_) => Err(self
2229                    .to_ast_err(ToASTErrorKind::IntegerLiteralTooLarge(*n))
2230                    .into()),
2231            },
2232            cst::Literal::Str(s) => {
2233                let maybe_str = s.as_valid_string();
2234                maybe_str.map(|lit| ExprOrSpecial::StrLit {
2235                    lit,
2236                    loc: self.loc.clone(),
2237                })
2238            }
2239        }
2240    }
2241}
2242
2243impl Node<Option<cst::RecInit>> {
2244    fn to_init<Build: ExprBuilderInfallibleBuild>(&self) -> Result<(SmolStr, Build::Expr)> {
2245        let lit = self.try_as_inner()?;
2246
2247        let maybe_attr = lit.0.to_expr_or_special::<Build>()?.into_valid_attr();
2248        let maybe_value = lit.1.to_expr::<Build>();
2249
2250        flatten_tuple_2(maybe_attr, maybe_value)
2251    }
2252}
2253
2254/// This section (construct_*) exists to handle differences between standard ast constructors and
2255/// the needs or conveniences here. Especially concerning source location data.
2256#[expect(
2257    clippy::too_many_arguments,
2258    reason = "policies just have this many components"
2259)]
2260fn construct_template_policy(
2261    id: ast::PolicyID,
2262    annotations: ast::Annotations,
2263    effect: ast::Effect,
2264    principal: ast::PrincipalConstraint,
2265    action: ast::ActionConstraint,
2266    resource: ast::ResourceConstraint,
2267    conds: Vec<ast::Expr>,
2268    loc: Option<&Loc>,
2269) -> ast::Template {
2270    let construct_template = |non_scope_constraint| {
2271        ast::Template::new(
2272            id,
2273            loc.cloned(),
2274            annotations,
2275            effect,
2276            principal,
2277            action,
2278            resource,
2279            non_scope_constraint,
2280        )
2281    };
2282
2283    // a right fold of conditions
2284    // e.g., [c1, c2, c3,] --> c1 && (c2 && c3)
2285    let mut conds_rev_iter = conds.into_iter().rev();
2286    if let Some(last_expr) = conds_rev_iter.next() {
2287        let builder = ast::ExprBuilder::new().with_maybe_source_loc(loc);
2288        construct_template(Some(
2289            conds_rev_iter.fold(last_expr, |acc, prev| builder.clone().and(prev, acc)),
2290        ))
2291    } else {
2292        construct_template(None)
2293    }
2294}
2295fn construct_string_from_var(v: ast::Var) -> SmolStr {
2296    match v {
2297        ast::Var::Principal => "principal".into(),
2298        ast::Var::Action => "action".into(),
2299        ast::Var::Resource => "resource".into(),
2300        ast::Var::Context => "context".into(),
2301    }
2302}
2303fn construct_name(path: Vec<ast::Id>, id: ast::Id, loc: Option<Loc>) -> ast::InternalName {
2304    ast::InternalName {
2305        id,
2306        path: Arc::new(path),
2307        loc,
2308    }
2309}
2310
2311fn construct_expr_rel<Build: ExprBuilderInfallibleBuild>(
2312    f: Build::Expr,
2313    rel: cst::RelOp,
2314    s: Build::Expr,
2315    loc: Option<Loc>,
2316) -> Result<Build::Expr> {
2317    let builder = Build::new().with_maybe_source_loc(loc.as_ref());
2318    match rel {
2319        cst::RelOp::Less => Ok(builder.less(f, s)),
2320        cst::RelOp::LessEq => Ok(builder.lesseq(f, s)),
2321        cst::RelOp::GreaterEq => Ok(builder.greatereq(f, s)),
2322        cst::RelOp::Greater => Ok(builder.greater(f, s)),
2323        cst::RelOp::NotEq => Ok(builder.noteq(f, s)),
2324        cst::RelOp::Eq => Ok(builder.is_eq(f, s)),
2325        cst::RelOp::In => Ok(builder.is_in(f, s)),
2326        cst::RelOp::InvalidSingleEq => {
2327            Err(ToASTError::new(ToASTErrorKind::InvalidSingleEq, loc).into())
2328        }
2329    }
2330}
2331
2332#[expect(
2333    clippy::panic,
2334    clippy::indexing_slicing,
2335    clippy::cognitive_complexity,
2336    reason = "Unit Test Code"
2337)]
2338#[cfg(test)]
2339mod tests {
2340    use super::*;
2341    use crate::{
2342        ast::{EntityUID, Expr},
2343        parser::{err::ParseErrors, test_utils::*, *},
2344        test_utils::*,
2345    };
2346    use ast::{InternalName, ReservedNameError};
2347    use cool_asserts::assert_matches;
2348
2349    #[track_caller]
2350    fn assert_parse_expr_succeeds(text: &str) -> Expr {
2351        text_to_cst::parse_expr(text)
2352            .expect("failed parser")
2353            .to_expr::<ast::ExprBuilder<()>>()
2354            .unwrap_or_else(|errs| {
2355                panic!("failed conversion to AST:\n{:?}", miette::Report::new(errs))
2356            })
2357    }
2358
2359    #[track_caller]
2360    fn assert_parse_expr_fails(text: &str) -> ParseErrors {
2361        let result = text_to_cst::parse_expr(text)
2362            .expect("failed parser")
2363            .to_expr::<ast::ExprBuilder<()>>();
2364        match result {
2365            Ok(expr) => {
2366                panic!("conversion to AST should have failed, but succeeded with:\n{expr}")
2367            }
2368            Err(errs) => errs,
2369        }
2370    }
2371
2372    #[track_caller]
2373    fn assert_parse_policy_succeeds(text: &str) -> ast::StaticPolicy {
2374        text_to_cst::parse_policy(text)
2375            .expect("failed parser")
2376            .to_policy(ast::PolicyID::from_string("id"))
2377            .unwrap_or_else(|errs| {
2378                panic!("failed conversion to AST:\n{:?}", miette::Report::new(errs))
2379            })
2380    }
2381
2382    #[track_caller]
2383    fn assert_parse_policy_fails(text: &str) -> ParseErrors {
2384        let result = text_to_cst::parse_policy(text)
2385            .expect("failed parser")
2386            .to_policy(ast::PolicyID::from_string("id"));
2387        match result {
2388            Ok(policy) => {
2389                panic!("conversion to AST should have failed, but succeeded with:\n{policy}")
2390            }
2391            Err(errs) => errs,
2392        }
2393    }
2394
2395    #[test]
2396    fn show_expr1() {
2397        assert_parse_expr_succeeds(
2398            r#"
2399            if 7 then 6 > 5 else !5 || "thursday" && ((8) >= "fish")
2400        "#,
2401        );
2402    }
2403
2404    #[test]
2405    fn show_expr2() {
2406        assert_parse_expr_succeeds(
2407            r#"
2408            [2,3,4].foo["hello"]
2409        "#,
2410        );
2411    }
2412
2413    #[test]
2414    fn show_expr3() {
2415        // these exprs are ill-typed, but are allowed by the parser
2416        let expr = assert_parse_expr_succeeds(
2417            r#"
2418            "first".some_ident
2419        "#,
2420        );
2421        assert_matches!(expr.expr_kind(), ast::ExprKind::GetAttr { attr, .. } => {
2422            assert_eq!(attr, "some_ident");
2423        });
2424    }
2425
2426    #[test]
2427    fn show_expr4() {
2428        let expr = assert_parse_expr_succeeds(
2429            r#"
2430            1.some_ident
2431        "#,
2432        );
2433        assert_matches!(expr.expr_kind(), ast::ExprKind::GetAttr { attr, .. } => {
2434            assert_eq!(attr, "some_ident");
2435        });
2436    }
2437
2438    #[test]
2439    fn show_expr5() {
2440        let expr = assert_parse_expr_succeeds(
2441            r#"
2442            "first"["some string"]
2443        "#,
2444        );
2445        assert_matches!(expr.expr_kind(), ast::ExprKind::GetAttr { attr, .. } => {
2446            assert_eq!(attr, "some string");
2447        });
2448    }
2449
2450    #[test]
2451    fn show_expr6() {
2452        let expr = assert_parse_expr_succeeds(
2453            r#"
2454            {"one":1,"two":2} has one
2455        "#,
2456        );
2457        assert_matches!(expr.expr_kind(), ast::ExprKind::HasAttr { attr, .. } => {
2458            assert_eq!(attr, "one");
2459        });
2460    }
2461
2462    #[test]
2463    fn show_expr7() {
2464        let expr = assert_parse_expr_succeeds(
2465            r#"
2466            {"one":1,"two":2}.one
2467        "#,
2468        );
2469        assert_matches!(expr.expr_kind(), ast::ExprKind::GetAttr { attr, .. } => {
2470            assert_eq!(attr, "one");
2471        });
2472    }
2473
2474    #[test]
2475    fn show_expr8() {
2476        // parses to the same AST expression as above
2477        let expr = assert_parse_expr_succeeds(
2478            r#"
2479            {"one":1,"two":2}["one"]
2480        "#,
2481        );
2482        assert_matches!(expr.expr_kind(), ast::ExprKind::GetAttr { attr, .. } => {
2483            assert_eq!(attr, "one");
2484        });
2485    }
2486
2487    #[test]
2488    fn show_expr9() {
2489        // accessing a record with a non-identifier attribute
2490        let expr = assert_parse_expr_succeeds(
2491            r#"
2492            {"this is a valid map key+.-_%()":1,"two":2}["this is a valid map key+.-_%()"]
2493        "#,
2494        );
2495        assert_matches!(expr.expr_kind(), ast::ExprKind::GetAttr { attr, .. } => {
2496            assert_eq!(attr, "this is a valid map key+.-_%()");
2497        });
2498    }
2499
2500    #[test]
2501    fn show_expr10() {
2502        let src = r#"
2503            {if true then a else b:"b"} ||
2504            {if false then a else b:"b"}
2505        "#;
2506        let errs = assert_parse_expr_fails(src);
2507        expect_n_errors(src, &errs, 4);
2508        expect_some_error_matches(
2509            src,
2510            &errs,
2511            &ExpectedErrorMessageBuilder::error("invalid variable: a")
2512                .help("the valid Cedar variables are `principal`, `action`, `resource`, and `context`; did you mean to enclose `a` in quotes to make a string?")
2513                .exactly_one_underline("a")
2514                .build(),
2515        );
2516        expect_some_error_matches(
2517            src,
2518            &errs,
2519            &ExpectedErrorMessageBuilder::error("invalid variable: b")
2520                .help("the valid Cedar variables are `principal`, `action`, `resource`, and `context`; did you mean to enclose `b` in quotes to make a string?")
2521                .exactly_one_underline("b")
2522                .build(),
2523        );
2524    }
2525
2526    #[test]
2527    fn show_expr11() {
2528        let expr = assert_parse_expr_succeeds(
2529            r#"
2530            {principal:"principal"}
2531        "#,
2532        );
2533        assert_matches!(expr.expr_kind(), ast::ExprKind::Record { .. });
2534    }
2535
2536    #[test]
2537    fn show_expr12() {
2538        let expr = assert_parse_expr_succeeds(
2539            r#"
2540            {"principal":"principal"}
2541        "#,
2542        );
2543        assert_matches!(expr.expr_kind(), ast::ExprKind::Record { .. });
2544    }
2545
2546    #[test]
2547    fn reserved_idents1() {
2548        let src = r#"
2549            The::true::path::to::"enlightenment".false
2550        "#;
2551        let errs = assert_parse_expr_fails(src);
2552        expect_n_errors(src, &errs, 2);
2553        expect_some_error_matches(
2554            src,
2555            &errs,
2556            &ExpectedErrorMessageBuilder::error(
2557                "this identifier is reserved and cannot be used: true",
2558            )
2559            .exactly_one_underline("true")
2560            .build(),
2561        );
2562        expect_some_error_matches(
2563            src,
2564            &errs,
2565            &ExpectedErrorMessageBuilder::error(
2566                "this identifier is reserved and cannot be used: false",
2567            )
2568            .exactly_one_underline("false")
2569            .build(),
2570        );
2571    }
2572
2573    #[test]
2574    fn reserved_idents2() {
2575        let src = r#"
2576            if {if: true}.if then {"if":false}["if"] else {when:true}.permit
2577        "#;
2578        let errs = assert_parse_expr_fails(src);
2579        expect_n_errors(src, &errs, 2);
2580        expect_some_error_matches(
2581            src,
2582            &errs,
2583            &ExpectedErrorMessageBuilder::error(
2584                "this identifier is reserved and cannot be used: if",
2585            )
2586            .exactly_one_underline("if: true")
2587            .build(),
2588        );
2589        expect_some_error_matches(
2590            src,
2591            &errs,
2592            &ExpectedErrorMessageBuilder::error(
2593                "this identifier is reserved and cannot be used: if",
2594            )
2595            .exactly_one_underline("if")
2596            .build(),
2597        );
2598    }
2599
2600    #[test]
2601    fn reserved_idents3() {
2602        let src = r#"
2603            if {where: true}.like || {has:false}.in then {"like":false}["in"] else {then:true}.else
2604        "#;
2605        let errs = assert_parse_expr_fails(src);
2606        expect_n_errors(src, &errs, 5);
2607        expect_some_error_matches(
2608            src,
2609            &errs,
2610            &ExpectedErrorMessageBuilder::error(
2611                "this identifier is reserved and cannot be used: has",
2612            )
2613            .exactly_one_underline("has")
2614            .build(),
2615        );
2616        expect_some_error_matches(
2617            src,
2618            &errs,
2619            &ExpectedErrorMessageBuilder::error(
2620                "this identifier is reserved and cannot be used: like",
2621            )
2622            .exactly_one_underline("like")
2623            .build(),
2624        );
2625        expect_some_error_matches(
2626            src,
2627            &errs,
2628            &ExpectedErrorMessageBuilder::error(
2629                "this identifier is reserved and cannot be used: in",
2630            )
2631            .exactly_one_underline("in")
2632            .build(),
2633        );
2634        expect_some_error_matches(
2635            src,
2636            &errs,
2637            &ExpectedErrorMessageBuilder::error(
2638                "this identifier is reserved and cannot be used: then",
2639            )
2640            .exactly_one_underline("then")
2641            .build(),
2642        );
2643        expect_some_error_matches(
2644            src,
2645            &errs,
2646            &ExpectedErrorMessageBuilder::error(
2647                "this identifier is reserved and cannot be used: else",
2648            )
2649            .exactly_one_underline("else")
2650            .build(),
2651        );
2652    }
2653
2654    #[test]
2655    fn show_policy1() {
2656        let src = r#"
2657            permit(principal:p,action:a,resource:r)when{w}unless{u}advice{"doit"};
2658        "#;
2659        let errs = assert_parse_policy_fails(src);
2660        expect_n_errors(src, &errs, 6);
2661        expect_some_error_matches(
2662            src,
2663            &errs,
2664            &ExpectedErrorMessageBuilder::error("type constraints using `:` are not supported")
2665                .help("try using `is` instead")
2666                .exactly_one_underline("p")
2667                .build(),
2668        );
2669        expect_some_error_matches(
2670            src,
2671            &errs,
2672            &ExpectedErrorMessageBuilder::error("type constraints using `:` are not supported")
2673                .help("try using `is` instead")
2674                .exactly_one_underline("a")
2675                .build(),
2676        );
2677        expect_some_error_matches(
2678            src,
2679            &errs,
2680            &ExpectedErrorMessageBuilder::error("type constraints using `:` are not supported")
2681                .help("try using `is` instead")
2682                .exactly_one_underline("r")
2683                .build(),
2684        );
2685        expect_some_error_matches(
2686            src,
2687            &errs,
2688            &ExpectedErrorMessageBuilder::error("invalid variable: w")
2689                .help("the valid Cedar variables are `principal`, `action`, `resource`, and `context`; did you mean to enclose `w` in quotes to make a string?")
2690                .exactly_one_underline("w")
2691                .build(),
2692        );
2693        expect_some_error_matches(
2694            src,
2695            &errs,
2696            &ExpectedErrorMessageBuilder::error("invalid variable: u")
2697                .help("the valid Cedar variables are `principal`, `action`, `resource`, and `context`; did you mean to enclose `u` in quotes to make a string?")
2698                .exactly_one_underline("u")
2699                .build(),
2700        );
2701        expect_some_error_matches(
2702            src,
2703            &errs,
2704            &ExpectedErrorMessageBuilder::error("invalid policy condition: advice")
2705                .help("condition must be either `when` or `unless`")
2706                .exactly_one_underline("advice")
2707                .build(),
2708        );
2709    }
2710
2711    #[test]
2712    fn show_policy2() {
2713        let src = r#"
2714            permit(principal,action,resource)when{true};
2715        "#;
2716        assert_parse_policy_succeeds(src);
2717    }
2718
2719    #[test]
2720    fn show_policy3() {
2721        let src = r#"
2722            permit(principal in User::"jane",action,resource);
2723        "#;
2724        assert_parse_policy_succeeds(src);
2725    }
2726
2727    #[test]
2728    fn show_policy4() {
2729        let src = r#"
2730            forbid(principal in User::"jane",action,resource)unless{
2731                context.group != "friends"
2732            };
2733        "#;
2734        assert_parse_policy_succeeds(src);
2735    }
2736
2737    #[test]
2738    fn single_annotation() {
2739        // common use-case
2740        let policy = assert_parse_policy_succeeds(
2741            r#"
2742            @anno("good annotation")permit(principal,action,resource);
2743        "#,
2744        );
2745        assert_matches!(
2746            policy.annotation(&ast::AnyId::new_unchecked("anno")),
2747            Some(annotation) => assert_eq!(annotation.as_ref(), "good annotation")
2748        );
2749    }
2750
2751    #[test]
2752    fn duplicate_annotations_error() {
2753        // duplication is error
2754        let src = r#"
2755            @anno("good annotation")
2756            @anno2("good annotation")
2757            @anno("oops, duplicate")
2758            permit(principal,action,resource);
2759        "#;
2760        let errs = assert_parse_policy_fails(src);
2761        // annotation duplication (anno)
2762        expect_n_errors(src, &errs, 1);
2763        expect_some_error_matches(
2764            src,
2765            &errs,
2766            &ExpectedErrorMessageBuilder::error("duplicate annotation: @anno")
2767                .exactly_one_underline("@anno(\"oops, duplicate\")")
2768                .build(),
2769        );
2770    }
2771
2772    #[test]
2773    fn multiple_policys_and_annotations_ok() {
2774        // can have multiple annotations
2775        let policyset = text_to_cst::parse_policies(
2776            r#"
2777            @anno1("first")
2778            permit(principal,action,resource);
2779
2780            @anno2("second")
2781            permit(principal,action,resource);
2782
2783            @anno3a("third-a")
2784            @anno3b("third-b")
2785            permit(principal,action,resource);
2786        "#,
2787        )
2788        .expect("should parse")
2789        .to_policyset()
2790        .unwrap_or_else(|errs| panic!("failed convert to AST:\n{:?}", miette::Report::new(errs)));
2791        assert_matches!(
2792            policyset
2793                .get(&ast::PolicyID::from_string("policy0"))
2794                .expect("should be a policy")
2795                .annotation(&ast::AnyId::new_unchecked("anno0")),
2796            None
2797        );
2798        assert_matches!(
2799            policyset
2800                .get(&ast::PolicyID::from_string("policy0"))
2801                .expect("should be a policy")
2802                .annotation(&ast::AnyId::new_unchecked("anno1")),
2803            Some(annotation) => assert_eq!(annotation.as_ref(), "first")
2804        );
2805        assert_matches!(
2806            policyset
2807                .get(&ast::PolicyID::from_string("policy1"))
2808                .expect("should be a policy")
2809                .annotation(&ast::AnyId::new_unchecked("anno2")),
2810            Some(annotation) => assert_eq!(annotation.as_ref(), "second")
2811        );
2812        assert_matches!(
2813            policyset
2814                .get(&ast::PolicyID::from_string("policy2"))
2815                .expect("should be a policy")
2816                .annotation(&ast::AnyId::new_unchecked("anno3a")),
2817            Some(annotation) => assert_eq!(annotation.as_ref(), "third-a")
2818        );
2819        assert_matches!(
2820            policyset
2821                .get(&ast::PolicyID::from_string("policy2"))
2822                .expect("should be a policy")
2823                .annotation(&ast::AnyId::new_unchecked("anno3b")),
2824            Some(annotation) => assert_eq!(annotation.as_ref(), "third-b")
2825        );
2826        assert_matches!(
2827            policyset
2828                .get(&ast::PolicyID::from_string("policy2"))
2829                .expect("should be a policy")
2830                .annotation(&ast::AnyId::new_unchecked("anno3c")),
2831            None
2832        );
2833        assert_eq!(
2834            policyset
2835                .get(&ast::PolicyID::from_string("policy2"))
2836                .expect("should be a policy")
2837                .annotations()
2838                .count(),
2839            2
2840        );
2841    }
2842
2843    #[test]
2844    fn reserved_word_annotations_ok() {
2845        // can have Cedar reserved words as annotation keys
2846        let policyset = text_to_cst::parse_policies(
2847            r#"
2848            @if("this is the annotation for `if`")
2849            @then("this is the annotation for `then`")
2850            @else("this is the annotation for `else`")
2851            @true("this is the annotation for `true`")
2852            @false("this is the annotation for `false`")
2853            @in("this is the annotation for `in`")
2854            @is("this is the annotation for `is`")
2855            @like("this is the annotation for `like`")
2856            @has("this is the annotation for `has`")
2857            @principal("this is the annotation for `principal`") // not reserved at time of this writing, but we test it anyway
2858            permit(principal, action, resource);
2859            "#,
2860        ).expect("should parse")
2861        .to_policyset()
2862        .unwrap_or_else(|errs| panic!("failed convert to AST:\n{:?}", miette::Report::new(errs)));
2863        let policy0 = policyset
2864            .get(&ast::PolicyID::from_string("policy0"))
2865            .expect("should be the right policy ID");
2866        assert_matches!(
2867            policy0.annotation(&ast::AnyId::new_unchecked("if")),
2868            Some(annotation) => assert_eq!(annotation.as_ref(), "this is the annotation for `if`")
2869        );
2870        assert_matches!(
2871            policy0.annotation(&ast::AnyId::new_unchecked("then")),
2872            Some(annotation) => assert_eq!(annotation.as_ref(), "this is the annotation for `then`")
2873        );
2874        assert_matches!(
2875            policy0.annotation(&ast::AnyId::new_unchecked("else")),
2876            Some(annotation) => assert_eq!(annotation.as_ref(), "this is the annotation for `else`")
2877        );
2878        assert_matches!(
2879            policy0.annotation(&ast::AnyId::new_unchecked("true")),
2880            Some(annotation) => assert_eq!(annotation.as_ref(), "this is the annotation for `true`")
2881        );
2882        assert_matches!(
2883            policy0.annotation(&ast::AnyId::new_unchecked("false")),
2884            Some(annotation) => assert_eq!(annotation.as_ref(), "this is the annotation for `false`")
2885        );
2886        assert_matches!(
2887            policy0.annotation(&ast::AnyId::new_unchecked("in")),
2888            Some(annotation) => assert_eq!(annotation.as_ref(), "this is the annotation for `in`")
2889        );
2890        assert_matches!(
2891            policy0.annotation(&ast::AnyId::new_unchecked("is")),
2892            Some(annotation) => assert_eq!(annotation.as_ref(), "this is the annotation for `is`")
2893        );
2894        assert_matches!(
2895            policy0.annotation(&ast::AnyId::new_unchecked("like")),
2896            Some(annotation) => assert_eq!(annotation.as_ref(), "this is the annotation for `like`")
2897        );
2898        assert_matches!(
2899            policy0.annotation(&ast::AnyId::new_unchecked("has")),
2900            Some(annotation) => assert_eq!(annotation.as_ref(), "this is the annotation for `has`")
2901        );
2902        assert_matches!(
2903            policy0.annotation(&ast::AnyId::new_unchecked("principal")),
2904            Some(annotation) => assert_eq!(annotation.as_ref(), "this is the annotation for `principal`")
2905        );
2906    }
2907
2908    #[test]
2909    fn single_annotation_without_value() {
2910        let policy = assert_parse_policy_succeeds(r#"@anno permit(principal,action,resource);"#);
2911        assert_matches!(
2912            policy.annotation(&ast::AnyId::new_unchecked("anno")),
2913            Some(annotation) => assert_eq!(annotation.as_ref(), ""),
2914        );
2915    }
2916
2917    #[test]
2918    fn duplicate_annotations_without_value() {
2919        let src = "@anno @anno permit(principal,action,resource);";
2920        let errs = assert_parse_policy_fails(src);
2921        expect_n_errors(src, &errs, 1);
2922        expect_some_error_matches(
2923            src,
2924            &errs,
2925            &ExpectedErrorMessageBuilder::error("duplicate annotation: @anno")
2926                .exactly_one_underline("@anno")
2927                .build(),
2928        );
2929    }
2930
2931    #[test]
2932    fn multiple_annotation_without_value() {
2933        let policy =
2934            assert_parse_policy_succeeds(r#"@foo @bar permit(principal,action,resource);"#);
2935        assert_matches!(
2936            policy.annotation(&ast::AnyId::new_unchecked("foo")),
2937            Some(annotation) => assert_eq!(annotation.as_ref(), ""),
2938        );
2939        assert_matches!(
2940            policy.annotation(&ast::AnyId::new_unchecked("bar")),
2941            Some(annotation) => assert_eq!(annotation.as_ref(), ""),
2942        );
2943    }
2944
2945    #[test]
2946    fn fail_scope1() {
2947        let src = r#"
2948            permit(
2949                principal in [User::"jane",Group::"friends"],
2950                action,
2951                resource
2952            );
2953        "#;
2954        let errs = assert_parse_policy_fails(src);
2955        expect_n_errors(src, &errs, 1);
2956        expect_some_error_matches(
2957            src,
2958            &errs,
2959            &ExpectedErrorMessageBuilder::error(
2960                "expected single entity uid or template slot, found set of entity uids",
2961            )
2962            .exactly_one_underline(r#"[User::"jane",Group::"friends"]"#)
2963            .build(),
2964        );
2965    }
2966
2967    #[test]
2968    fn fail_scope2() {
2969        let src = r#"
2970            permit(
2971                principal in User::"jane",
2972                action == if true then Photo::"view" else Photo::"edit",
2973                resource
2974            );
2975        "#;
2976        let errs = assert_parse_policy_fails(src);
2977        expect_n_errors(src, &errs, 1);
2978        expect_some_error_matches(
2979            src,
2980            &errs,
2981            &ExpectedErrorMessageBuilder::error("expected an entity uid, found an `if` expression")
2982                .exactly_one_underline(r#"if true then Photo::"view" else Photo::"edit""#)
2983                .build(),
2984        );
2985    }
2986
2987    #[test]
2988    fn fail_scope3() {
2989        let src = r#"
2990            permit(principal,action,resource,context);
2991        "#;
2992        let errs = assert_parse_policy_fails(src);
2993        expect_n_errors(src, &errs, 1);
2994        expect_some_error_matches(
2995            src,
2996            &errs,
2997            &ExpectedErrorMessageBuilder::error(
2998                "this policy has an extra element in the scope: context",
2999            )
3000            .help("policy scopes must contain a `principal`, `action`, and `resource` element in that order")
3001            .exactly_one_underline("context")
3002            .build(),
3003        );
3004    }
3005
3006    #[test]
3007    fn method_call2() {
3008        assert_parse_expr_succeeds(
3009            r#"
3010                principal.contains(resource)
3011                "#,
3012        );
3013
3014        let src = r#"
3015        contains(principal,resource)
3016        "#;
3017        let errs = assert_parse_expr_fails(src);
3018        expect_n_errors(src, &errs, 1);
3019        expect_some_error_matches(
3020            src,
3021            &errs,
3022            &ExpectedErrorMessageBuilder::error("`contains` is a method, not a function")
3023                .help("use a method-style call `e.contains(..)`")
3024                .exactly_one_underline("contains(principal,resource)")
3025                .build(),
3026        );
3027    }
3028
3029    #[test]
3030    fn construct_record_1() {
3031        let e = assert_parse_expr_succeeds(
3032            r#"
3033                {one:"one"}
3034                "#,
3035        );
3036        // ast should be acceptable, with record construction
3037        assert_matches!(e.expr_kind(), ast::ExprKind::Record { .. });
3038        println!("{e}");
3039    }
3040
3041    #[test]
3042    fn construct_record_2() {
3043        let e = assert_parse_expr_succeeds(
3044            r#"
3045                {"one":"one"}
3046                "#,
3047        );
3048        // ast should be acceptable, with record construction
3049        assert_matches!(e.expr_kind(), ast::ExprKind::Record { .. });
3050        println!("{e}");
3051    }
3052
3053    #[test]
3054    fn construct_record_3() {
3055        let e = assert_parse_expr_succeeds(
3056            r#"
3057                {"one":"one",two:"two"}
3058                "#,
3059        );
3060        // ast should be acceptable, with record construction
3061        assert_matches!(e.expr_kind(), ast::ExprKind::Record { .. });
3062        println!("{e}");
3063    }
3064
3065    #[test]
3066    fn construct_record_4() {
3067        let e = assert_parse_expr_succeeds(
3068            r#"
3069                {one:"one","two":"two"}
3070                "#,
3071        );
3072        // ast should be acceptable, with record construction
3073        assert_matches!(e.expr_kind(), ast::ExprKind::Record { .. });
3074        println!("{e}");
3075    }
3076
3077    #[test]
3078    fn construct_record_5() {
3079        let e = assert_parse_expr_succeeds(
3080            r#"
3081                {one:"b\"","b\"":2}
3082                "#,
3083        );
3084        // ast should be acceptable, with record construction
3085        assert_matches!(e.expr_kind(), ast::ExprKind::Record { .. });
3086        println!("{e}");
3087    }
3088
3089    #[test]
3090    fn construct_invalid_get_1() {
3091        let src = r#"
3092            {"one":1, "two":"two"}[0]
3093        "#;
3094        let errs = assert_parse_expr_fails(src);
3095        expect_n_errors(src, &errs, 1);
3096        expect_some_error_matches(
3097            src,
3098            &errs,
3099            &ExpectedErrorMessageBuilder::error("invalid string literal: 0")
3100                .exactly_one_underline("0")
3101                .build(),
3102        );
3103    }
3104
3105    #[test]
3106    fn construct_invalid_get_2() {
3107        let src = r#"
3108            {"one":1, "two":"two"}[-1]
3109        "#;
3110        let errs = assert_parse_expr_fails(src);
3111        expect_n_errors(src, &errs, 1);
3112        expect_some_error_matches(
3113            src,
3114            &errs,
3115            &ExpectedErrorMessageBuilder::error("invalid string literal: (-1)")
3116                .exactly_one_underline("-1")
3117                .build(),
3118        );
3119    }
3120
3121    #[test]
3122    fn construct_invalid_get_3() {
3123        let src = r#"
3124            {"one":1, "two":"two"}[true]
3125        "#;
3126        let errs = assert_parse_expr_fails(src);
3127        expect_n_errors(src, &errs, 1);
3128        expect_some_error_matches(
3129            src,
3130            &errs,
3131            &ExpectedErrorMessageBuilder::error("invalid string literal: true")
3132                .exactly_one_underline("true")
3133                .build(),
3134        );
3135    }
3136
3137    #[test]
3138    fn construct_invalid_get_4() {
3139        let src = r#"
3140            {"one":1, "two":"two"}[one]
3141        "#;
3142        let errs = assert_parse_expr_fails(src);
3143        expect_n_errors(src, &errs, 1);
3144        expect_some_error_matches(
3145            src,
3146            &errs,
3147            &ExpectedErrorMessageBuilder::error("invalid string literal: one")
3148                .exactly_one_underline("one")
3149                .build(),
3150        );
3151    }
3152
3153    #[test]
3154    fn construct_invalid_get_var() {
3155        let src = r#"
3156            {"principal":1, "two":"two"}[principal]
3157        "#;
3158        let errs = assert_parse_expr_fails(src);
3159        expect_n_errors(src, &errs, 1);
3160        expect_some_error_matches(
3161            src,
3162            &errs,
3163            &ExpectedErrorMessageBuilder::error("invalid string literal: principal")
3164                .exactly_one_underline("principal")
3165                .build(),
3166        );
3167    }
3168
3169    #[test]
3170    fn construct_has_1() {
3171        let expr = assert_parse_expr_succeeds(
3172            r#"
3173            {"one":1,"two":2} has "arbitrary+ _string"
3174        "#,
3175        );
3176        assert_matches!(expr.expr_kind(), ast::ExprKind::HasAttr { attr, .. } => {
3177            assert_eq!(attr, "arbitrary+ _string");
3178        });
3179    }
3180
3181    #[test]
3182    fn construct_has_2() {
3183        let src = r#"
3184            {"one":1,"two":2} has 1
3185        "#;
3186        let errs = assert_parse_expr_fails(src);
3187        expect_n_errors(src, &errs, 1);
3188        expect_some_error_matches(
3189            src,
3190            &errs,
3191            &ExpectedErrorMessageBuilder::error("invalid RHS of a `has` operation: 1")
3192                .help("valid RHS of a `has` operation is either a sequence of identifiers separated by `.` or a string literal")
3193                .exactly_one_underline("1")
3194                .build(),
3195        );
3196    }
3197
3198    #[test]
3199    fn construct_like_1() {
3200        let expr = assert_parse_expr_succeeds(
3201            r#"
3202            "354 hams" like "*5*"
3203        "#,
3204        );
3205        assert_matches!(expr.expr_kind(), ast::ExprKind::Like { pattern, .. } => {
3206            assert_eq!(pattern.to_string(), "*5*");
3207        });
3208    }
3209
3210    #[test]
3211    fn construct_like_2() {
3212        let src = r#"
3213            "354 hams" like 354
3214        "#;
3215        let errs = assert_parse_expr_fails(src);
3216        expect_n_errors(src, &errs, 1);
3217        expect_some_error_matches(
3218            src,
3219            &errs,
3220            &ExpectedErrorMessageBuilder::error(
3221                "right hand side of a `like` expression must be a pattern literal, but got `354`",
3222            )
3223            .exactly_one_underline("354")
3224            .build(),
3225        );
3226    }
3227
3228    #[test]
3229    fn construct_like_3() {
3230        let expr = assert_parse_expr_succeeds(
3231            r#"
3232            "string\\with\\backslashes" like "string\\with\\backslashes"
3233        "#,
3234        );
3235        assert_matches!(expr.expr_kind(), ast::ExprKind::Like { pattern, .. } => {
3236            assert_eq!(pattern.to_string(), r"string\\with\\backslashes");
3237        });
3238    }
3239
3240    #[test]
3241    fn construct_like_4() {
3242        let expr = assert_parse_expr_succeeds(
3243            r#"
3244            "string\\with\\backslashes" like "string\*with\*backslashes"
3245        "#,
3246        );
3247        assert_matches!(expr.expr_kind(), ast::ExprKind::Like { pattern, .. } => {
3248            assert_eq!(pattern.to_string(), r"string\*with\*backslashes");
3249        });
3250    }
3251
3252    #[test]
3253    fn construct_like_5() {
3254        let src = r#"
3255            "string\*with\*escaped\*stars" like "string\*with\*escaped\*stars"
3256        "#;
3257        let errs = assert_parse_expr_fails(src);
3258        expect_n_errors(src, &errs, 3);
3259        // all three errors are the same -- they report a use of \* in the first argument
3260        expect_some_error_matches(
3261            src,
3262            &errs,
3263            &ExpectedErrorMessageBuilder::error("the input `\\*` is not a valid escape")
3264                .exactly_one_underline(r#""string\*with\*escaped\*stars""#)
3265                .build(),
3266        );
3267    }
3268
3269    #[test]
3270    fn construct_like_6() {
3271        let expr = assert_parse_expr_succeeds(
3272            r#"
3273            "string*with*stars" like "string\*with\*stars"
3274        "#,
3275        );
3276        assert_matches!(expr.expr_kind(), ast::ExprKind::Like { pattern, .. } => {
3277            assert_eq!(pattern.to_string(), "string\\*with\\*stars");
3278        });
3279    }
3280
3281    #[test]
3282    fn construct_like_7() {
3283        let expr = assert_parse_expr_succeeds(
3284            r#"
3285            "string\\*with\\*backslashes\\*and\\*stars" like "string\\\*with\\\*backslashes\\\*and\\\*stars"
3286        "#,
3287        );
3288        assert_matches!(expr.expr_kind(), ast::ExprKind::Like { pattern, .. } => {
3289            assert_eq!(
3290                pattern.to_string(),
3291                r"string\\\*with\\\*backslashes\\\*and\\\*stars"
3292            );
3293        });
3294    }
3295
3296    #[test]
3297    fn construct_like_var() {
3298        let src = r#"
3299            "principal" like principal
3300        "#;
3301        let errs = assert_parse_expr_fails(src);
3302        expect_n_errors(src, &errs, 1);
3303        expect_some_error_matches(
3304            src,
3305            &errs,
3306            &ExpectedErrorMessageBuilder::error(
3307                "right hand side of a `like` expression must be a pattern literal, but got `principal`",
3308            )
3309            .exactly_one_underline("principal")
3310            .build(),
3311        );
3312    }
3313
3314    #[test]
3315    fn construct_like_name() {
3316        let src = r#"
3317            "foo::bar::baz" like foo::bar
3318        "#;
3319        let errs = assert_parse_expr_fails(src);
3320        expect_n_errors(src, &errs, 1);
3321        expect_some_error_matches(
3322            src,
3323            &errs,
3324            &ExpectedErrorMessageBuilder::error(
3325                "right hand side of a `like` expression must be a pattern literal, but got `foo::bar`",
3326            )
3327            .exactly_one_underline("foo::bar")
3328            .build(),
3329        );
3330    }
3331
3332    #[test]
3333    fn pattern_roundtrip() {
3334        let test_pattern = ast::Pattern::from(vec![
3335            PatternElem::Char('h'),
3336            PatternElem::Char('e'),
3337            PatternElem::Char('l'),
3338            PatternElem::Char('l'),
3339            PatternElem::Char('o'),
3340            PatternElem::Char('\\'),
3341            PatternElem::Char('0'),
3342            PatternElem::Char('*'),
3343            PatternElem::Char('\\'),
3344            PatternElem::Char('*'),
3345        ]);
3346        let e1 = ast::Expr::like(ast::Expr::val("hello"), test_pattern.clone());
3347        let s1 = format!("{e1}");
3348        // Char('\\') prints to r#"\\"# and Char('*') prints to r#"\*"#.
3349        assert_eq!(s1, r#""hello" like "hello\\0\*\\\*""#);
3350        let e2 = assert_parse_expr_succeeds(&s1);
3351        assert_matches!(e2.expr_kind(), ast::ExprKind::Like { pattern, .. } => {
3352            assert_eq!(pattern.get_elems(), test_pattern.get_elems());
3353        });
3354        let s2 = format!("{e2}");
3355        assert_eq!(s1, s2);
3356    }
3357
3358    #[test]
3359    fn issue_wf_5046() {
3360        let policy = parse_policy(
3361            Some(ast::PolicyID::from_string("WF-5046")),
3362            r#"permit(
3363            principal,
3364            action in [Action::"action"],
3365            resource in G::""
3366          ) when {
3367            true && ("" like "/gisterNatives\\*D")
3368          };"#,
3369        );
3370        assert!(policy.is_ok());
3371    }
3372
3373    #[test]
3374    fn entity_access() {
3375        // entities can be accessed using the same notation as records
3376
3377        // ok
3378        let expr = assert_parse_expr_succeeds(
3379            r#"
3380            User::"jane" has age
3381        "#,
3382        );
3383        assert_matches!(expr.expr_kind(), ast::ExprKind::HasAttr { attr, .. } => {
3384            assert_eq!(attr, "age");
3385        });
3386
3387        // ok
3388        let expr = assert_parse_expr_succeeds(
3389            r#"
3390            User::"jane" has "arbitrary+ _string"
3391        "#,
3392        );
3393        assert_matches!(expr.expr_kind(), ast::ExprKind::HasAttr { attr, .. } => {
3394            assert_eq!(attr, "arbitrary+ _string");
3395        });
3396
3397        // not ok: 1 is not a valid attribute
3398        let src = r#"
3399            User::"jane" has 1
3400        "#;
3401        let errs = assert_parse_expr_fails(src);
3402        expect_n_errors(src, &errs, 1);
3403        expect_some_error_matches(
3404            src,
3405            &errs,
3406            &ExpectedErrorMessageBuilder::error("invalid RHS of a `has` operation: 1")
3407                .help("valid RHS of a `has` operation is either a sequence of identifiers separated by `.` or a string literal")
3408                .exactly_one_underline("1")
3409                .build(),
3410        );
3411
3412        // ok
3413        let expr = assert_parse_expr_succeeds(
3414            r#"
3415            User::"jane".age
3416        "#,
3417        );
3418        assert_matches!(expr.expr_kind(), ast::ExprKind::GetAttr { attr, .. } => {
3419            assert_eq!(attr, "age");
3420        });
3421
3422        // ok
3423        let expr: ast::Expr = assert_parse_expr_succeeds(
3424            r#"
3425            User::"jane"["arbitrary+ _string"]
3426        "#,
3427        );
3428        assert_matches!(expr.expr_kind(), ast::ExprKind::GetAttr { attr, .. } => {
3429            assert_eq!(attr, "arbitrary+ _string");
3430        });
3431
3432        // not ok: age is not a string literal
3433        let src = r#"
3434            User::"jane"[age]
3435        "#;
3436        let errs = assert_parse_expr_fails(src);
3437        expect_n_errors(src, &errs, 1);
3438        expect_some_error_matches(
3439            src,
3440            &errs,
3441            &ExpectedErrorMessageBuilder::error("invalid string literal: age")
3442                .exactly_one_underline("age")
3443                .build(),
3444        );
3445    }
3446
3447    #[test]
3448    fn relational_ops1() {
3449        let src = r#"
3450            3 >= 2 >= 1
3451        "#;
3452        let errs = assert_parse_expr_fails(src);
3453        expect_n_errors(src, &errs, 1);
3454        expect_some_error_matches(
3455            src,
3456            &errs,
3457            &ExpectedErrorMessageBuilder::error("multiple relational operators (>, ==, in, etc.) must be used with parentheses to make ordering explicit")
3458                .exactly_one_underline("3 >= 2 >= 1")
3459                .build(),
3460        );
3461    }
3462
3463    #[test]
3464    fn relational_ops2() {
3465        assert_parse_expr_succeeds(
3466            r#"
3467                    3 >= ("dad" in "dad")
3468                    "#,
3469        );
3470    }
3471
3472    #[test]
3473    fn relational_ops3() {
3474        assert_parse_expr_succeeds(
3475            r#"
3476                (3 >= 2) == true
3477                "#,
3478        );
3479    }
3480
3481    #[test]
3482    fn relational_ops4() {
3483        let src = r#"
3484            if 4 < 3 then 4 != 3 else 4 == 3 < 4
3485        "#;
3486        let errs = assert_parse_expr_fails(src);
3487        expect_n_errors(src, &errs, 1);
3488        expect_some_error_matches(
3489            src,
3490            &errs,
3491            &ExpectedErrorMessageBuilder::error("multiple relational operators (>, ==, in, etc.) must be used with parentheses to make ordering explicit")
3492                .exactly_one_underline("4 == 3 < 4")
3493                .build(),
3494        );
3495    }
3496
3497    #[test]
3498    fn arithmetic() {
3499        assert_parse_expr_succeeds(r#" 2 + 4 "#);
3500        assert_parse_expr_succeeds(r#" 2 + -5 "#);
3501        assert_parse_expr_succeeds(r#" 2 - 5 "#);
3502        assert_parse_expr_succeeds(r#" 2 * 5 "#);
3503        assert_parse_expr_succeeds(r#" 2 * -5 "#);
3504        assert_parse_expr_succeeds(r#" context.size * 4 "#);
3505        assert_parse_expr_succeeds(r#" 4 * context.size "#);
3506        assert_parse_expr_succeeds(r#" context.size * context.scale "#);
3507        assert_parse_expr_succeeds(r#" 5 + 10 + 90 "#);
3508        assert_parse_expr_succeeds(r#" 5 + 10 - 90 * -2 "#);
3509        assert_parse_expr_succeeds(r#" 5 + 10 * 90 - 2 "#);
3510        assert_parse_expr_succeeds(r#" 5 - 10 - 90 - 2 "#);
3511        assert_parse_expr_succeeds(r#" 5 * context.size * 10 "#);
3512        assert_parse_expr_succeeds(r#" context.size * 3 * context.scale "#);
3513    }
3514
3515    const CORRECT_TEMPLATES: [&str; 7] = [
3516        r#"permit(principal == ?principal, action == Action::"action", resource == ?resource);"#,
3517        r#"permit(principal in ?principal, action == Action::"action", resource in ?resource);"#,
3518        r#"permit(principal in ?principal, action == Action::"action", resource in ?resource);"#,
3519        r#"permit(principal in p::"principal", action == Action::"action", resource in ?resource);"#,
3520        r#"permit(principal == p::"principal", action == Action::"action", resource in ?resource);"#,
3521        r#"permit(principal in ?principal, action == Action::"action", resource in r::"resource");"#,
3522        r#"permit(principal in ?principal, action == Action::"action", resource == r::"resource");"#,
3523    ];
3524
3525    #[test]
3526    fn template_tests() {
3527        for src in CORRECT_TEMPLATES {
3528            text_to_cst::parse_policy(src)
3529                .expect("parse_error")
3530                .to_template(ast::PolicyID::from_string("i0"))
3531                .unwrap_or_else(|errs| {
3532                    panic!(
3533                        "Failed to create a policy template: {:?}",
3534                        miette::Report::new(errs)
3535                    );
3536                });
3537        }
3538    }
3539
3540    #[test]
3541    fn var_type() {
3542        assert_parse_policy_succeeds(
3543            r#"
3544                permit(principal,action,resource);
3545                "#,
3546        );
3547
3548        let src = r#"
3549            permit(principal:User,action,resource);
3550        "#;
3551        let errs = assert_parse_policy_fails(src);
3552        expect_n_errors(src, &errs, 1);
3553        expect_some_error_matches(
3554            src,
3555            &errs,
3556            &ExpectedErrorMessageBuilder::error("type constraints using `:` are not supported")
3557                .help("try using `is` instead")
3558                .exactly_one_underline("User")
3559                .build(),
3560        );
3561    }
3562
3563    #[test]
3564    fn unescape_err_positions() {
3565        let assert_invalid_escape = |p_src, underline| {
3566            assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
3567                expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("the input `\\q` is not a valid escape").exactly_one_underline(underline).build());
3568            });
3569        };
3570        assert_invalid_escape(
3571            r#"@foo("\q")permit(principal, action, resource);"#,
3572            r#"@foo("\q")"#,
3573        );
3574        assert_invalid_escape(
3575            r#"permit(principal, action, resource) when { "\q" };"#,
3576            r#""\q""#,
3577        );
3578        assert_invalid_escape(
3579            r#"permit(principal, action, resource) when { "\q".contains(0) };"#,
3580            r#""\q""#,
3581        );
3582        assert_invalid_escape(
3583            r#"permit(principal, action, resource) when { "\q".bar };"#,
3584            r#""\q""#,
3585        );
3586        assert_invalid_escape(
3587            r#"permit(principal, action, resource) when { "\q"["a"] };"#,
3588            r#""\q""#,
3589        );
3590        assert_invalid_escape(
3591            r#"permit(principal, action, resource) when { "" like "\q" };"#,
3592            r#""\q""#,
3593        );
3594        assert_invalid_escape(
3595            r#"permit(principal, action, resource) when { {}["\q"] };"#,
3596            r#""\q""#,
3597        );
3598        assert_invalid_escape(
3599            r#"permit(principal, action, resource) when { {"\q": 0} };"#,
3600            r#""\q""#,
3601        );
3602        assert_invalid_escape(
3603            r#"permit(principal, action, resource) when { User::"\q" };"#,
3604            r#"User::"\q""#,
3605        );
3606    }
3607
3608    #[track_caller] // report the caller's location as the location of the panic, not the location in this function
3609    fn expect_action_error(test: &str, msg: &str, underline: &str) {
3610        assert_matches!(parse_policyset(test), Err(es) => {
3611            expect_some_error_matches(
3612                test,
3613                &es,
3614                &ExpectedErrorMessageBuilder::error(msg)
3615                    .help("action entities must have type `Action`, optionally in a namespace")
3616                    .exactly_one_underline(underline)
3617                    .build(),
3618            );
3619        });
3620    }
3621
3622    #[test]
3623    fn action_must_be_action() {
3624        parse_policyset(r#"permit(principal, action == Action::"view", resource);"#)
3625            .expect("Valid policy failed to parse");
3626        parse_policyset(r#"permit(principal, action == Foo::Action::"view", resource);"#)
3627            .expect("Valid policy failed to parse");
3628        parse_policyset(r#"permit(principal, action in Action::"view", resource);"#)
3629            .expect("Valid policy failed to parse");
3630        parse_policyset(r#"permit(principal, action in Foo::Action::"view", resource);"#)
3631            .expect("Valid policy failed to parse");
3632        parse_policyset(r#"permit(principal, action in [Foo::Action::"view"], resource);"#)
3633            .expect("Valid policy failed to parse");
3634        parse_policyset(
3635            r#"permit(principal, action in [Foo::Action::"view", Action::"view"], resource);"#,
3636        )
3637        .expect("Valid policy failed to parse");
3638
3639        expect_action_error(
3640            r#"permit(principal, action == Foo::"view", resource);"#,
3641            "expected an entity uid with type `Action` but got `Foo::\"view\"`",
3642            "Foo::\"view\"",
3643        );
3644        expect_action_error(
3645            r#"permit(principal, action == Action::Foo::"view", resource);"#,
3646            "expected an entity uid with type `Action` but got `Action::Foo::\"view\"`",
3647            "Action::Foo::\"view\"",
3648        );
3649        expect_action_error(
3650            r#"permit(principal, action == Bar::Action::Foo::"view", resource);"#,
3651            "expected an entity uid with type `Action` but got `Bar::Action::Foo::\"view\"`",
3652            "Bar::Action::Foo::\"view\"",
3653        );
3654        expect_action_error(
3655            r#"permit(principal, action in Bar::Action::Foo::"view", resource);"#,
3656            "expected an entity uid with type `Action` but got `Bar::Action::Foo::\"view\"`",
3657            "Bar::Action::Foo::\"view\"",
3658        );
3659        expect_action_error(
3660            r#"permit(principal, action in [Bar::Action::Foo::"view"], resource);"#,
3661            "expected an entity uid with type `Action` but got `Bar::Action::Foo::\"view\"`",
3662            "[Bar::Action::Foo::\"view\"]",
3663        );
3664        expect_action_error(
3665            r#"permit(principal, action in [Bar::Action::Foo::"view", Action::"check"], resource);"#,
3666            "expected an entity uid with type `Action` but got `Bar::Action::Foo::\"view\"`",
3667            "[Bar::Action::Foo::\"view\", Action::\"check\"]",
3668        );
3669        expect_action_error(
3670            r#"permit(principal, action in [Bar::Action::Foo::"view", Foo::"delete", Action::"check"], resource);"#,
3671            "expected entity uids with type `Action` but got `Bar::Action::Foo::\"view\"` and `Foo::\"delete\"`",
3672            "[Bar::Action::Foo::\"view\", Foo::\"delete\", Action::\"check\"]",
3673        );
3674    }
3675
3676    #[test]
3677    fn method_style() {
3678        let src = r#"permit(principal, action, resource)
3679            when { contains(true) < 1 };"#;
3680        assert_matches!(parse_policyset(src), Err(e) => {
3681            expect_n_errors(src, &e, 1);
3682            expect_some_error_matches(src, &e, &ExpectedErrorMessageBuilder::error(
3683                "`contains` is a method, not a function",
3684            ).help(
3685                "use a method-style call `e.contains(..)`",
3686            ).exactly_one_underline("contains(true)").build());
3687        });
3688    }
3689
3690    #[test]
3691    fn test_mul() {
3692        for (str, expected) in [
3693            ("--2*3", Expr::mul(Expr::neg(Expr::val(-2)), Expr::val(3))),
3694            (
3695                "1 * 2 * false",
3696                Expr::mul(Expr::mul(Expr::val(1), Expr::val(2)), Expr::val(false)),
3697            ),
3698            (
3699                "0 * 1 * principal",
3700                Expr::mul(
3701                    Expr::mul(Expr::val(0), Expr::val(1)),
3702                    Expr::var(ast::Var::Principal),
3703                ),
3704            ),
3705            (
3706                "0 * (-1) * principal",
3707                Expr::mul(
3708                    Expr::mul(Expr::val(0), Expr::val(-1)),
3709                    Expr::var(ast::Var::Principal),
3710                ),
3711            ),
3712            (
3713                "0 * 6 * context.foo",
3714                Expr::mul(
3715                    Expr::mul(Expr::val(0), Expr::val(6)),
3716                    Expr::get_attr(Expr::var(ast::Var::Context), "foo".into()),
3717                ),
3718            ),
3719            (
3720                "(0 * 6) * context.foo",
3721                Expr::mul(
3722                    Expr::mul(Expr::val(0), Expr::val(6)),
3723                    Expr::get_attr(Expr::var(ast::Var::Context), "foo".into()),
3724                ),
3725            ),
3726            (
3727                "0 * (6 * context.foo)",
3728                Expr::mul(
3729                    Expr::val(0),
3730                    Expr::mul(
3731                        Expr::val(6),
3732                        Expr::get_attr(Expr::var(ast::Var::Context), "foo".into()),
3733                    ),
3734                ),
3735            ),
3736            (
3737                "0 * (context.foo * 6)",
3738                Expr::mul(
3739                    Expr::val(0),
3740                    Expr::mul(
3741                        Expr::get_attr(Expr::var(ast::Var::Context), "foo".into()),
3742                        Expr::val(6),
3743                    ),
3744                ),
3745            ),
3746            (
3747                "1 * 2 * 3 * context.foo * 4 * 5 * 6",
3748                Expr::mul(
3749                    Expr::mul(
3750                        Expr::mul(
3751                            Expr::mul(
3752                                Expr::mul(Expr::mul(Expr::val(1), Expr::val(2)), Expr::val(3)),
3753                                Expr::get_attr(Expr::var(ast::Var::Context), "foo".into()),
3754                            ),
3755                            Expr::val(4),
3756                        ),
3757                        Expr::val(5),
3758                    ),
3759                    Expr::val(6),
3760                ),
3761            ),
3762            (
3763                "principal * (1 + 2)",
3764                Expr::mul(
3765                    Expr::var(ast::Var::Principal),
3766                    Expr::add(Expr::val(1), Expr::val(2)),
3767                ),
3768            ),
3769            (
3770                "principal * -(-1)",
3771                Expr::mul(Expr::var(ast::Var::Principal), Expr::neg(Expr::val(-1))),
3772            ),
3773            (
3774                "principal * --1",
3775                Expr::mul(Expr::var(ast::Var::Principal), Expr::neg(Expr::val(-1))),
3776            ),
3777            (
3778                r#"false * "bob""#,
3779                Expr::mul(Expr::val(false), Expr::val("bob")),
3780            ),
3781        ] {
3782            let e = assert_parse_expr_succeeds(str);
3783            assert!(
3784                e.eq_shape(&expected),
3785                "{e:?} and {expected:?} should have the same shape",
3786            );
3787        }
3788    }
3789
3790    #[test]
3791    fn test_not() {
3792        for (es, expr) in [
3793            (
3794                "!1 + 2 == 3",
3795                Expr::is_eq(
3796                    Expr::add(Expr::not(Expr::val(1)), Expr::val(2)),
3797                    Expr::val(3),
3798                ),
3799            ),
3800            (
3801                "!!1 + 2 == 3",
3802                Expr::is_eq(
3803                    Expr::add(Expr::not(Expr::not(Expr::val(1))), Expr::val(2)),
3804                    Expr::val(3),
3805                ),
3806            ),
3807            (
3808                "!!!1 + 2 == 3",
3809                Expr::is_eq(
3810                    Expr::add(Expr::not(Expr::not(Expr::not(Expr::val(1)))), Expr::val(2)),
3811                    Expr::val(3),
3812                ),
3813            ),
3814            (
3815                "!!!!1 + 2 == 3",
3816                Expr::is_eq(
3817                    Expr::add(
3818                        Expr::not(Expr::not(Expr::not(Expr::not(Expr::val(1))))),
3819                        Expr::val(2),
3820                    ),
3821                    Expr::val(3),
3822                ),
3823            ),
3824            (
3825                "!!(-1) + 2 == 3",
3826                Expr::is_eq(
3827                    Expr::add(Expr::not(Expr::not(Expr::val(-1))), Expr::val(2)),
3828                    Expr::val(3),
3829                ),
3830            ),
3831        ] {
3832            let e = assert_parse_expr_succeeds(es);
3833            assert!(
3834                e.eq_shape(&expr),
3835                "{e:?} and {expr:?} should have the same shape."
3836            );
3837        }
3838    }
3839
3840    #[test]
3841    fn test_neg() {
3842        for (es, expr) in [
3843            ("-(1 + 2)", Expr::neg(Expr::add(Expr::val(1), Expr::val(2)))),
3844            ("1-(2)", Expr::sub(Expr::val(1), Expr::val(2))),
3845            ("1-2", Expr::sub(Expr::val(1), Expr::val(2))),
3846            ("(-1)", Expr::val(-1)),
3847            ("-(-1)", Expr::neg(Expr::val(-1))),
3848            ("--1", Expr::neg(Expr::val(-1))),
3849            ("--(--1)", Expr::neg(Expr::neg(Expr::neg(Expr::val(-1))))),
3850            ("2--1", Expr::sub(Expr::val(2), Expr::val(-1))),
3851            ("-9223372036854775808", Expr::val(-(9223372036854775808))),
3852            // Evaluating this expression leads to overflows but the parser
3853            // won't reject it.
3854            (
3855                "--9223372036854775808",
3856                Expr::neg(Expr::val(-9223372036854775808)),
3857            ),
3858            (
3859                "-(9223372036854775807)",
3860                Expr::neg(Expr::val(9223372036854775807)),
3861            ),
3862        ] {
3863            let e = assert_parse_expr_succeeds(es);
3864            assert!(
3865                e.eq_shape(&expr),
3866                "{e:?} and {expr:?} should have the same shape."
3867            );
3868        }
3869
3870        for (es, em) in [
3871            (
3872                "-9223372036854775809",
3873                ExpectedErrorMessageBuilder::error(
3874                    "integer literal `9223372036854775809` is too large",
3875                )
3876                .help("maximum allowed integer literal is `9223372036854775807`")
3877                .exactly_one_underline("-9223372036854775809")
3878                .build(),
3879            ),
3880            // This test doesn't fail with an internal representation of i128:
3881            // Contrary to Rust, this expression is not valid because the
3882            // parser treats it as a negation operation whereas the operand
3883            // (9223372036854775808) is too large.
3884            (
3885                "-(9223372036854775808)",
3886                ExpectedErrorMessageBuilder::error(
3887                    "integer literal `9223372036854775808` is too large",
3888                )
3889                .help("maximum allowed integer literal is `9223372036854775807`")
3890                .exactly_one_underline("9223372036854775808")
3891                .build(),
3892            ),
3893        ] {
3894            let errs = assert_parse_expr_fails(es);
3895            expect_err(es, &miette::Report::new(errs), &em);
3896        }
3897    }
3898
3899    #[test]
3900    fn test_is_condition_ok() {
3901        for (es, expr) in [
3902            (
3903                r#"User::"alice" is User"#,
3904                Expr::is_entity_type(
3905                    Expr::val(r#"User::"alice""#.parse::<EntityUID>().unwrap()),
3906                    "User".parse().unwrap(),
3907                ),
3908            ),
3909            (
3910                r#"principal is User"#,
3911                Expr::is_entity_type(Expr::var(ast::Var::Principal), "User".parse().unwrap()),
3912            ),
3913            (
3914                r#"principal.foo is User"#,
3915                Expr::is_entity_type(
3916                    Expr::get_attr(Expr::var(ast::Var::Principal), "foo".into()),
3917                    "User".parse().unwrap(),
3918                ),
3919            ),
3920            (
3921                r#"1 is User"#,
3922                Expr::is_entity_type(Expr::val(1), "User".parse().unwrap()),
3923            ),
3924            (
3925                r#"principal is User in Group::"friends""#,
3926                Expr::and(
3927                    Expr::is_entity_type(Expr::var(ast::Var::Principal), "User".parse().unwrap()),
3928                    Expr::is_in(
3929                        Expr::var(ast::Var::Principal),
3930                        Expr::val(r#"Group::"friends""#.parse::<EntityUID>().unwrap()),
3931                    ),
3932                ),
3933            ),
3934            (
3935                r#"principal is User && principal in Group::"friends""#,
3936                Expr::and(
3937                    Expr::is_entity_type(Expr::var(ast::Var::Principal), "User".parse().unwrap()),
3938                    Expr::is_in(
3939                        Expr::var(ast::Var::Principal),
3940                        Expr::val(r#"Group::"friends""#.parse::<EntityUID>().unwrap()),
3941                    ),
3942                ),
3943            ),
3944            (
3945                r#"principal is User || principal in Group::"friends""#,
3946                Expr::or(
3947                    Expr::is_entity_type(Expr::var(ast::Var::Principal), "User".parse().unwrap()),
3948                    Expr::is_in(
3949                        Expr::var(ast::Var::Principal),
3950                        Expr::val(r#"Group::"friends""#.parse::<EntityUID>().unwrap()),
3951                    ),
3952                ),
3953            ),
3954            (
3955                r#"true && principal is User in principal"#,
3956                Expr::and(
3957                    Expr::val(true),
3958                    Expr::and(
3959                        Expr::is_entity_type(
3960                            Expr::var(ast::Var::Principal),
3961                            "User".parse().unwrap(),
3962                        ),
3963                        Expr::is_in(
3964                            Expr::var(ast::Var::Principal),
3965                            Expr::var(ast::Var::Principal),
3966                        ),
3967                    ),
3968                ),
3969            ),
3970            (
3971                r#"principal is User in principal && true"#,
3972                Expr::and(
3973                    Expr::and(
3974                        Expr::is_entity_type(
3975                            Expr::var(ast::Var::Principal),
3976                            "User".parse().unwrap(),
3977                        ),
3978                        Expr::is_in(
3979                            Expr::var(ast::Var::Principal),
3980                            Expr::var(ast::Var::Principal),
3981                        ),
3982                    ),
3983                    Expr::val(true),
3984                ),
3985            ),
3986            (
3987                r#"principal is A::B::C::User"#,
3988                Expr::is_entity_type(
3989                    Expr::var(ast::Var::Principal),
3990                    "A::B::C::User".parse().unwrap(),
3991                ),
3992            ),
3993            (
3994                r#"principal is A::B::C::User in Group::"friends""#,
3995                Expr::and(
3996                    Expr::is_entity_type(
3997                        Expr::var(ast::Var::Principal),
3998                        "A::B::C::User".parse().unwrap(),
3999                    ),
4000                    Expr::is_in(
4001                        Expr::var(ast::Var::Principal),
4002                        Expr::val(r#"Group::"friends""#.parse::<EntityUID>().unwrap()),
4003                    ),
4004                ),
4005            ),
4006            (
4007                r#"if principal is User then 1 else 2"#,
4008                Expr::ite(
4009                    Expr::is_entity_type(Expr::var(ast::Var::Principal), "User".parse().unwrap()),
4010                    Expr::val(1),
4011                    Expr::val(2),
4012                ),
4013            ),
4014            (
4015                r#"if principal is User in Group::"friends" then 1 else 2"#,
4016                Expr::ite(
4017                    Expr::and(
4018                        Expr::is_entity_type(
4019                            Expr::var(ast::Var::Principal),
4020                            "User".parse().unwrap(),
4021                        ),
4022                        Expr::is_in(
4023                            Expr::var(ast::Var::Principal),
4024                            Expr::val(r#"Group::"friends""#.parse::<EntityUID>().unwrap()),
4025                        ),
4026                    ),
4027                    Expr::val(1),
4028                    Expr::val(2),
4029                ),
4030            ),
4031            (
4032                r#"principal::"alice" is principal"#,
4033                Expr::is_entity_type(
4034                    Expr::val(r#"principal::"alice""#.parse::<EntityUID>().unwrap()),
4035                    "principal".parse().unwrap(),
4036                ),
4037            ),
4038            (
4039                r#"foo::principal::"alice" is foo::principal"#,
4040                Expr::is_entity_type(
4041                    Expr::val(r#"foo::principal::"alice""#.parse::<EntityUID>().unwrap()),
4042                    "foo::principal".parse().unwrap(),
4043                ),
4044            ),
4045            (
4046                r#"principal::foo::"alice" is principal::foo"#,
4047                Expr::is_entity_type(
4048                    Expr::val(r#"principal::foo::"alice""#.parse::<EntityUID>().unwrap()),
4049                    "principal::foo".parse().unwrap(),
4050                ),
4051            ),
4052            (
4053                r#"resource::"thing" is resource"#,
4054                Expr::is_entity_type(
4055                    Expr::val(r#"resource::"thing""#.parse::<EntityUID>().unwrap()),
4056                    "resource".parse().unwrap(),
4057                ),
4058            ),
4059            (
4060                r#"action::"do" is action"#,
4061                Expr::is_entity_type(
4062                    Expr::val(r#"action::"do""#.parse::<EntityUID>().unwrap()),
4063                    "action".parse().unwrap(),
4064                ),
4065            ),
4066            (
4067                r#"context::"stuff" is context"#,
4068                Expr::is_entity_type(
4069                    Expr::val(r#"context::"stuff""#.parse::<EntityUID>().unwrap()),
4070                    "context".parse().unwrap(),
4071                ),
4072            ),
4073        ] {
4074            let e = parse_expr(es).unwrap();
4075            assert!(
4076                e.eq_shape(&expr),
4077                "{e:?} and {expr:?} should have the same shape."
4078            );
4079        }
4080    }
4081
4082    #[test]
4083    fn is_scope() {
4084        for (src, p, a, r) in [
4085            (
4086                r#"permit(principal is User, action, resource);"#,
4087                PrincipalConstraint::is_entity_type(Arc::new("User".parse().unwrap())),
4088                ActionConstraint::any(),
4089                ResourceConstraint::any(),
4090            ),
4091            (
4092                r#"permit(principal is principal, action, resource);"#,
4093                PrincipalConstraint::is_entity_type(Arc::new("principal".parse().unwrap())),
4094                ActionConstraint::any(),
4095                ResourceConstraint::any(),
4096            ),
4097            (
4098                r#"permit(principal is A::User, action, resource);"#,
4099                PrincipalConstraint::is_entity_type(Arc::new("A::User".parse().unwrap())),
4100                ActionConstraint::any(),
4101                ResourceConstraint::any(),
4102            ),
4103            (
4104                r#"permit(principal is User in Group::"thing", action, resource);"#,
4105                PrincipalConstraint::is_entity_type_in(
4106                    Arc::new("User".parse().unwrap()),
4107                    Arc::new(r#"Group::"thing""#.parse().unwrap()),
4108                ),
4109                ActionConstraint::any(),
4110                ResourceConstraint::any(),
4111            ),
4112            (
4113                r#"permit(principal is principal in Group::"thing", action, resource);"#,
4114                PrincipalConstraint::is_entity_type_in(
4115                    Arc::new("principal".parse().unwrap()),
4116                    Arc::new(r#"Group::"thing""#.parse().unwrap()),
4117                ),
4118                ActionConstraint::any(),
4119                ResourceConstraint::any(),
4120            ),
4121            (
4122                r#"permit(principal is A::User in Group::"thing", action, resource);"#,
4123                PrincipalConstraint::is_entity_type_in(
4124                    Arc::new("A::User".parse().unwrap()),
4125                    Arc::new(r#"Group::"thing""#.parse().unwrap()),
4126                ),
4127                ActionConstraint::any(),
4128                ResourceConstraint::any(),
4129            ),
4130            (
4131                r#"permit(principal is User in ?principal, action, resource);"#,
4132                PrincipalConstraint::is_entity_type_in_slot(Arc::new("User".parse().unwrap())),
4133                ActionConstraint::any(),
4134                ResourceConstraint::any(),
4135            ),
4136            (
4137                r#"permit(principal, action, resource is Folder);"#,
4138                PrincipalConstraint::any(),
4139                ActionConstraint::any(),
4140                ResourceConstraint::is_entity_type(Arc::new("Folder".parse().unwrap())),
4141            ),
4142            (
4143                r#"permit(principal, action, resource is Folder in Folder::"inner");"#,
4144                PrincipalConstraint::any(),
4145                ActionConstraint::any(),
4146                ResourceConstraint::is_entity_type_in(
4147                    Arc::new("Folder".parse().unwrap()),
4148                    Arc::new(r#"Folder::"inner""#.parse().unwrap()),
4149                ),
4150            ),
4151            (
4152                r#"permit(principal, action, resource is Folder in ?resource);"#,
4153                PrincipalConstraint::any(),
4154                ActionConstraint::any(),
4155                ResourceConstraint::is_entity_type_in_slot(Arc::new("Folder".parse().unwrap())),
4156            ),
4157        ] {
4158            let policy = parse_policy_or_template(None, src).unwrap();
4159            assert_eq!(policy.principal_constraint(), &p);
4160            assert_eq!(policy.action_constraint(), &a);
4161            assert_eq!(policy.resource_constraint(), &r);
4162        }
4163    }
4164
4165    #[test]
4166    fn is_err() {
4167        let invalid_is_policies = [
4168            (
4169                r#"permit(principal in Group::"friends" is User, action, resource);"#,
4170                ExpectedErrorMessageBuilder::error("when `is` and `in` are used together, `is` must come first")
4171                    .help("try `_ is _ in _`")
4172                    .exactly_one_underline(r#"principal in Group::"friends" is User"#)
4173                    .build(),
4174            ),
4175            (
4176                r#"permit(principal, action in Group::"action_group" is Action, resource);"#,
4177                ExpectedErrorMessageBuilder::error("`is` cannot appear in the action scope")
4178                    .help("try moving `action is ..` into a `when` condition")
4179                    .exactly_one_underline(r#"action in Group::"action_group" is Action"#)
4180                    .build(),
4181            ),
4182            (
4183                r#"permit(principal, action, resource in Folder::"folder" is File);"#,
4184                ExpectedErrorMessageBuilder::error("when `is` and `in` are used together, `is` must come first")
4185                    .help("try `_ is _ in _`")
4186                    .exactly_one_underline(r#"resource in Folder::"folder" is File"#)
4187                    .build(),
4188            ),
4189            (
4190                r#"permit(principal is User == User::"Alice", action, resource);"#,
4191                ExpectedErrorMessageBuilder::error(
4192                    "`is` cannot be used together with `==`",
4193                ).help(
4194                    "try using `_ is _ in _`"
4195                ).exactly_one_underline("principal is User == User::\"Alice\"").build(),
4196            ),
4197            (
4198                r#"permit(principal, action, resource is Doc == Doc::"a");"#,
4199                ExpectedErrorMessageBuilder::error(
4200                    "`is` cannot be used together with `==`",
4201                ).help(
4202                    "try using `_ is _ in _`"
4203                ).exactly_one_underline("resource is Doc == Doc::\"a\"").build(),
4204            ),
4205            (
4206                r#"permit(principal is User::"alice", action, resource);"#,
4207                ExpectedErrorMessageBuilder::error(
4208                    r#"right hand side of an `is` expression must be an entity type name, but got `User::"alice"`"#,
4209                ).help(r#"try using `==` to test for equality: `principal == User::"alice"`"#)
4210                .exactly_one_underline("User::\"alice\"").build(),
4211            ),
4212            (
4213                r#"permit(principal, action, resource is File::"f");"#,
4214                ExpectedErrorMessageBuilder::error(
4215                    r#"right hand side of an `is` expression must be an entity type name, but got `File::"f"`"#,
4216                ).help(r#"try using `==` to test for equality: `resource == File::"f"`"#)
4217                .exactly_one_underline("File::\"f\"").build(),
4218            ),
4219            (
4220                r#"permit(principal is User in 1, action, resource);"#,
4221                ExpectedErrorMessageBuilder::error(
4222                    "expected an entity uid or matching template slot, found literal `1`",
4223                ).exactly_one_underline("1").build(),
4224            ),
4225            (
4226                r#"permit(principal, action, resource is File in 1);"#,
4227                ExpectedErrorMessageBuilder::error(
4228                    "expected an entity uid or matching template slot, found literal `1`",
4229                ).exactly_one_underline("1").build(),
4230            ),
4231            (
4232                r#"permit(principal is User in User, action, resource);"#,
4233                ExpectedErrorMessageBuilder::error(
4234                    "expected an entity uid or matching template slot, found name `User`",
4235                )
4236                .help(
4237                    "try using `is` to test for an entity type or including an identifier string if you intended this name to be an entity uid"
4238                )
4239                .exactly_one_underline("User").build(),
4240            ),
4241            (
4242                r#"permit(principal is User::"Alice" in Group::"f", action, resource);"#,
4243                ExpectedErrorMessageBuilder::error(
4244                    r#"right hand side of an `is` expression must be an entity type name, but got `User::"Alice"`"#,
4245                ).help(r#"try using `==` to test for equality: `principal == User::"Alice"`"#)
4246                .exactly_one_underline("User::\"Alice\"").build(),
4247            ),
4248            (
4249                r#"permit(principal, action, resource is File in File);"#,
4250                ExpectedErrorMessageBuilder::error(
4251                    "expected an entity uid or matching template slot, found name `File`",
4252                )
4253                .help(
4254                    "try using `is` to test for an entity type or including an identifier string if you intended this name to be an entity uid"
4255                )
4256                .exactly_one_underline("File").build(),
4257            ),
4258            (
4259                r#"permit(principal, action, resource is File::"file" in Folder::"folder");"#,
4260                ExpectedErrorMessageBuilder::error(
4261                    r#"right hand side of an `is` expression must be an entity type name, but got `File::"file"`"#,
4262                ).help(
4263                    r#"try using `==` to test for equality: `resource == File::"file"`"#
4264                ).exactly_one_underline("File::\"file\"").build(),
4265            ),
4266            (
4267                r#"permit(principal is 1, action, resource);"#,
4268                ExpectedErrorMessageBuilder::error(
4269                    r#"right hand side of an `is` expression must be an entity type name, but got `1`"#,
4270                ).help(
4271                    "try using `==` to test for equality: `principal == 1`"
4272                ).exactly_one_underline("1").build(),
4273            ),
4274            (
4275                r#"permit(principal, action, resource is 1);"#,
4276                ExpectedErrorMessageBuilder::error(
4277                    r#"right hand side of an `is` expression must be an entity type name, but got `1`"#,
4278                ).help(
4279                    "try using `==` to test for equality: `resource == 1`"
4280                ).exactly_one_underline("1").build(),
4281            ),
4282            (
4283                r#"permit(principal, action is Action, resource);"#,
4284                ExpectedErrorMessageBuilder::error(
4285                    "`is` cannot appear in the action scope",
4286                ).help(
4287                    "try moving `action is ..` into a `when` condition"
4288                ).exactly_one_underline("action is Action").build(),
4289            ),
4290            (
4291                r#"permit(principal, action is Action::"a", resource);"#,
4292                ExpectedErrorMessageBuilder::error(
4293                    "`is` cannot appear in the action scope",
4294                ).help(
4295                    "try moving `action is ..` into a `when` condition"
4296                ).exactly_one_underline("action is Action::\"a\"").build(),
4297            ),
4298            (
4299                r#"permit(principal, action is Action in Action::"A", resource);"#,
4300                ExpectedErrorMessageBuilder::error(
4301                    "`is` cannot appear in the action scope",
4302                ).help(
4303                    "try moving `action is ..` into a `when` condition"
4304                ).exactly_one_underline("action is Action in Action::\"A\"").build(),
4305            ),
4306            (
4307                r#"permit(principal, action is Action in Action, resource);"#,
4308                ExpectedErrorMessageBuilder::error(
4309                    "`is` cannot appear in the action scope",
4310                ).help(
4311                    "try moving `action is ..` into a `when` condition"
4312                ).exactly_one_underline("action is Action in Action").build(),
4313            ),
4314            (
4315                r#"permit(principal, action is Action::"a" in Action::"b", resource);"#,
4316                ExpectedErrorMessageBuilder::error(
4317                    "`is` cannot appear in the action scope",
4318                ).help(
4319                    "try moving `action is ..` into a `when` condition"
4320                ).exactly_one_underline("action is Action::\"a\" in Action::\"b\"").build(),
4321            ),
4322            (
4323                r#"permit(principal, action is Action in ?action, resource);"#,
4324                ExpectedErrorMessageBuilder::error(
4325                    "`is` cannot appear in the action scope",
4326                ).help(
4327                    "try moving `action is ..` into a `when` condition"
4328                ).exactly_one_underline("action is Action in ?action").build(),
4329            ),
4330            (
4331                r#"permit(principal, action is ?action, resource);"#,
4332                ExpectedErrorMessageBuilder::error(
4333                    "`is` cannot appear in the action scope",
4334                ).help(
4335                    "try moving `action is ..` into a `when` condition"
4336                ).exactly_one_underline("action is ?action").build(),
4337            ),
4338            (
4339                r#"permit(principal is User in ?resource, action, resource);"#,
4340                ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?resource instead of ?principal").exactly_one_underline("?resource").build(),
4341            ),
4342            (
4343                r#"permit(principal, action, resource is Folder in ?principal);"#,
4344                ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?principal instead of ?resource").exactly_one_underline("?principal").build(),
4345            ),
4346            (
4347                r#"permit(principal is ?principal, action, resource);"#,
4348                ExpectedErrorMessageBuilder::error(
4349                    "right hand side of an `is` expression must be an entity type name, but got `?principal`",
4350                ).help(
4351                    "try using `==` to test for equality: `principal == ?principal`"
4352                ).exactly_one_underline("?principal").build(),
4353            ),
4354            (
4355                r#"permit(principal, action, resource is ?resource);"#,
4356                ExpectedErrorMessageBuilder::error(
4357                    "right hand side of an `is` expression must be an entity type name, but got `?resource`",
4358                ).help(
4359                    "try using `==` to test for equality: `resource == ?resource`"
4360                ).exactly_one_underline("?resource").build(),
4361            ),
4362            (
4363                r#"permit(principal, action, resource) when { principal is 1 };"#,
4364                ExpectedErrorMessageBuilder::error(
4365                    r#"right hand side of an `is` expression must be an entity type name, but got `1`"#,
4366                ).help(
4367                    "try using `==` to test for equality: `principal == 1`"
4368                ).exactly_one_underline("1").build(),
4369            ),
4370            (
4371                r#"permit(principal, action, resource) when { principal is User::"alice" in Group::"friends" };"#,
4372                ExpectedErrorMessageBuilder::error(
4373                    r#"right hand side of an `is` expression must be an entity type name, but got `User::"alice"`"#,
4374                ).help(
4375                    r#"try using `==` to test for equality: `principal == User::"alice"`"#
4376                ).exactly_one_underline("User::\"alice\"").build(),
4377            ),
4378            (
4379                r#"permit(principal, action, resource) when { principal is ! User::"alice" in Group::"friends" };"#,
4380                ExpectedErrorMessageBuilder::error(
4381                    r#"right hand side of an `is` expression must be an entity type name, but got `! User::"alice"`"#,
4382                ).help(
4383                    r#"try using `==` to test for equality: `principal == ! User::"alice"`"#
4384                ).exactly_one_underline("! User::\"alice\"").build(),
4385            ),
4386            (
4387                r#"permit(principal, action, resource) when { principal is User::"alice" + User::"alice" in Group::"friends" };"#,
4388                ExpectedErrorMessageBuilder::error(
4389                    r#"right hand side of an `is` expression must be an entity type name, but got `User::"alice" + User::"alice"`"#,
4390                ).help(
4391                    r#"try using `==` to test for equality: `principal == User::"alice" + User::"alice"`"#
4392                ).exactly_one_underline("User::\"alice\" + User::\"alice\"").build(),
4393            ),
4394            (
4395                r#"permit(principal, action, resource) when { principal is User in User::"alice" in Group::"friends" };"#,
4396                ExpectedErrorMessageBuilder::error("unexpected token `in`")
4397                    .exactly_one_underline_with_label("in", "expected `&&`, `||`, or `}`")
4398                    .build(),
4399            ),
4400            (
4401                r#"permit(principal, action, resource) when { principal is User == User::"alice" in Group::"friends" };"#,
4402                ExpectedErrorMessageBuilder::error("unexpected token `==`")
4403                    .exactly_one_underline_with_label("==", "expected `&&`, `||`, `}`, or `in`")
4404                    .build(),
4405            ),
4406            (
4407                // `_ in _ is _` in the policy condition is an error in the text->CST parser
4408                r#"permit(principal, action, resource) when { principal in Group::"friends" is User };"#,
4409                ExpectedErrorMessageBuilder::error("unexpected token `is`")
4410                    .exactly_one_underline_with_label(r#"is"#, "expected `!=`, `&&`, `<`, `<=`, `==`, `>`, `>=`, `||`, `}`, or `in`")
4411                    .build(),
4412            ),
4413            (
4414                r#"permit(principal is "User", action, resource);"#,
4415                ExpectedErrorMessageBuilder::error(
4416                    r#"right hand side of an `is` expression must be an entity type name, but got `"User"`"#,
4417                ).help(
4418                    "try removing the quotes: `principal is User`"
4419                ).exactly_one_underline("\"User\"").build(),
4420            ),
4421            (
4422                r#"permit(principal, action, resource) when { principal is "User" };"#,
4423                ExpectedErrorMessageBuilder::error(
4424                    r#"right hand side of an `is` expression must be an entity type name, but got `"User"`"#,
4425                ).help(
4426                    "try removing the quotes: `principal is User`"
4427                ).exactly_one_underline("\"User\"").build(),
4428            ),
4429        ];
4430        for (p_src, expected) in invalid_is_policies {
4431            assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4432                expect_err(p_src, &miette::Report::new(e), &expected);
4433            });
4434        }
4435    }
4436
4437    #[test]
4438    fn issue_255() {
4439        let policy = r#"
4440            permit (
4441                principal == name-with-dashes::"Alice",
4442                action,
4443                resource
4444            );
4445        "#;
4446        assert_matches!(
4447            parse_policy(None, policy),
4448            Err(e) => {
4449                expect_n_errors(policy, &e, 1);
4450                expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
4451                    "expected an entity uid or matching template slot, found a `+/-` expression",
4452                ).help(
4453                    "entity types and namespaces cannot use `+` or `-` characters -- perhaps try `_` or `::` instead?",
4454                ).exactly_one_underline("name-with-dashes::\"Alice\"").build());
4455            }
4456        );
4457    }
4458
4459    #[test]
4460    fn invalid_methods_function_calls() {
4461        let invalid_exprs = [
4462            (
4463                r#"contains([], 1)"#,
4464                ExpectedErrorMessageBuilder::error("`contains` is a method, not a function")
4465                    .help("use a method-style call `e.contains(..)`")
4466                    .exactly_one_underline("contains([], 1)")
4467                    .build(),
4468            ),
4469            (
4470                r#"[].contains()"#,
4471                ExpectedErrorMessageBuilder::error(
4472                    "call to `contains` requires exactly 1 argument, but got 0 arguments",
4473                )
4474                .exactly_one_underline("[].contains()")
4475                .build(),
4476            ),
4477            (
4478                r#"[].contains(1, 2)"#,
4479                ExpectedErrorMessageBuilder::error(
4480                    "call to `contains` requires exactly 1 argument, but got 2 arguments",
4481                )
4482                .exactly_one_underline("[].contains(1, 2)")
4483                .build(),
4484            ),
4485            (
4486                r#"[].containsAll()"#,
4487                ExpectedErrorMessageBuilder::error(
4488                    "call to `containsAll` requires exactly 1 argument, but got 0 arguments",
4489                )
4490                .exactly_one_underline("[].containsAll()")
4491                .build(),
4492            ),
4493            (
4494                r#"[].containsAll(1, 2)"#,
4495                ExpectedErrorMessageBuilder::error(
4496                    "call to `containsAll` requires exactly 1 argument, but got 2 arguments",
4497                )
4498                .exactly_one_underline("[].containsAll(1, 2)")
4499                .build(),
4500            ),
4501            (
4502                r#"[].containsAny()"#,
4503                ExpectedErrorMessageBuilder::error(
4504                    "call to `containsAny` requires exactly 1 argument, but got 0 arguments",
4505                )
4506                .exactly_one_underline("[].containsAny()")
4507                .build(),
4508            ),
4509            (
4510                r#"[].containsAny(1, 2)"#,
4511                ExpectedErrorMessageBuilder::error(
4512                    "call to `containsAny` requires exactly 1 argument, but got 2 arguments",
4513                )
4514                .exactly_one_underline("[].containsAny(1, 2)")
4515                .build(),
4516            ),
4517            (
4518                r#"[].isEmpty([])"#,
4519                ExpectedErrorMessageBuilder::error(
4520                    "call to `isEmpty` requires exactly 0 arguments, but got 1 argument",
4521                )
4522                .exactly_one_underline("[].isEmpty([])")
4523                .build(),
4524            ),
4525            (
4526                r#""1.1.1.1".ip()"#,
4527                ExpectedErrorMessageBuilder::error("`ip` is a function, not a method")
4528                    .help("use a function-style call `ip(..)`")
4529                    .exactly_one_underline(r#""1.1.1.1".ip()"#)
4530                    .build(),
4531            ),
4532            (
4533                r#"greaterThan(1, 2)"#,
4534                ExpectedErrorMessageBuilder::error("`greaterThan` is a method, not a function")
4535                    .help("use a method-style call `e.greaterThan(..)`")
4536                    .exactly_one_underline("greaterThan(1, 2)")
4537                    .build(),
4538            ),
4539            (
4540                "[].bar()",
4541                ExpectedErrorMessageBuilder::error("`bar` is not a valid method")
4542                    .exactly_one_underline("[].bar()")
4543                    .build(),
4544            ),
4545            (
4546                "principal.addr.isipv4()",
4547                ExpectedErrorMessageBuilder::error("`isipv4` is not a valid method")
4548                    .exactly_one_underline("principal.addr.isipv4()")
4549                    .help("did you mean `isIpv4`?")
4550                    .build(),
4551            ),
4552            (
4553                "bar([])",
4554                ExpectedErrorMessageBuilder::error("`bar` is not a valid function")
4555                    .exactly_one_underline("bar([])")
4556                    .help("did you mean `ip`?")
4557                    .build(),
4558            ),
4559            (
4560                r#"Ip("1.1.1.1/24")"#,
4561                ExpectedErrorMessageBuilder::error("`Ip` is not a valid function")
4562                    .exactly_one_underline(r#"Ip("1.1.1.1/24")"#)
4563                    .help("did you mean `ip`?")
4564                    .build(),
4565            ),
4566            (
4567                "principal()",
4568                ExpectedErrorMessageBuilder::error("`principal(...)` is not a valid function call")
4569                    .help("variables cannot be called as functions")
4570                    .exactly_one_underline("principal()")
4571                    .build(),
4572            ),
4573            (
4574                "(1+1)()",
4575                ExpectedErrorMessageBuilder::error(
4576                    "function calls must be of the form `<name>(arg1, arg2, ...)`",
4577                )
4578                .exactly_one_underline("(1+1)()")
4579                .build(),
4580            ),
4581            (
4582                "foo.bar()",
4583                ExpectedErrorMessageBuilder::error(
4584                    "attempted to call `foo.bar(...)`, but `foo` does not have any methods",
4585                )
4586                .exactly_one_underline("foo.bar()")
4587                .build(),
4588            ),
4589        ];
4590        for (src, expected) in invalid_exprs {
4591            assert_matches!(parse_expr(src), Err(e) => {
4592                expect_err(src, &miette::Report::new(e), &expected);
4593            });
4594        }
4595    }
4596
4597    #[test]
4598    fn invalid_slot() {
4599        let invalid_policies = [
4600            (
4601                r#"permit(principal == ?resource, action, resource);"#,
4602                ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?resource instead of ?principal").exactly_one_underline("?resource").build(),
4603            ),
4604            (
4605                r#"permit(principal in ?resource, action, resource);"#,
4606                ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?resource instead of ?principal").exactly_one_underline("?resource").build(),
4607            ),
4608            (
4609                r#"permit(principal == ?foo, action, resource);"#,
4610                ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?foo instead of ?principal").exactly_one_underline("?foo").build(),
4611            ),
4612            (
4613                r#"permit(principal in ?foo, action, resource);"#,
4614                ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?foo instead of ?principal").exactly_one_underline("?foo").build(),
4615            ),
4616
4617            (
4618                r#"permit(principal, action, resource == ?principal);"#,
4619                ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?principal instead of ?resource").exactly_one_underline("?principal").build(),
4620            ),
4621            (
4622                r#"permit(principal, action, resource in ?principal);"#,
4623                ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?principal instead of ?resource").exactly_one_underline("?principal").build(),
4624            ),
4625            (
4626                r#"permit(principal, action, resource == ?baz);"#,
4627                ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?baz instead of ?resource").exactly_one_underline("?baz").build(),
4628            ),
4629            (
4630                r#"permit(principal, action, resource in ?baz);"#,
4631                ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?baz instead of ?resource").exactly_one_underline("?baz").build(),
4632            ),
4633            (
4634                r#"permit(principal, action, resource) when { principal == ?foo};"#,
4635                ExpectedErrorMessageBuilder::error(
4636                    "`?foo` is not a valid template slot",
4637                ).help(
4638                    "a template slot may only be `?principal` or `?resource`",
4639                ).exactly_one_underline("?foo").build(),
4640            ),
4641
4642            (
4643                r#"permit(principal, action == ?action, resource);"#,
4644                ExpectedErrorMessageBuilder::error("expected single entity uid, found template slot").exactly_one_underline("?action").build(),
4645            ),
4646            (
4647                r#"permit(principal, action in ?action, resource);"#,
4648                ExpectedErrorMessageBuilder::error("expected single entity uid or set of entity uids, found template slot").exactly_one_underline("?action").build(),
4649            ),
4650            (
4651                r#"permit(principal, action == ?principal, resource);"#,
4652                ExpectedErrorMessageBuilder::error("expected single entity uid, found template slot").exactly_one_underline("?principal").build(),
4653            ),
4654            (
4655                r#"permit(principal, action in ?principal, resource);"#,
4656                ExpectedErrorMessageBuilder::error("expected single entity uid or set of entity uids, found template slot").exactly_one_underline("?principal").build(),
4657            ),
4658            (
4659                r#"permit(principal, action == ?resource, resource);"#,
4660                ExpectedErrorMessageBuilder::error("expected single entity uid, found template slot").exactly_one_underline("?resource").build(),
4661            ),
4662            (
4663                r#"permit(principal, action in ?resource, resource);"#,
4664                ExpectedErrorMessageBuilder::error("expected single entity uid or set of entity uids, found template slot").exactly_one_underline("?resource").build(),
4665            ),
4666            (
4667                r#"permit(principal, action in [?bar], resource);"#,
4668                ExpectedErrorMessageBuilder::error("expected single entity uid, found template slot").exactly_one_underline("?bar").build(),
4669            ),
4670        ];
4671
4672        for (p_src, expected) in invalid_policies {
4673            assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4674                expect_err(p_src, &miette::Report::new(e), &expected);
4675            });
4676            let forbid_src = format!("forbid{}", &p_src[6..]);
4677            assert_matches!(parse_policy_or_template(None, &forbid_src), Err(e) => {
4678                expect_err(forbid_src.as_str(), &miette::Report::new(e), &expected);
4679            });
4680        }
4681    }
4682
4683    #[test]
4684    fn missing_scope_constraint() {
4685        let p_src = "permit();";
4686        assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4687            expect_err(
4688                p_src,
4689                &miette::Report::new(e),
4690                &ExpectedErrorMessageBuilder::error("this policy is missing the `principal` variable in the scope")
4691                    .exactly_one_underline("")
4692                    .help("policy scopes must contain a `principal`, `action`, and `resource` element in that order")
4693                    .build()
4694            );
4695        });
4696        let p_src = "permit(principal);";
4697        assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4698            expect_err(
4699                p_src,
4700                &miette::Report::new(e),
4701                &ExpectedErrorMessageBuilder::error("this policy is missing the `action` variable in the scope")
4702                    .exactly_one_underline("")
4703                    .help("policy scopes must contain a `principal`, `action`, and `resource` element in that order")
4704                    .build()
4705            );
4706        });
4707        let p_src = "permit(principal, action);";
4708        assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4709            expect_err(
4710                p_src,
4711                &miette::Report::new(e),
4712                &ExpectedErrorMessageBuilder::error("this policy is missing the `resource` variable in the scope")
4713                    .exactly_one_underline("")
4714                    .help("policy scopes must contain a `principal`, `action`, and `resource` element in that order")
4715                    .build()
4716            );
4717        });
4718    }
4719
4720    #[test]
4721    fn invalid_scope_constraint() {
4722        let p_src = "permit(foo, action, resource);";
4723        assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4724            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4725                "found an invalid variable in the policy scope: foo",
4726                ).help(
4727                "policy scopes must contain a `principal`, `action`, and `resource` element in that order",
4728            ).exactly_one_underline("foo").build());
4729        });
4730        let p_src = "permit(foo::principal, action, resource);";
4731        assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4732            expect_err(
4733                p_src,
4734                &miette::Report::new(e),
4735                &ExpectedErrorMessageBuilder::error("unexpected token `::`")
4736                    .exactly_one_underline_with_label("::", "expected `!=`, `)`, `,`, `:`, `<`, `<=`, `==`, `>`, `>=`, `in`, or `is`")
4737                    .build()
4738            );
4739        });
4740        let p_src = "permit(resource, action, resource);";
4741        assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4742            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4743                "found the variable `resource` where the variable `principal` must be used",
4744                ).help(
4745                "policy scopes must contain a `principal`, `action`, and `resource` element in that order",
4746            ).exactly_one_underline("resource").build());
4747        });
4748
4749        let p_src = "permit(principal, principal, resource);";
4750        assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4751            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4752                "found the variable `principal` where the variable `action` must be used",
4753                ).help(
4754                "policy scopes must contain a `principal`, `action`, and `resource` element in that order",
4755            ).exactly_one_underline("principal").build());
4756        });
4757        let p_src = "permit(principal, if, resource);";
4758        assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4759            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4760                "found an invalid variable in the policy scope: if",
4761                ).help(
4762                "policy scopes must contain a `principal`, `action`, and `resource` element in that order",
4763            ).exactly_one_underline("if").build());
4764        });
4765
4766        let p_src = "permit(principal, action, like);";
4767        assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4768            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4769                "found an invalid variable in the policy scope: like",
4770                ).help(
4771                "policy scopes must contain a `principal`, `action`, and `resource` element in that order",
4772            ).exactly_one_underline("like").build());
4773        });
4774        let p_src = "permit(principal, action, principal);";
4775        assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4776            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4777                "found the variable `principal` where the variable `resource` must be used",
4778                ).help(
4779                "policy scopes must contain a `principal`, `action`, and `resource` element in that order",
4780            ).exactly_one_underline("principal").build());
4781        });
4782        let p_src = "permit(principal, action, action);";
4783        assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4784            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4785                "found the variable `action` where the variable `resource` must be used",
4786                ).help(
4787                "policy scopes must contain a `principal`, `action`, and `resource` element in that order",
4788            ).exactly_one_underline("action").build());
4789        });
4790    }
4791
4792    #[test]
4793    fn invalid_scope_operator() {
4794        let p_src = r#"permit(principal > User::"alice", action, resource);"#;
4795        assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4796            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4797                "invalid operator in the policy scope: >",
4798                ).help(
4799                "policy scope clauses can only use `==`, `in`, `is`, or `_ is _ in _`"
4800            ).exactly_one_underline("principal > User::\"alice\"").build());
4801        });
4802        let p_src = r#"permit(principal, action != Action::"view", resource);"#;
4803        assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4804            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4805                "invalid operator in the action scope: !=",
4806                ).help(
4807                "action scope clauses can only use `==` or `in`"
4808            ).exactly_one_underline("action != Action::\"view\"").build());
4809        });
4810        let p_src = r#"permit(principal, action, resource <= Folder::"things");"#;
4811        assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4812            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4813                "invalid operator in the policy scope: <=",
4814                ).help(
4815                "policy scope clauses can only use `==`, `in`, `is`, or `_ is _ in _`"
4816            ).exactly_one_underline("resource <= Folder::\"things\"").build());
4817        });
4818        let p_src = r#"permit(principal = User::"alice", action, resource);"#;
4819        assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4820            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4821                "'=' is not a valid operator in Cedar",
4822                ).help(
4823                "try using '==' instead",
4824            ).exactly_one_underline("principal = User::\"alice\"").build());
4825        });
4826        let p_src = r#"permit(principal, action = Action::"act", resource);"#;
4827        assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4828            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4829                "'=' is not a valid operator in Cedar",
4830                ).help(
4831                "try using '==' instead",
4832            ).exactly_one_underline("action = Action::\"act\"").build());
4833        });
4834        let p_src = r#"permit(principal, action, resource = Photo::"photo");"#;
4835        assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4836            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4837                "'=' is not a valid operator in Cedar",
4838                ).help(
4839                "try using '==' instead",
4840            ).exactly_one_underline("resource = Photo::\"photo\"").build());
4841        });
4842    }
4843
4844    #[test]
4845    fn scope_action_eq_set() {
4846        let p_src = r#"permit(principal, action == [Action::"view", Action::"edit"], resource);"#;
4847        assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4848            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("expected single entity uid, found set of entity uids").exactly_one_underline(r#"[Action::"view", Action::"edit"]"#).build());
4849        });
4850    }
4851
4852    #[test]
4853    fn scope_compare_to_string() {
4854        let p_src = r#"permit(principal == "alice", action, resource);"#;
4855        assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4856            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4857                r#"expected an entity uid or matching template slot, found literal `"alice"`"#
4858            ).help(
4859                "try including the entity type if you intended this string to be an entity uid"
4860            ).exactly_one_underline(r#""alice""#).build());
4861        });
4862        let p_src = r#"permit(principal in "bob_friends", action, resource);"#;
4863        assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4864            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4865                r#"expected an entity uid or matching template slot, found literal `"bob_friends"`"#
4866            ).help(
4867                "try including the entity type if you intended this string to be an entity uid"
4868            ).exactly_one_underline(r#""bob_friends""#).build());
4869        });
4870        let p_src = r#"permit(principal, action, resource in "jane_photos");"#;
4871        assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4872            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4873                r#"expected an entity uid or matching template slot, found literal `"jane_photos"`"#
4874            ).help(
4875                "try including the entity type if you intended this string to be an entity uid"
4876            ).exactly_one_underline(r#""jane_photos""#).build());
4877        });
4878        let p_src = r#"permit(principal, action in ["view_actions"], resource);"#;
4879        assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4880            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4881                r#"expected an entity uid, found literal `"view_actions"`"#
4882            ).help(
4883                "try including the entity type if you intended this string to be an entity uid"
4884            ).exactly_one_underline(r#""view_actions""#).build());
4885        });
4886    }
4887
4888    #[test]
4889    fn scope_compare_to_name() {
4890        let p_src = r#"permit(principal == User, action, resource);"#;
4891        assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4892            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4893                "expected an entity uid or matching template slot, found name `User`"
4894            ).help(
4895                    "try using `is` to test for an entity type or including an identifier string if you intended this name to be an entity uid"
4896            ).exactly_one_underline("User").build());
4897        });
4898        let p_src = r#"permit(principal in Group, action, resource);"#;
4899        assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4900            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4901                "expected an entity uid or matching template slot, found name `Group`"
4902            ).help(
4903                "try using `is` to test for an entity type or including an identifier string if you intended this name to be an entity uid"
4904            ).exactly_one_underline("Group").build());
4905        });
4906        let p_src = r#"permit(principal, action, resource in Album);"#;
4907        assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4908            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4909                "expected an entity uid or matching template slot, found name `Album`"
4910            ).help(
4911                "try using `is` to test for an entity type or including an identifier string if you intended this name to be an entity uid"
4912            ).exactly_one_underline("Album").build());
4913        });
4914        let p_src = r#"permit(principal, action == Action, resource);"#;
4915        assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4916            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4917                "expected an entity uid, found name `Action`"
4918            ).help(
4919                "try including an identifier string if you intended this name to be an entity uid"
4920            ).exactly_one_underline("Action").build());
4921        });
4922    }
4923
4924    #[test]
4925    fn scope_and() {
4926        let p_src = r#"permit(principal == User::"alice" && principal in Group::"jane_friends", action, resource);"#;
4927        assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4928            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4929                "expected an entity uid or matching template slot, found a `&&` expression"
4930            ).help(
4931                "the policy scope can only contain one constraint per variable. Consider moving the second operand of this `&&` into a `when` condition",
4932            ).exactly_one_underline(r#"User::"alice" && principal in Group::"jane_friends""#).build());
4933        });
4934    }
4935
4936    #[test]
4937    fn scope_or() {
4938        let p_src =
4939            r#"permit(principal == User::"alice" || principal == User::"bob", action, resource);"#;
4940        assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4941            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4942                "expected an entity uid or matching template slot, found a `||` expression"
4943            ).help(
4944                "the policy scope can only contain one constraint per variable. Consider moving the second operand of this `||` into a new policy",
4945            ).exactly_one_underline(r#"User::"alice" || principal == User::"bob""#).build());
4946        });
4947    }
4948
4949    #[test]
4950    fn scope_action_in_set_set() {
4951        let p_src = r#"permit(principal, action in [[Action::"view"]], resource);"#;
4952        assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4953            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("expected single entity uid, found set of entity uids").exactly_one_underline(r#"[Action::"view"]"#).build());
4954        });
4955    }
4956
4957    #[test]
4958    fn scope_unexpected_nested_sets() {
4959        let policy = r#"
4960            permit (
4961                principal == [[User::"alice"]],
4962                action,
4963                resource
4964            );
4965        "#;
4966        assert_matches!(
4967            parse_policy(None, policy),
4968            Err(e) => {
4969                expect_n_errors(policy, &e, 1);
4970                expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
4971                    "expected single entity uid or template slot, found set of entity uids",
4972                ).exactly_one_underline(r#"[[User::"alice"]]"#).build());
4973            }
4974        );
4975
4976        let policy = r#"
4977            permit (
4978                principal,
4979                action,
4980                resource == [[?resource]]
4981            );
4982        "#;
4983        assert_matches!(
4984            parse_policy(None, policy),
4985            Err(e) => {
4986                expect_n_errors(policy, &e, 1);
4987                expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
4988                    "expected single entity uid or template slot, found set of entity uids",
4989                ).exactly_one_underline("[[?resource]]").build());
4990            }
4991        );
4992
4993        let policy = r#"
4994            permit (
4995                principal,
4996                action in [[[Action::"act"]]],
4997                resource
4998            );
4999        "#;
5000        assert_matches!(
5001            parse_policy(None, policy),
5002            Err(e) => {
5003                expect_n_errors(policy, &e, 1);
5004                expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5005                    "expected single entity uid, found set of entity uids",
5006                ).exactly_one_underline(r#"[[Action::"act"]]"#).build());
5007            }
5008        );
5009    }
5010
5011    #[test]
5012    fn unsupported_ops() {
5013        let src = "1/2";
5014        assert_matches!(parse_expr(src), Err(e) => {
5015            expect_err(src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("division is not supported").exactly_one_underline("1/2").build());
5016        });
5017        let src = "7 % 3";
5018        assert_matches!(parse_expr(src), Err(e) => {
5019            expect_err(src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("remainder/modulo is not supported").exactly_one_underline("7 % 3").build());
5020        });
5021        let src = "7 = 3";
5022        assert_matches!(parse_expr(src), Err(e) => {
5023            expect_err(src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("'=' is not a valid operator in Cedar").exactly_one_underline("7 = 3").help("try using '==' instead").build());
5024        });
5025    }
5026
5027    #[test]
5028    fn over_unary() {
5029        let src = "!!!!!!false";
5030        assert_matches!(parse_expr(src), Err(e) => {
5031            expect_err(src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
5032                "too many occurrences of `!`",
5033                ).help(
5034                "cannot chain more the 4 applications of a unary operator"
5035            ).exactly_one_underline("!!!!!!false").build());
5036        });
5037        let src = "-------0";
5038        assert_matches!(parse_expr(src), Err(e) => {
5039            expect_err(src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
5040                "too many occurrences of `-`",
5041                ).help(
5042                "cannot chain more the 4 applications of a unary operator"
5043            ).exactly_one_underline("-------0").build());
5044        });
5045    }
5046
5047    #[test]
5048    fn arbitrary_variables() {
5049        #[track_caller]
5050        fn expect_arbitrary_var(name: &str) {
5051            assert_matches!(parse_expr(name), Err(e) => {
5052                expect_err(name, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
5053                    &format!("invalid variable: {name}"),
5054                ).help(
5055                    &format!("the valid Cedar variables are `principal`, `action`, `resource`, and `context`; did you mean to enclose `{name}` in quotes to make a string?"),
5056                ).exactly_one_underline(name).build());
5057            })
5058        }
5059        expect_arbitrary_var("foo::principal");
5060        expect_arbitrary_var("bar::action");
5061        expect_arbitrary_var("baz::resource");
5062        expect_arbitrary_var("buz::context");
5063        expect_arbitrary_var("foo::principal");
5064        expect_arbitrary_var("foo::bar::principal");
5065        expect_arbitrary_var("principal::foo");
5066        expect_arbitrary_var("principal::foo::bar");
5067        expect_arbitrary_var("foo::principal::bar");
5068        expect_arbitrary_var("foo");
5069        expect_arbitrary_var("foo::bar");
5070        expect_arbitrary_var("foo::bar::baz");
5071    }
5072
5073    #[test]
5074    fn empty_clause() {
5075        #[track_caller]
5076        fn expect_empty_clause(policy: &str, clause: &str) {
5077            assert_matches!(parse_policy_or_template(None, policy), Err(e) => {
5078                expect_err(policy, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
5079                    &format!("`{clause}` condition clause cannot be empty")
5080                ).exactly_one_underline(&format!("{clause} {{}}")).build());
5081            })
5082        }
5083
5084        expect_empty_clause("permit(principal, action, resource) when {};", "when");
5085        expect_empty_clause("permit(principal, action, resource) unless {};", "unless");
5086        expect_empty_clause(
5087            "permit(principal, action, resource) when { principal has foo } when {};",
5088            "when",
5089        );
5090        expect_empty_clause(
5091            "permit(principal, action, resource) when { principal has foo } unless {};",
5092            "unless",
5093        );
5094        expect_empty_clause(
5095            "permit(principal, action, resource) when {} unless { resource.bar };",
5096            "when",
5097        );
5098        expect_empty_clause(
5099            "permit(principal, action, resource) unless {} unless { resource.bar };",
5100            "unless",
5101        );
5102    }
5103
5104    #[test]
5105    fn namespaced_attr() {
5106        #[track_caller]
5107        fn expect_namespaced_attr(expr: &str, name: &str) {
5108            assert_matches!(parse_expr(expr), Err(e) => {
5109                expect_err(expr, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
5110                    &format!("`{name}` cannot be used as an attribute as it contains a namespace")
5111                ).exactly_one_underline(name).build());
5112            })
5113        }
5114
5115        expect_namespaced_attr("principal has foo::bar", "foo::bar");
5116        expect_namespaced_attr("principal has foo::bar::baz", "foo::bar::baz");
5117        expect_namespaced_attr("principal has foo::principal", "foo::principal");
5118        expect_namespaced_attr("{foo::bar: 1}", "foo::bar");
5119
5120        let expr = "principal has if::foo";
5121        assert_matches!(parse_expr(expr), Err(e) => {
5122            expect_err(expr, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
5123                "this identifier is reserved and cannot be used: if"
5124            ).exactly_one_underline("if").build());
5125        })
5126    }
5127
5128    #[test]
5129    fn reserved_ident_var() {
5130        #[track_caller]
5131        fn expect_reserved_ident(name: &str, reserved: &str) {
5132            assert_matches!(parse_expr(name), Err(e) => {
5133                expect_err(name, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
5134                    &format!("this identifier is reserved and cannot be used: {reserved}"),
5135                ).exactly_one_underline(reserved).build());
5136            })
5137        }
5138        expect_reserved_ident("if::principal", "if");
5139        expect_reserved_ident("then::action", "then");
5140        expect_reserved_ident("else::resource", "else");
5141        expect_reserved_ident("true::context", "true");
5142        expect_reserved_ident("false::bar::principal", "false");
5143        expect_reserved_ident("foo::in::principal", "in");
5144        expect_reserved_ident("foo::is::bar::principal", "is");
5145    }
5146
5147    #[test]
5148    fn reserved_namespace() {
5149        assert_matches!(parse_expr(r#"__cedar::"""#),
5150            Err(errs) if matches!(errs.as_ref().first(),
5151                ParseError::ToAST(to_ast_err) if matches!(to_ast_err.kind(),
5152                    ToASTErrorKind::ReservedNamespace(ReservedNameError(n)) if *n == "__cedar".parse::<InternalName>().unwrap())));
5153        assert_matches!(parse_expr(r#"__cedar::A::"""#),
5154            Err(errs) if matches!(errs.as_ref().first(),
5155                ParseError::ToAST(to_ast_err) if matches!(to_ast_err.kind(),
5156                    ToASTErrorKind::ReservedNamespace(ReservedNameError(n)) if *n == "__cedar::A".parse::<InternalName>().unwrap())));
5157        assert_matches!(parse_expr(r#"A::__cedar::B::"""#),
5158            Err(errs) if matches!(errs.as_ref().first(),
5159                ParseError::ToAST(to_ast_err) if matches!(to_ast_err.kind(),
5160                    ToASTErrorKind::ReservedNamespace(ReservedNameError(n)) if *n == "A::__cedar::B".parse::<InternalName>().unwrap())));
5161        assert_matches!(parse_expr(r#"[A::"", __cedar::Action::"action"]"#),
5162            Err(errs) if matches!(errs.as_ref().first(),
5163                ParseError::ToAST(to_ast_err) if matches!(to_ast_err.kind(),
5164                    ToASTErrorKind::ReservedNamespace(ReservedNameError(n)) if *n == "__cedar::Action".parse::<InternalName>().unwrap())));
5165        assert_matches!(parse_expr(r#"principal is __cedar::A"#),
5166            Err(errs) if matches!(errs.as_ref().first(),
5167                ParseError::ToAST(to_ast_err) if matches!(to_ast_err.kind(),
5168                    ToASTErrorKind::ReservedNamespace(ReservedNameError(n)) if *n == "__cedar::A".parse::<InternalName>().unwrap())));
5169        assert_matches!(parse_expr(r#"__cedar::decimal("0.0")"#),
5170            Err(errs) if matches!(errs.as_ref().first(),
5171                ParseError::ToAST(to_ast_err) if matches!(to_ast_err.kind(),
5172                    ToASTErrorKind::ReservedNamespace(ReservedNameError(n)) if *n == "__cedar::decimal".parse::<InternalName>().unwrap())));
5173        assert_matches!(parse_expr(r#"ip("").__cedar()"#),
5174            Err(errs) if matches!(errs.as_ref().first(),
5175                ParseError::ToAST(to_ast_err) if matches!(to_ast_err.kind(),
5176                    ToASTErrorKind::ReservedNamespace(ReservedNameError(n)) if *n == "__cedar".parse::<InternalName>().unwrap())));
5177        assert_matches!(parse_expr(r#"{__cedar: 0}"#),
5178            Err(errs) if matches!(errs.as_ref().first(),
5179                ParseError::ToAST(to_ast_err) if matches!(to_ast_err.kind(),
5180                    ToASTErrorKind::ReservedNamespace(ReservedNameError(n)) if *n == "__cedar".parse::<InternalName>().unwrap())));
5181        assert_matches!(parse_expr(r#"{a: 0}.__cedar"#),
5182            Err(errs) if matches!(errs.as_ref().first(),
5183                ParseError::ToAST(to_ast_err) if matches!(to_ast_err.kind(),
5184                    ToASTErrorKind::ReservedNamespace(ReservedNameError(n)) if *n == "__cedar".parse::<InternalName>().unwrap())));
5185        // We allow `__cedar` as an annotation identifier
5186        assert_matches!(
5187            parse_policy(
5188                None,
5189                r#"@__cedar("foo") permit(principal, action, resource);"#
5190            ),
5191            Ok(_)
5192        );
5193    }
5194
5195    #[test]
5196    fn arbitrary_name_attr_access() {
5197        let src = "foo.attr";
5198        assert_matches!(parse_expr(src), Err(e) => {
5199            expect_err(src, &miette::Report::new(e),
5200                &ExpectedErrorMessageBuilder::error("invalid member access `foo.attr`, `foo` has no fields or methods")
5201                    .exactly_one_underline("foo.attr")
5202                    .build()
5203            );
5204        });
5205
5206        let src = r#"foo["attr"]"#;
5207        assert_matches!(parse_expr(src), Err(e) => {
5208            expect_err(src, &miette::Report::new(e),
5209                &ExpectedErrorMessageBuilder::error(r#"invalid indexing expression `foo["attr"]`, `foo` has no fields"#)
5210                    .exactly_one_underline(r#"foo["attr"]"#)
5211                    .build()
5212            );
5213        });
5214
5215        let src = r#"foo["\n"]"#;
5216        assert_matches!(parse_expr(src), Err(e) => {
5217            expect_err(src, &miette::Report::new(e),
5218                &ExpectedErrorMessageBuilder::error(r#"invalid indexing expression `foo["\n"]`, `foo` has no fields"#)
5219                    .exactly_one_underline(r#"foo["\n"]"#)
5220                    .build()
5221            );
5222        });
5223    }
5224
5225    #[test]
5226    fn extended_has() {
5227        assert_matches!(
5228            parse_policy(
5229                None,
5230                r#"
5231        permit(
5232  principal is User,
5233  action == Action::"preview",
5234  resource == Movie::"Blockbuster"
5235) when {
5236  principal has contactInfo.address.zip &&
5237  principal.contactInfo.address.zip == "90210"
5238};
5239        "#
5240            ),
5241            Ok(_)
5242        );
5243
5244        assert_matches!(parse_expr(r#"context has a.b"#), Ok(e) => {
5245            assert!(e.eq_shape(&parse_expr(r#"(context has a) && (context.a has b)"#).unwrap()));
5246        });
5247
5248        assert_matches!(parse_expr(r#"context has a.b.c"#), Ok(e) => {
5249            assert!(e.eq_shape(&parse_expr(r#"((context has a) && (context.a has b)) && (context.a.b has c)"#).unwrap()));
5250        });
5251
5252        let policy = r#"permit(principal, action, resource) when {
5253            principal has a.if
5254          };"#;
5255        assert_matches!(
5256            parse_policy(None, policy),
5257            Err(e) => {
5258                expect_n_errors(policy, &e, 1);
5259                expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5260                    "this identifier is reserved and cannot be used: if",
5261                ).exactly_one_underline(r#"if"#).build());
5262            }
5263        );
5264        let policy = r#"permit(principal, action, resource) when {
5265            principal has if.a
5266          };"#;
5267        assert_matches!(
5268            parse_policy(None, policy),
5269            Err(e) => {
5270                expect_n_errors(policy, &e, 1);
5271                expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5272                    "this identifier is reserved and cannot be used: if",
5273                ).exactly_one_underline(r#"if"#).build());
5274            }
5275        );
5276        let policy = r#"permit(principal, action, resource) when {
5277            principal has true.if
5278          };"#;
5279        assert_matches!(
5280            parse_policy(None, policy),
5281            Err(e) => {
5282                expect_n_errors(policy, &e, 1);
5283                expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5284                    "this identifier is reserved and cannot be used: true",
5285                ).exactly_one_underline(r#"true"#).build());
5286            }
5287        );
5288        let policy = r#"permit(principal, action, resource) when {
5289            principal has a.__cedar
5290          };"#;
5291        assert_matches!(
5292            parse_policy(None, policy),
5293            Err(e) => {
5294                expect_n_errors(policy, &e, 1);
5295                expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5296                    "The name `__cedar` contains `__cedar`, which is reserved",
5297                ).exactly_one_underline(r#"__cedar"#).build());
5298            }
5299        );
5300
5301        let help_msg = "valid RHS of a `has` operation is either a sequence of identifiers separated by `.` or a string literal";
5302
5303        let policy = r#"permit(principal, action, resource) when {
5304            principal has 1 + 1
5305          };"#;
5306        assert_matches!(
5307            parse_policy(None, policy),
5308            Err(e) => {
5309                expect_n_errors(policy, &e, 1);
5310                expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5311                    "invalid RHS of a `has` operation: 1 + 1",
5312                ).help(help_msg).
5313                exactly_one_underline(r#"1 + 1"#).build());
5314            }
5315        );
5316        let policy = r#"permit(principal, action, resource) when {
5317            principal has a - 1
5318          };"#;
5319        assert_matches!(
5320            parse_policy(None, policy),
5321            Err(e) => {
5322                expect_n_errors(policy, &e, 1);
5323                expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5324                    "invalid RHS of a `has` operation: a - 1",
5325                ).help(help_msg).exactly_one_underline(r#"a - 1"#).build());
5326            }
5327        );
5328        let policy = r#"permit(principal, action, resource) when {
5329            principal has a*3 + 1
5330          };"#;
5331        assert_matches!(
5332            parse_policy(None, policy),
5333            Err(e) => {
5334                expect_n_errors(policy, &e, 1);
5335                expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5336                    "invalid RHS of a `has` operation: a * 3 + 1",
5337                ).help(help_msg).exactly_one_underline(r#"a*3 + 1"#).build());
5338            }
5339        );
5340        let policy = r#"permit(principal, action, resource) when {
5341            principal has 3*a
5342          };"#;
5343        assert_matches!(
5344            parse_policy(None, policy),
5345            Err(e) => {
5346                expect_n_errors(policy, &e, 1);
5347                expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5348                    "invalid RHS of a `has` operation: 3 * a",
5349                ).help(help_msg).exactly_one_underline(r#"3*a"#).build());
5350            }
5351        );
5352        let policy = r#"permit(principal, action, resource) when {
5353            principal has -a.b
5354          };"#;
5355        assert_matches!(
5356            parse_policy(None, policy),
5357            Err(e) => {
5358                expect_n_errors(policy, &e, 1);
5359                expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5360                    "invalid RHS of a `has` operation: -a.b",
5361                ).help(help_msg).exactly_one_underline(r#"-a.b"#).build());
5362            }
5363        );
5364        let policy = r#"permit(principal, action, resource) when {
5365            principal has !a.b
5366          };"#;
5367        assert_matches!(
5368            parse_policy(None, policy),
5369            Err(e) => {
5370                expect_n_errors(policy, &e, 1);
5371                expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5372                    "invalid RHS of a `has` operation: !a.b",
5373                ).help(help_msg).exactly_one_underline(r#"!a.b"#).build());
5374            }
5375        );
5376        let policy = r#"permit(principal, action, resource) when {
5377            principal has a::b.c
5378          };"#;
5379        assert_matches!(
5380            parse_policy(None, policy),
5381            Err(e) => {
5382                expect_n_errors(policy, &e, 1);
5383                expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5384                    "`a::b.c` cannot be used as an attribute as it contains a namespace",
5385                ).exactly_one_underline(r#"a::b"#).build());
5386            }
5387        );
5388        let policy = r#"permit(principal, action, resource) when {
5389            principal has A::""
5390          };"#;
5391        assert_matches!(
5392            parse_policy(None, policy),
5393            Err(e) => {
5394                expect_n_errors(policy, &e, 1);
5395                expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5396                    "invalid RHS of a `has` operation: A::\"\"",
5397                ).help(help_msg).exactly_one_underline(r#"A::"""#).build());
5398            }
5399        );
5400        let policy = r#"permit(principal, action, resource) when {
5401            principal has A::"".a
5402          };"#;
5403        assert_matches!(
5404            parse_policy(None, policy),
5405            Err(e) => {
5406                expect_n_errors(policy, &e, 1);
5407                expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5408                    "invalid RHS of a `has` operation: A::\"\".a",
5409                ).help(help_msg).exactly_one_underline(r#"A::"""#).build());
5410            }
5411        );
5412        let policy = r#"permit(principal, action, resource) when {
5413            principal has ?principal
5414          };"#;
5415        assert_matches!(
5416            parse_policy(None, policy),
5417            Err(e) => {
5418                expect_n_errors(policy, &e, 1);
5419                expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5420                    "invalid RHS of a `has` operation: ?principal",
5421                ).help(help_msg).exactly_one_underline(r#"?principal"#).build());
5422            }
5423        );
5424        let policy = r#"permit(principal, action, resource) when {
5425            principal has ?principal.a
5426          };"#;
5427        assert_matches!(
5428            parse_policy(None, policy),
5429            Err(e) => {
5430                expect_n_errors(policy, &e, 1);
5431                expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5432                    "invalid RHS of a `has` operation: ?principal.a",
5433                ).help(help_msg).exactly_one_underline(r#"?principal"#).build());
5434            }
5435        );
5436        let policy = r#"permit(principal, action, resource) when {
5437            principal has (b).a
5438          };"#;
5439        assert_matches!(
5440            parse_policy(None, policy),
5441            Err(e) => {
5442                expect_n_errors(policy, &e, 1);
5443                expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5444                    "invalid RHS of a `has` operation: (b).a",
5445                ).help(help_msg).exactly_one_underline(r#"(b)"#).build());
5446            }
5447        );
5448        let policy = r#"permit(principal, action, resource) when {
5449            principal has [b].a
5450          };"#;
5451        assert_matches!(
5452            parse_policy(None, policy),
5453            Err(e) => {
5454                expect_n_errors(policy, &e, 1);
5455                expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5456                    "invalid RHS of a `has` operation: [b].a",
5457                ).help(help_msg).exactly_one_underline(r#"[b]"#).build());
5458            }
5459        );
5460        let policy = r#"permit(principal, action, resource) when {
5461            principal has {b:1}.a
5462          };"#;
5463        assert_matches!(
5464            parse_policy(None, policy),
5465            Err(e) => {
5466                expect_n_errors(policy, &e, 1);
5467                expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5468                    "invalid RHS of a `has` operation: {b: 1}.a",
5469                ).help(help_msg).exactly_one_underline(r#"{b:1}"#).build());
5470            }
5471        );
5472    }
5473
5474    #[cfg(feature = "tolerant-ast")]
5475    #[track_caller]
5476    fn assert_parse_policy_allows_errors(text: &str) -> ast::StaticPolicy {
5477        text_to_cst::parse_policy_tolerant(text)
5478            .expect("failed parser")
5479            .to_policy_tolerant(ast::PolicyID::from_string("id"))
5480            .unwrap_or_else(|errs| {
5481                panic!("failed conversion to AST:\n{:?}", miette::Report::new(errs))
5482            })
5483    }
5484
5485    #[cfg(feature = "tolerant-ast")]
5486    #[track_caller]
5487    fn assert_parse_policy_allows_errors_fails(text: &str) -> ParseErrors {
5488        let result = text_to_cst::parse_policy_tolerant(text)
5489            .expect("failed parser")
5490            .to_policy_tolerant(ast::PolicyID::from_string("id"));
5491        match result {
5492            Ok(policy) => {
5493                panic!("conversion to AST should have failed, but succeeded with:\n{policy}")
5494            }
5495            Err(errs) => errs,
5496        }
5497    }
5498
5499    // Test parsing AST that allows Error nodes
5500    #[cfg(feature = "tolerant-ast")]
5501    #[test]
5502    fn parsing_with_errors_succeeds_with_empty_when() {
5503        let src = r#"
5504            permit(principal, action, resource) when {};
5505        "#;
5506        assert_parse_policy_allows_errors(src);
5507    }
5508
5509    // Test parsing AST that allows Error nodes
5510    #[cfg(feature = "tolerant-ast")]
5511    #[test]
5512    fn parsing_with_errors_succeeds_with_invalid_variable_in_when() {
5513        let src = r#"
5514            permit(principal, action, resource) when { pri };
5515        "#;
5516        assert_parse_policy_allows_errors(src);
5517    }
5518
5519    #[cfg(feature = "tolerant-ast")]
5520    #[test]
5521    fn parsing_with_errors_succeeds_with_invalid_method() {
5522        let src = r#"
5523            permit(principal, action, resource) when { ip(principal.ip).i() };
5524        "#;
5525        assert_parse_policy_allows_errors(src);
5526    }
5527
5528    #[cfg(feature = "tolerant-ast")]
5529    #[test]
5530    fn parsing_with_errors_succeeds_with_invalid_uid_resource_constraint() {
5531        let src = r#"
5532            permit (
5533                principal,
5534                action,
5535                resource in H
5536            )
5537            when { true };
5538        "#;
5539        assert_parse_policy_allows_errors(src);
5540    }
5541
5542    #[cfg(feature = "tolerant-ast")]
5543    #[test]
5544    fn parsing_with_errors_succeeds_with_invalid_uid_principal_constraint() {
5545        let src = r#"
5546            permit (
5547                principal in J,
5548                action,
5549                resource
5550            )
5551            when { true };
5552        "#;
5553        assert_parse_policy_allows_errors(src);
5554    }
5555
5556    #[cfg(feature = "tolerant-ast")]
5557    #[test]
5558    fn invalid_action_constraint_in_a_list() {
5559        let src = r#"
5560            permit (
5561                principal,
5562                action in [A],
5563                resource
5564            )
5565            when { true };
5566        "#;
5567        assert_parse_policy_allows_errors(src);
5568    }
5569
5570    #[cfg(feature = "tolerant-ast")]
5571    #[test]
5572    fn parsing_with_errors_succeeds_with_invalid_bracket_for_in() {
5573        let src = r#"
5574            permit (
5575                principal,
5576                action,
5577                resource in [
5578            )
5579            when { true };
5580        "#;
5581        assert_parse_policy_allows_errors(src);
5582    }
5583
5584    #[cfg(feature = "tolerant-ast")]
5585    #[test]
5586    fn parsing_with_errors_succeeds_with_missing_second_operand_eq_and_in() {
5587        // Test for == operator
5588        let src_eq_cases = [
5589            r#"permit(principal ==, action, resource);"#,
5590            r#"permit(principal, action ==, resource);"#,
5591            r#"permit(principal, action, resource ==);"#,
5592            r#"permit(principal ==, action ==, resource);"#,
5593            r#"permit(principal, action ==, resource ==);"#,
5594            r#"permit(principal ==, action, resource ==);"#,
5595            r#"permit(principal ==, action ==, resource ==);"#,
5596        ];
5597
5598        for src in src_eq_cases.iter() {
5599            assert_parse_policy_allows_errors(src);
5600        }
5601
5602        // Test for in operator
5603        let src_in_cases = [
5604            r#"permit(principal in, action, resource);"#,
5605            r#"permit(principal, action in, resource);"#,
5606            r#"permit(principal, action, resource in);"#,
5607            r#"permit(principal in, action in, resource);"#,
5608            r#"permit(principal, action in, resource in);"#,
5609            r#"permit(principal in, action, resource in);"#,
5610            r#"permit(principal in, action in, resource in);"#,
5611        ];
5612
5613        for src in src_in_cases.iter() {
5614            assert_parse_policy_allows_errors(src);
5615        }
5616
5617        // Cases with "is" and missing operands
5618        let src_in_cases = [
5619            r#"permit(principal is something in, action, resource);"#,
5620            r#"permit(principal, action, resource is something in);"#,
5621        ];
5622        for src in src_in_cases.iter() {
5623            assert_parse_policy_allows_errors(src);
5624        }
5625    }
5626
5627    #[cfg(feature = "tolerant-ast")]
5628    #[test]
5629    fn parsing_with_errors_succeeds_with_invalid_variable_in_when_missing_operand() {
5630        let src = r#"
5631            permit(principal, action, resource) when { principal == };
5632        "#;
5633        assert_parse_policy_allows_errors(src);
5634
5635        let src = r#"
5636        permit(principal, action, resource) when { resource == };
5637        "#;
5638        assert_parse_policy_allows_errors(src);
5639
5640        let src = r#"
5641        permit(principal, action, resource) when { action == };
5642        "#;
5643        assert_parse_policy_allows_errors(src);
5644
5645        let src = r#"
5646        permit(principal, action, resource) when { principal == User::test && action == };
5647        "#;
5648        assert_parse_policy_allows_errors(src);
5649
5650        let src = r#"
5651        permit(principal, action, resource) when { action == &&  principal == User::test};
5652        "#;
5653        assert_parse_policy_allows_errors(src);
5654    }
5655
5656    #[cfg(feature = "tolerant-ast")]
5657    #[test]
5658    fn parsing_with_errors_succeeds_with_missing_second_operand_is() {
5659        let src = r#"
5660            permit(principal is something in, action, resource);
5661        "#;
5662        assert_parse_policy_allows_errors(src);
5663    }
5664
5665    #[cfg(feature = "tolerant-ast")]
5666    #[test]
5667    fn show_policy1_errors_enabled() {
5668        let src = r#"
5669            permit(principal:p,action:a,resource:r)when{w}unless{u}advice{"doit"};
5670        "#;
5671        let errs = assert_parse_policy_allows_errors_fails(src);
5672        expect_n_errors(src, &errs, 4);
5673        expect_some_error_matches(
5674            src,
5675            &errs,
5676            &ExpectedErrorMessageBuilder::error("type constraints using `:` are not supported")
5677                .help("try using `is` instead")
5678                .exactly_one_underline("p")
5679                .build(),
5680        );
5681        expect_some_error_matches(
5682            src,
5683            &errs,
5684            &ExpectedErrorMessageBuilder::error("type constraints using `:` are not supported")
5685                .help("try using `is` instead")
5686                .exactly_one_underline("a")
5687                .build(),
5688        );
5689        expect_some_error_matches(
5690            src,
5691            &errs,
5692            &ExpectedErrorMessageBuilder::error("type constraints using `:` are not supported")
5693                .help("try using `is` instead")
5694                .exactly_one_underline("r")
5695                .build(),
5696        );
5697        expect_some_error_matches(
5698            src,
5699            &errs,
5700            &ExpectedErrorMessageBuilder::error("invalid policy condition: advice")
5701                .help("condition must be either `when` or `unless`")
5702                .exactly_one_underline("advice")
5703                .build(),
5704        );
5705    }
5706
5707    #[cfg(feature = "tolerant-ast")]
5708    #[test]
5709    fn show_policy2_errors_enabled() {
5710        let src = r#"
5711            permit(principal,action,resource)when{true};
5712        "#;
5713        assert_parse_policy_allows_errors(src);
5714    }
5715
5716    #[cfg(feature = "tolerant-ast")]
5717    #[test]
5718    fn show_policy3_errors_enabled() {
5719        let src = r#"
5720            permit(principal in User::"jane",action,resource);
5721        "#;
5722        assert_parse_policy_allows_errors(src);
5723    }
5724
5725    #[cfg(feature = "tolerant-ast")]
5726    #[test]
5727    fn show_policy4_errors_enabled() {
5728        let src = r#"
5729            forbid(principal in User::"jane",action,resource)unless{
5730                context.group != "friends"
5731            };
5732        "#;
5733        assert_parse_policy_allows_errors(src);
5734    }
5735
5736    #[cfg(feature = "tolerant-ast")]
5737    #[test]
5738    fn invalid_policy_errors_enabled() {
5739        let src = r#"
5740            permit(principal,;
5741        "#;
5742        assert_parse_policy_allows_errors(src);
5743    }
5744
5745    #[cfg(feature = "tolerant-ast")]
5746    #[test]
5747    fn invalid_policy_with_trailing_dot_errors_enabled() {
5748        let src = r#"
5749            permit(principal, action, resource) { principal. };
5750        "#;
5751        assert_parse_policy_allows_errors(src);
5752    }
5753
5754    #[cfg(feature = "tolerant-ast")]
5755    #[test]
5756    fn missing_entity_identifier_errors_enabled() {
5757        let src = r#"
5758            permit(principal, action == Action::, resource);
5759        "#;
5760        assert_parse_policy_allows_errors(src);
5761    }
5762
5763    #[cfg(feature = "tolerant-ast")]
5764    #[test]
5765    fn single_annotation_errors_enabled() {
5766        // common use-case
5767        let policy = assert_parse_policy_allows_errors(
5768            r#"
5769            @anno("good annotation")permit(principal,action,resource);
5770        "#,
5771        );
5772        assert_matches!(
5773            policy.annotation(&ast::AnyId::new_unchecked("anno")),
5774            Some(annotation) => assert_eq!(annotation.as_ref(), "good annotation")
5775        );
5776    }
5777
5778    #[cfg(feature = "tolerant-ast")]
5779    #[test]
5780    fn duplicate_annotations_error_errors_enabled() {
5781        // duplication is error
5782        let src = r#"
5783            @anno("good annotation")
5784            @anno2("good annotation")
5785            @anno("oops, duplicate")
5786            permit(principal,action,resource);
5787        "#;
5788        let errs = assert_parse_policy_allows_errors_fails(src);
5789        // annotation duplication (anno)
5790        expect_n_errors(src, &errs, 1);
5791        expect_some_error_matches(
5792            src,
5793            &errs,
5794            &ExpectedErrorMessageBuilder::error("duplicate annotation: @anno")
5795                .exactly_one_underline("@anno(\"oops, duplicate\")")
5796                .build(),
5797        );
5798    }
5799
5800    #[cfg(feature = "tolerant-ast")]
5801    #[test]
5802    fn multiple_policys_with_unparsable_policy_ok() {
5803        // When we have a malformed policy, it should become an error node but the rest of the policies should parse
5804        let policyset = text_to_cst::parse_policies_tolerant(
5805            r#"
5806            // POLICY 1
5807            @id("Photo.owner")
5808            permit (
5809            principal,
5810            action in
5811                [PhotoApp::Action::"viewPhoto",
5812                PhotoApp::Action::"editPhoto",
5813                PhotoApp::Action::"deletePhoto"],
5814            resource in PhotoApp::Application::"PhotoApp"
5815            )
5816            when { resource.owner == principal };
5817
5818            // POLICY2 - unparsable
5819            @id("label_private")
5820            forbid (
5821            principal,
5822            acti
5823
5824            // POLICY3 - unparsable because previous policy is missing a ";"
5825            @id("Photo.subjects")
5826            permit (
5827            principal,
5828            action == PhotoApp::Action::"viewPhoto",
5829            resource in PhotoApp::Application::"PhotoApp"
5830            )
5831            when { resource has subjects && resource.subjects.contains(principal) };
5832
5833            // POLICY 4
5834            @id("PhotoJudge")
5835            permit (
5836            principal in PhotoApp::Role::"PhotoJudge",
5837            action == PhotoApp::Action::"viewPhoto",
5838            resource in PhotoApp::Application::"PhotoApp"
5839            )
5840            when { resource.labels.contains("contest") }
5841            when { context has judgingSession && context.judgingSession == true };
5842        "#,
5843        )
5844        .expect("should parse")
5845        .to_policyset_tolerant()
5846        .unwrap_or_else(|errs| panic!("failed convert to AST:\n{:?}", miette::Report::new(errs)));
5847        policyset
5848            .get(&ast::PolicyID::from_string("policy0"))
5849            .expect("should be a policy");
5850        policyset
5851            .get(&ast::PolicyID::from_string("policy1"))
5852            .expect("should be a policy");
5853        policyset
5854            .get(&ast::PolicyID::from_string("policy2"))
5855            .expect("should be a policy");
5856        assert!(policyset
5857            .get(&ast::PolicyID::from_string("policy3"))
5858            .is_none());
5859    }
5860
5861    #[cfg(feature = "tolerant-ast")]
5862    #[test]
5863    fn fail_scope1_tolerant_ast() {
5864        let src = r#"
5865            permit(
5866                principal in [User::"jane",Group::"friends"],
5867                action,
5868                resource
5869            );
5870        "#;
5871        let errs = assert_parse_policy_allows_errors_fails(src);
5872        expect_n_errors(src, &errs, 1);
5873        expect_some_error_matches(
5874            src,
5875            &errs,
5876            &ExpectedErrorMessageBuilder::error(
5877                "expected single entity uid or template slot, found set of entity uids",
5878            )
5879            .exactly_one_underline(r#"[User::"jane",Group::"friends"]"#)
5880            .build(),
5881        );
5882    }
5883
5884    #[cfg(feature = "tolerant-ast")]
5885    #[test]
5886    fn fail_scope2_tolerant_ast() {
5887        let src = r#"
5888            permit(
5889                principal in User::"jane",
5890                action == if true then Photo::"view" else Photo::"edit",
5891                resource
5892            );
5893        "#;
5894        let errs = assert_parse_policy_allows_errors_fails(src);
5895        expect_n_errors(src, &errs, 1);
5896        expect_some_error_matches(
5897            src,
5898            &errs,
5899            &ExpectedErrorMessageBuilder::error("expected an entity uid, found an `if` expression")
5900                .exactly_one_underline(r#"if true then Photo::"view" else Photo::"edit""#)
5901                .build(),
5902        );
5903    }
5904
5905    #[cfg(feature = "tolerant-ast")]
5906    #[test]
5907    fn invalid_slot_tolerant_ast() {
5908        let invalid_policies = [
5909            (
5910                r#"permit(principal == ?resource, action, resource);"#,
5911                ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?resource instead of ?principal").exactly_one_underline("?resource").build(),
5912            ),
5913            (
5914                r#"permit(principal in ?resource, action, resource);"#,
5915                ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?resource instead of ?principal").exactly_one_underline("?resource").build(),
5916            ),
5917            (
5918                r#"permit(principal == ?foo, action, resource);"#,
5919                ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?foo instead of ?principal").exactly_one_underline("?foo").build(),
5920            ),
5921            (
5922                r#"permit(principal in ?foo, action, resource);"#,
5923                ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?foo instead of ?principal").exactly_one_underline("?foo").build(),
5924            ),
5925
5926            (
5927                r#"permit(principal, action, resource == ?principal);"#,
5928                ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?principal instead of ?resource").exactly_one_underline("?principal").build(),
5929            ),
5930            (
5931                r#"permit(principal, action, resource in ?principal);"#,
5932                ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?principal instead of ?resource").exactly_one_underline("?principal").build(),
5933            ),
5934            (
5935                r#"permit(principal, action, resource == ?baz);"#,
5936                ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?baz instead of ?resource").exactly_one_underline("?baz").build(),
5937            ),
5938            (
5939                r#"permit(principal, action, resource in ?baz);"#,
5940                ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?baz instead of ?resource").exactly_one_underline("?baz").build(),
5941            ),
5942            (
5943                r#"permit(principal, action, resource) when { principal == ?foo};"#,
5944                ExpectedErrorMessageBuilder::error(
5945                    "`?foo` is not a valid template slot",
5946                ).help(
5947                    "a template slot may only be `?principal` or `?resource`",
5948                ).exactly_one_underline("?foo").build(),
5949            ),
5950
5951            (
5952                r#"permit(principal, action == ?action, resource);"#,
5953                ExpectedErrorMessageBuilder::error("expected single entity uid, found template slot").exactly_one_underline("?action").build(),
5954            ),
5955            (
5956                r#"permit(principal, action in ?action, resource);"#,
5957                ExpectedErrorMessageBuilder::error("expected single entity uid or set of entity uids, found template slot").exactly_one_underline("?action").build(),
5958            ),
5959            (
5960                r#"permit(principal, action == ?principal, resource);"#,
5961                ExpectedErrorMessageBuilder::error("expected single entity uid, found template slot").exactly_one_underline("?principal").build(),
5962            ),
5963            (
5964                r#"permit(principal, action in ?principal, resource);"#,
5965                ExpectedErrorMessageBuilder::error("expected single entity uid or set of entity uids, found template slot").exactly_one_underline("?principal").build(),
5966            ),
5967            (
5968                r#"permit(principal, action == ?resource, resource);"#,
5969                ExpectedErrorMessageBuilder::error("expected single entity uid, found template slot").exactly_one_underline("?resource").build(),
5970            ),
5971            (
5972                r#"permit(principal, action in ?resource, resource);"#,
5973                ExpectedErrorMessageBuilder::error("expected single entity uid or set of entity uids, found template slot").exactly_one_underline("?resource").build(),
5974            ),
5975            (
5976                r#"permit(principal, action in [?bar], resource);"#,
5977                ExpectedErrorMessageBuilder::error("expected single entity uid, found template slot").exactly_one_underline("?bar").build(),
5978            ),
5979        ];
5980
5981        for (p_src, expected) in invalid_policies {
5982            assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
5983                expect_err(p_src, &miette::Report::new(e), &expected);
5984            });
5985            let forbid_src = format!("forbid{}", &p_src[6..]);
5986            assert_matches!(parse_policy_or_template_tolerant(None, &forbid_src), Err(e) => {
5987                expect_err(forbid_src.as_str(), &miette::Report::new(e), &expected);
5988            });
5989        }
5990    }
5991
5992    #[cfg(feature = "tolerant-ast")]
5993    #[test]
5994    fn missing_scope_constraint_tolerant_ast() {
5995        let p_src = "permit();";
5996        assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
5997            expect_err(
5998                p_src,
5999                &miette::Report::new(e),
6000                &ExpectedErrorMessageBuilder::error("this policy is missing the `principal` variable in the scope")
6001                    .exactly_one_underline("")
6002                    .help("policy scopes must contain a `principal`, `action`, and `resource` element in that order")
6003                    .build()
6004            );
6005        });
6006        let p_src = "permit(principal);";
6007        assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6008            expect_err(
6009                p_src,
6010                &miette::Report::new(e),
6011                &ExpectedErrorMessageBuilder::error("this policy is missing the `action` variable in the scope")
6012                    .exactly_one_underline("")
6013                    .help("policy scopes must contain a `principal`, `action`, and `resource` element in that order")
6014                    .build()
6015            );
6016        });
6017        let p_src = "permit(principal, action);";
6018        assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6019            expect_err(
6020                p_src,
6021                &miette::Report::new(e),
6022                &ExpectedErrorMessageBuilder::error("this policy is missing the `resource` variable in the scope")
6023                    .exactly_one_underline("")
6024                    .help("policy scopes must contain a `principal`, `action`, and `resource` element in that order")
6025                    .build()
6026            );
6027        });
6028    }
6029
6030    #[cfg(feature = "tolerant-ast")]
6031    #[test]
6032    fn invalid_scope_constraint_tolerant() {
6033        let p_src = "permit(foo, action, resource);";
6034        assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6035            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6036                "found an invalid variable in the policy scope: foo",
6037                ).help(
6038                "policy scopes must contain a `principal`, `action`, and `resource` element in that order",
6039            ).exactly_one_underline("foo").build());
6040        });
6041
6042        let p_src = "permit(resource, action, resource);";
6043        assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6044            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6045                "found the variable `resource` where the variable `principal` must be used",
6046                ).help(
6047                "policy scopes must contain a `principal`, `action`, and `resource` element in that order",
6048            ).exactly_one_underline("resource").build());
6049        });
6050
6051        let p_src = "permit(principal, principal, resource);";
6052        assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6053            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6054                "found the variable `principal` where the variable `action` must be used",
6055                ).help(
6056                "policy scopes must contain a `principal`, `action`, and `resource` element in that order",
6057            ).exactly_one_underline("principal").build());
6058        });
6059        let p_src = "permit(principal, if, resource);";
6060        assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6061            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6062                "found an invalid variable in the policy scope: if",
6063                ).help(
6064                "policy scopes must contain a `principal`, `action`, and `resource` element in that order",
6065            ).exactly_one_underline("if").build());
6066        });
6067
6068        let p_src = "permit(principal, action, like);";
6069        assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6070            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6071                "found an invalid variable in the policy scope: like",
6072                ).help(
6073                "policy scopes must contain a `principal`, `action`, and `resource` element in that order",
6074            ).exactly_one_underline("like").build());
6075        });
6076        let p_src = "permit(principal, action, principal);";
6077        assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6078            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6079                "found the variable `principal` where the variable `resource` must be used",
6080                ).help(
6081                "policy scopes must contain a `principal`, `action`, and `resource` element in that order",
6082            ).exactly_one_underline("principal").build());
6083        });
6084        let p_src = "permit(principal, action, action);";
6085        assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6086            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6087                "found the variable `action` where the variable `resource` must be used",
6088                ).help(
6089                "policy scopes must contain a `principal`, `action`, and `resource` element in that order",
6090            ).exactly_one_underline("action").build());
6091        });
6092    }
6093
6094    #[cfg(feature = "tolerant-ast")]
6095    #[test]
6096    fn invalid_scope_operator_tolerant() {
6097        let p_src = r#"permit(principal > User::"alice", action, resource);"#;
6098        assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6099            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6100                "invalid operator in the policy scope: >",
6101                ).help(
6102                "policy scope clauses can only use `==`, `in`, `is`, or `_ is _ in _`"
6103            ).exactly_one_underline("principal > User::\"alice\"").build());
6104        });
6105        let p_src = r#"permit(principal, action != Action::"view", resource);"#;
6106        assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6107            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6108                "invalid operator in the action scope: !=",
6109                ).help(
6110                "action scope clauses can only use `==` or `in`"
6111            ).exactly_one_underline("action != Action::\"view\"").build());
6112        });
6113        let p_src = r#"permit(principal, action, resource <= Folder::"things");"#;
6114        assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6115            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6116                "invalid operator in the policy scope: <=",
6117                ).help(
6118                "policy scope clauses can only use `==`, `in`, `is`, or `_ is _ in _`"
6119            ).exactly_one_underline("resource <= Folder::\"things\"").build());
6120        });
6121        let p_src = r#"permit(principal = User::"alice", action, resource);"#;
6122        assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6123            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6124                "'=' is not a valid operator in Cedar",
6125                ).help(
6126                "try using '==' instead",
6127            ).exactly_one_underline("principal = User::\"alice\"").build());
6128        });
6129        let p_src = r#"permit(principal, action = Action::"act", resource);"#;
6130        assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6131            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6132                "'=' is not a valid operator in Cedar",
6133                ).help(
6134                "try using '==' instead",
6135            ).exactly_one_underline("action = Action::\"act\"").build());
6136        });
6137        let p_src = r#"permit(principal, action, resource = Photo::"photo");"#;
6138        assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6139            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6140                "'=' is not a valid operator in Cedar",
6141                ).help(
6142                "try using '==' instead",
6143            ).exactly_one_underline("resource = Photo::\"photo\"").build());
6144        });
6145    }
6146
6147    #[cfg(feature = "tolerant-ast")]
6148    #[test]
6149    fn scope_action_eq_set_tolerant() {
6150        let p_src = r#"permit(principal, action == [Action::"view", Action::"edit"], resource);"#;
6151        assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6152            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("expected single entity uid, found set of entity uids").exactly_one_underline(r#"[Action::"view", Action::"edit"]"#).build());
6153        });
6154    }
6155
6156    #[cfg(feature = "tolerant-ast")]
6157    #[test]
6158    fn scope_compare_to_string_tolerant() {
6159        let p_src = r#"permit(principal == "alice", action, resource);"#;
6160        assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6161            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6162                r#"expected an entity uid or matching template slot, found literal `"alice"`"#
6163            ).help(
6164                "try including the entity type if you intended this string to be an entity uid"
6165            ).exactly_one_underline(r#""alice""#).build());
6166        });
6167        let p_src = r#"permit(principal in "bob_friends", action, resource);"#;
6168        assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6169            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6170                r#"expected an entity uid or matching template slot, found literal `"bob_friends"`"#
6171            ).help(
6172                "try including the entity type if you intended this string to be an entity uid"
6173            ).exactly_one_underline(r#""bob_friends""#).build());
6174        });
6175        let p_src = r#"permit(principal, action, resource in "jane_photos");"#;
6176        assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6177            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6178                r#"expected an entity uid or matching template slot, found literal `"jane_photos"`"#
6179            ).help(
6180                "try including the entity type if you intended this string to be an entity uid"
6181            ).exactly_one_underline(r#""jane_photos""#).build());
6182        });
6183        let p_src = r#"permit(principal, action in ["view_actions"], resource);"#;
6184        assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6185            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6186                r#"expected an entity uid, found literal `"view_actions"`"#
6187            ).help(
6188                "try including the entity type if you intended this string to be an entity uid"
6189            ).exactly_one_underline(r#""view_actions""#).build());
6190        });
6191    }
6192
6193    #[cfg(feature = "tolerant-ast")]
6194    #[test]
6195    fn scope_and_tolerant() {
6196        let p_src = r#"permit(principal == User::"alice" && principal in Group::"jane_friends", action, resource);"#;
6197        assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6198            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6199                "expected an entity uid or matching template slot, found a `&&` expression"
6200            ).help(
6201                "the policy scope can only contain one constraint per variable. Consider moving the second operand of this `&&` into a `when` condition",
6202            ).exactly_one_underline(r#"User::"alice" && principal in Group::"jane_friends""#).build());
6203        });
6204    }
6205
6206    #[cfg(feature = "tolerant-ast")]
6207    #[test]
6208    fn scope_or_tolerant() {
6209        let p_src =
6210            r#"permit(principal == User::"alice" || principal == User::"bob", action, resource);"#;
6211        assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6212            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6213                "expected an entity uid or matching template slot, found a `||` expression"
6214            ).help(
6215                "the policy scope can only contain one constraint per variable. Consider moving the second operand of this `||` into a new policy",
6216            ).exactly_one_underline(r#"User::"alice" || principal == User::"bob""#).build());
6217        });
6218    }
6219
6220    #[cfg(feature = "tolerant-ast")]
6221    #[test]
6222    fn scope_action_in_set_set_tolerant() {
6223        let p_src = r#"permit(principal, action in [[Action::"view"]], resource);"#;
6224        assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6225            expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("expected single entity uid, found set of entity uids").exactly_one_underline(r#"[Action::"view"]"#).build());
6226        });
6227    }
6228
6229    #[cfg(feature = "tolerant-ast")]
6230    fn parse_policy_or_template_tolerant(
6231        id: Option<ast::PolicyID>,
6232        text: &str,
6233    ) -> Result<ast::Template> {
6234        let id = id.unwrap_or_else(|| ast::PolicyID::from_string("policy0"));
6235        let cst = text_to_cst::parse_policy_tolerant(text)?;
6236        cst.to_template_tolerant(id)
6237    }
6238}