Skip to main content

boa_ast/expression/
optional.rs

1use super::{Expression, access::PropertyAccessField};
2use crate::{
3    Span, Spanned,
4    function::PrivateName,
5    join_nodes,
6    visitor::{VisitWith, Visitor, VisitorMut},
7};
8use boa_interner::{Interner, ToInternedString};
9use core::{fmt::Write as _, ops::ControlFlow};
10
11/// List of valid operations in an [`Optional`] chain.
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
14#[derive(Clone, Debug, PartialEq)]
15pub enum OptionalOperationKind {
16    /// A property access (`a?.prop`).
17    SimplePropertyAccess {
18        /// The field accessed.
19        field: PropertyAccessField,
20    },
21    /// A private property access (`a?.#prop`).
22    PrivatePropertyAccess {
23        /// The private property accessed.
24        field: PrivateName,
25    },
26    /// A function call (`a?.(arg)`).
27    Call {
28        /// The args passed to the function call.
29        args: Box<[Expression]>,
30    },
31}
32
33impl VisitWith for OptionalOperationKind {
34    fn visit_with<'a, V>(&'a self, visitor: &mut V) -> ControlFlow<V::BreakTy>
35    where
36        V: Visitor<'a>,
37    {
38        match self {
39            Self::SimplePropertyAccess { field } => visitor.visit_property_access_field(field),
40            Self::PrivatePropertyAccess { field } => visitor.visit_private_name(field),
41            Self::Call { args } => {
42                for arg in args {
43                    visitor.visit_expression(arg)?;
44                }
45                ControlFlow::Continue(())
46            }
47        }
48    }
49
50    fn visit_with_mut<'a, V>(&'a mut self, visitor: &mut V) -> ControlFlow<V::BreakTy>
51    where
52        V: VisitorMut<'a>,
53    {
54        match self {
55            Self::SimplePropertyAccess { field } => visitor.visit_property_access_field_mut(field),
56            Self::PrivatePropertyAccess { field } => visitor.visit_private_name_mut(field),
57            Self::Call { args } => {
58                for arg in args.iter_mut() {
59                    visitor.visit_expression_mut(arg)?;
60                }
61                ControlFlow::Continue(())
62            }
63        }
64    }
65}
66
67/// Operation within an [`Optional`] chain.
68///
69/// An operation within an `Optional` chain can be either shorted or non-shorted. A shorted operation
70/// (`?.item`) will force the expression to return `undefined` if the target is `undefined` or `null`.
71/// In contrast, a non-shorted operation (`.prop`) will try to access the property, even if the target
72/// is `undefined` or `null`.
73#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
74#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
75#[derive(Clone, Debug, PartialEq)]
76pub struct OptionalOperation {
77    kind: OptionalOperationKind,
78    shorted: bool,
79    span: Span,
80}
81
82impl OptionalOperation {
83    /// Creates a new `OptionalOperation`.
84    #[inline]
85    #[must_use]
86    pub const fn new(kind: OptionalOperationKind, shorted: bool, span: Span) -> Self {
87        Self {
88            kind,
89            shorted,
90            span,
91        }
92    }
93    /// Gets the kind of operation.
94    #[inline]
95    #[must_use]
96    pub const fn kind(&self) -> &OptionalOperationKind {
97        &self.kind
98    }
99
100    /// Returns `true` if the operation short-circuits the [`Optional`] chain when the target is
101    /// `undefined` or `null`.
102    #[inline]
103    #[must_use]
104    pub const fn shorted(&self) -> bool {
105        self.shorted
106    }
107}
108
109impl Spanned for OptionalOperation {
110    #[inline]
111    fn span(&self) -> Span {
112        self.span
113    }
114}
115
116impl ToInternedString for OptionalOperation {
117    fn to_interned_string(&self, interner: &Interner) -> String {
118        let mut buf = if self.shorted {
119            String::from("?.")
120        } else {
121            if let OptionalOperationKind::SimplePropertyAccess {
122                field: PropertyAccessField::Const(name),
123            } = &self.kind
124            {
125                return format!(".{}", interner.resolve_expect(name.sym()));
126            }
127
128            if let OptionalOperationKind::PrivatePropertyAccess { field } = &self.kind {
129                return format!(".#{}", interner.resolve_expect(field.description()));
130            }
131
132            String::new()
133        };
134        match &self.kind {
135            OptionalOperationKind::SimplePropertyAccess { field } => match field {
136                PropertyAccessField::Const(name) => {
137                    buf.push_str(&interner.resolve_expect(name.sym()).to_string());
138                }
139                PropertyAccessField::Expr(expr) => {
140                    let _ = write!(buf, "[{}]", expr.to_interned_string(interner));
141                }
142            },
143            OptionalOperationKind::PrivatePropertyAccess { field } => {
144                let _ = write!(buf, "#{}", interner.resolve_expect(field.description()));
145            }
146            OptionalOperationKind::Call { args } => {
147                let _ = write!(buf, "({})", join_nodes(interner, args));
148            }
149        }
150        buf
151    }
152}
153
154impl VisitWith for OptionalOperation {
155    fn visit_with<'a, V>(&'a self, visitor: &mut V) -> ControlFlow<V::BreakTy>
156    where
157        V: Visitor<'a>,
158    {
159        visitor.visit_optional_operation_kind(&self.kind)
160    }
161
162    fn visit_with_mut<'a, V>(&'a mut self, visitor: &mut V) -> ControlFlow<V::BreakTy>
163    where
164        V: VisitorMut<'a>,
165    {
166        visitor.visit_optional_operation_kind_mut(&mut self.kind)
167    }
168}
169
170/// An optional chain expression, as defined by the [spec].
171///
172/// [Optional chaining][mdn] allows for short-circuiting property accesses and function calls, which
173/// will return `undefined` instead of returning an error if the access target or the call is
174/// either `undefined` or `null`.
175///
176/// An example of optional chaining:
177///
178/// ```Javascript
179/// const adventurer = {
180///   name: 'Alice',
181///   cat: {
182///     name: 'Dinah'
183///   }
184/// };
185///
186/// console.log(adventurer.cat?.name); // Dinah
187/// console.log(adventurer.dog?.name); // undefined
188/// ```
189///
190/// [spec]: https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#prod-OptionalExpression
191/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining
192#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
193#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
194#[derive(Clone, Debug, PartialEq)]
195pub struct Optional {
196    target: Box<Expression>,
197    chain: Box<[OptionalOperation]>,
198    span: Span,
199}
200
201impl Optional {
202    /// Creates a new `Optional` expression.
203    #[inline]
204    #[must_use]
205    pub fn new(target: Expression, chain: Box<[OptionalOperation]>, span: Span) -> Self {
206        Self {
207            target: Box::new(target),
208            chain,
209            span,
210        }
211    }
212
213    /// Gets the target of this `Optional` expression.
214    #[inline]
215    #[must_use]
216    pub fn target(&self) -> &Expression {
217        self.target.as_ref()
218    }
219
220    /// Gets the chain of accesses and calls that will be applied to the target at runtime.
221    #[inline]
222    #[must_use]
223    pub fn chain(&self) -> &[OptionalOperation] {
224        self.chain.as_ref()
225    }
226}
227
228impl Spanned for Optional {
229    #[inline]
230    fn span(&self) -> Span {
231        self.span
232    }
233}
234
235impl From<Optional> for Expression {
236    fn from(opt: Optional) -> Self {
237        Self::Optional(opt)
238    }
239}
240
241impl ToInternedString for Optional {
242    fn to_interned_string(&self, interner: &Interner) -> String {
243        let mut buf = self.target.to_interned_string(interner);
244
245        for item in &*self.chain {
246            buf.push_str(&item.to_interned_string(interner));
247        }
248
249        buf
250    }
251}
252
253impl VisitWith for Optional {
254    fn visit_with<'a, V>(&'a self, visitor: &mut V) -> ControlFlow<V::BreakTy>
255    where
256        V: Visitor<'a>,
257    {
258        visitor.visit_expression(&self.target)?;
259        for op in &*self.chain {
260            visitor.visit_optional_operation(op)?;
261        }
262        ControlFlow::Continue(())
263    }
264
265    fn visit_with_mut<'a, V>(&'a mut self, visitor: &mut V) -> ControlFlow<V::BreakTy>
266    where
267        V: VisitorMut<'a>,
268    {
269        visitor.visit_expression_mut(&mut self.target)?;
270        for op in &mut *self.chain {
271            visitor.visit_optional_operation_mut(op)?;
272        }
273        ControlFlow::Continue(())
274    }
275}