mirror of https://github.com/boa-dev/boa.git
Browse Source
This Pull Request implements optional chains.
Example:
```Javascript
const adventurer = {
name: 'Alice',
cat: {
name: 'Dinah'
}
};
console.log(adventurer.cat?.name); // Dinah
console.log(adventurer.dog?.name); // undefined
```
Since I needed to implement `Opcode::RotateLeft`, and #2378 had an implementation for `Opcode::RotateRight`, I took the opportunity to integrate both ops into this PR (big thanks to @HalidOdat for the original implementation!).
This PR almost has 100% conformance for the `optional-chaining` test suite. However, there's this one [test](85373b4ce1/test/language/expressions/optional-chaining/member-expression.js
) that can't be solved until we properly set function names for function expressions in object and class definitions.
pull/2391/head
José Julián Espina
2 years ago
17 changed files with 898 additions and 113 deletions
@ -0,0 +1,202 @@ |
|||||||
|
use boa_interner::{Interner, Sym, ToInternedString}; |
||||||
|
|
||||||
|
use crate::syntax::ast::{join_nodes, ContainsSymbol}; |
||||||
|
|
||||||
|
use super::{access::PropertyAccessField, Expression}; |
||||||
|
|
||||||
|
/// List of valid operations in an [`Optional`] chain.
|
||||||
|
#[cfg_attr(feature = "deser", derive(serde::Serialize, serde::Deserialize))] |
||||||
|
#[derive(Clone, Debug, PartialEq)] |
||||||
|
pub enum OptionalOperationKind { |
||||||
|
/// A property access (`a?.prop`).
|
||||||
|
SimplePropertyAccess { |
||||||
|
/// The field accessed.
|
||||||
|
field: PropertyAccessField, |
||||||
|
}, |
||||||
|
/// A private property access (`a?.#prop`).
|
||||||
|
PrivatePropertyAccess { |
||||||
|
/// The private property accessed.
|
||||||
|
field: Sym, |
||||||
|
}, |
||||||
|
/// A function call (`a?.(arg)`).
|
||||||
|
Call { |
||||||
|
/// The args passed to the function call.
|
||||||
|
args: Box<[Expression]>, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
impl OptionalOperationKind { |
||||||
|
#[inline] |
||||||
|
pub(crate) fn contains_arguments(&self) -> bool { |
||||||
|
match self { |
||||||
|
OptionalOperationKind::SimplePropertyAccess { field } => field.contains_arguments(), |
||||||
|
OptionalOperationKind::PrivatePropertyAccess { .. } => false, |
||||||
|
OptionalOperationKind::Call { args } => args.iter().any(Expression::contains_arguments), |
||||||
|
} |
||||||
|
} |
||||||
|
#[inline] |
||||||
|
pub(crate) fn contains(&self, symbol: ContainsSymbol) -> bool { |
||||||
|
match self { |
||||||
|
OptionalOperationKind::SimplePropertyAccess { field } => field.contains(symbol), |
||||||
|
OptionalOperationKind::PrivatePropertyAccess { .. } => false, |
||||||
|
OptionalOperationKind::Call { args } => args.iter().any(|e| e.contains(symbol)), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Operation within an [`Optional`] chain.
|
||||||
|
///
|
||||||
|
/// An operation within an `Optional` chain can be either shorted or non-shorted. A shorted operation
|
||||||
|
/// (`?.item`) will force the expression to return `undefined` if the target is `undefined` or `null`.
|
||||||
|
/// In contrast, a non-shorted operation (`.prop`) will try to access the property, even if the target
|
||||||
|
/// is `undefined` or `null`.
|
||||||
|
#[cfg_attr(feature = "deser", derive(serde::Serialize, serde::Deserialize))] |
||||||
|
#[derive(Clone, Debug, PartialEq)] |
||||||
|
pub struct OptionalOperation { |
||||||
|
kind: OptionalOperationKind, |
||||||
|
shorted: bool, |
||||||
|
} |
||||||
|
|
||||||
|
impl OptionalOperation { |
||||||
|
/// Creates a new `OptionalOperation`.
|
||||||
|
#[inline] |
||||||
|
pub fn new(kind: OptionalOperationKind, shorted: bool) -> Self { |
||||||
|
Self { kind, shorted } |
||||||
|
} |
||||||
|
/// Gets the kind of operation.
|
||||||
|
#[inline] |
||||||
|
pub fn kind(&self) -> &OptionalOperationKind { |
||||||
|
&self.kind |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns `true` if the operation short-circuits the [`Optional`] chain when the target is
|
||||||
|
/// `undefined` or `null`.
|
||||||
|
#[inline] |
||||||
|
pub fn shorted(&self) -> bool { |
||||||
|
self.shorted |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
pub(crate) fn contains_arguments(&self) -> bool { |
||||||
|
self.kind.contains_arguments() |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
pub(crate) fn contains(&self, symbol: ContainsSymbol) -> bool { |
||||||
|
self.kind.contains(symbol) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl ToInternedString for OptionalOperation { |
||||||
|
fn to_interned_string(&self, interner: &Interner) -> String { |
||||||
|
let mut buf = if self.shorted { |
||||||
|
String::from("?.") |
||||||
|
} else { |
||||||
|
if let OptionalOperationKind::SimplePropertyAccess { |
||||||
|
field: PropertyAccessField::Const(name), |
||||||
|
} = &self.kind |
||||||
|
{ |
||||||
|
return format!(".{}", interner.resolve_expect(*name)); |
||||||
|
} |
||||||
|
|
||||||
|
if let OptionalOperationKind::PrivatePropertyAccess { field } = &self.kind { |
||||||
|
return format!(".#{}", interner.resolve_expect(*field)); |
||||||
|
} |
||||||
|
|
||||||
|
String::new() |
||||||
|
}; |
||||||
|
buf.push_str(&match &self.kind { |
||||||
|
OptionalOperationKind::SimplePropertyAccess { field } => match field { |
||||||
|
PropertyAccessField::Const(name) => interner.resolve_expect(*name).to_string(), |
||||||
|
PropertyAccessField::Expr(expr) => { |
||||||
|
format!("[{}]", expr.to_interned_string(interner)) |
||||||
|
} |
||||||
|
}, |
||||||
|
OptionalOperationKind::PrivatePropertyAccess { field } => { |
||||||
|
format!("#{}", interner.resolve_expect(*field)) |
||||||
|
} |
||||||
|
OptionalOperationKind::Call { args } => format!("({})", join_nodes(interner, args)), |
||||||
|
}); |
||||||
|
buf |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// An optional chain expression, as defined by the [spec].
|
||||||
|
///
|
||||||
|
/// [Optional chaining][mdn] allows for short-circuiting property accesses and function calls, which
|
||||||
|
/// will return `undefined` instead of returning an error if the access target or the call is
|
||||||
|
/// either `undefined` or `null`.
|
||||||
|
///
|
||||||
|
/// An example of optional chaining:
|
||||||
|
///
|
||||||
|
/// ```Javascript
|
||||||
|
/// const adventurer = {
|
||||||
|
/// name: 'Alice',
|
||||||
|
/// cat: {
|
||||||
|
/// name: 'Dinah'
|
||||||
|
/// }
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// console.log(adventurer.cat?.name); // Dinah
|
||||||
|
/// console.log(adventurer.dog?.name); // undefined
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#prod-OptionalExpression
|
||||||
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining
|
||||||
|
#[cfg_attr(feature = "deser", derive(serde::Serialize, serde::Deserialize))] |
||||||
|
#[derive(Clone, Debug, PartialEq)] |
||||||
|
pub struct Optional { |
||||||
|
target: Box<Expression>, |
||||||
|
chain: Box<[OptionalOperation]>, |
||||||
|
} |
||||||
|
|
||||||
|
impl Optional { |
||||||
|
/// Creates a new `Optional` expression.
|
||||||
|
#[inline] |
||||||
|
pub fn new(target: Expression, chain: Box<[OptionalOperation]>) -> Self { |
||||||
|
Self { |
||||||
|
target: Box::new(target), |
||||||
|
chain, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Gets the target of this `Optional` expression.
|
||||||
|
#[inline] |
||||||
|
pub fn target(&self) -> &Expression { |
||||||
|
self.target.as_ref() |
||||||
|
} |
||||||
|
|
||||||
|
/// Gets the chain of accesses and calls that will be applied to the target at runtime.
|
||||||
|
#[inline] |
||||||
|
pub fn chain(&self) -> &[OptionalOperation] { |
||||||
|
self.chain.as_ref() |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
pub(crate) fn contains_arguments(&self) -> bool { |
||||||
|
self.target.contains_arguments() |
||||||
|
|| self.chain.iter().any(OptionalOperation::contains_arguments) |
||||||
|
} |
||||||
|
#[inline] |
||||||
|
pub(crate) fn contains(&self, symbol: ContainsSymbol) -> bool { |
||||||
|
self.target.contains(symbol) || self.chain.iter().any(|item| item.contains(symbol)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<Optional> for Expression { |
||||||
|
fn from(opt: Optional) -> Self { |
||||||
|
Expression::Optional(opt) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl ToInternedString for Optional { |
||||||
|
fn to_interned_string(&self, interner: &Interner) -> String { |
||||||
|
let mut buf = self.target.to_interned_string(interner); |
||||||
|
|
||||||
|
for item in &*self.chain { |
||||||
|
buf.push_str(&item.to_interned_string(interner)); |
||||||
|
} |
||||||
|
|
||||||
|
buf |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,168 @@ |
|||||||
|
#[cfg(test)] |
||||||
|
mod tests; |
||||||
|
|
||||||
|
use std::io::Read; |
||||||
|
|
||||||
|
use boa_interner::{Interner, Sym}; |
||||||
|
use boa_profiler::Profiler; |
||||||
|
|
||||||
|
use crate::syntax::{ |
||||||
|
ast::{ |
||||||
|
self, |
||||||
|
expression::{ |
||||||
|
access::PropertyAccessField, Optional, OptionalOperation, OptionalOperationKind, |
||||||
|
}, |
||||||
|
Punctuator, |
||||||
|
}, |
||||||
|
lexer::{Token, TokenKind}, |
||||||
|
parser::{ |
||||||
|
cursor::Cursor, expression::Expression, AllowAwait, AllowYield, ParseError, ParseResult, |
||||||
|
TokenParser, |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
use super::arguments::Arguments; |
||||||
|
|
||||||
|
/// Parses an optional expression.
|
||||||
|
///
|
||||||
|
/// More information:
|
||||||
|
/// - [MDN documentation][mdn]
|
||||||
|
/// - [ECMAScript specification][spec]
|
||||||
|
///
|
||||||
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining
|
||||||
|
/// [spec]: https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#prod-OptionalExpression
|
||||||
|
#[derive(Debug, Clone)] |
||||||
|
pub(in crate::syntax::parser) struct OptionalExpression { |
||||||
|
allow_yield: AllowYield, |
||||||
|
allow_await: AllowAwait, |
||||||
|
target: ast::Expression, |
||||||
|
} |
||||||
|
|
||||||
|
impl OptionalExpression { |
||||||
|
/// Creates a new `OptionalExpression` parser.
|
||||||
|
pub(in crate::syntax::parser) fn new<Y, A>( |
||||||
|
allow_yield: Y, |
||||||
|
allow_await: A, |
||||||
|
target: ast::Expression, |
||||||
|
) -> Self |
||||||
|
where |
||||||
|
Y: Into<AllowYield>, |
||||||
|
A: Into<AllowAwait>, |
||||||
|
{ |
||||||
|
Self { |
||||||
|
allow_yield: allow_yield.into(), |
||||||
|
allow_await: allow_await.into(), |
||||||
|
target, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<R> TokenParser<R> for OptionalExpression |
||||||
|
where |
||||||
|
R: Read, |
||||||
|
{ |
||||||
|
type Output = Optional; |
||||||
|
|
||||||
|
fn parse(self, cursor: &mut Cursor<R>, interner: &mut Interner) -> ParseResult<Self::Output> { |
||||||
|
fn parse_const_access<R: Read>( |
||||||
|
cursor: &mut Cursor<R>, |
||||||
|
token: &Token, |
||||||
|
interner: &mut Interner, |
||||||
|
) -> ParseResult<OptionalOperationKind> { |
||||||
|
let item = match token.kind() { |
||||||
|
TokenKind::Identifier(name) => OptionalOperationKind::SimplePropertyAccess { |
||||||
|
field: PropertyAccessField::Const(*name), |
||||||
|
}, |
||||||
|
TokenKind::Keyword((kw, _)) => OptionalOperationKind::SimplePropertyAccess { |
||||||
|
field: PropertyAccessField::Const(kw.to_sym(interner)), |
||||||
|
}, |
||||||
|
TokenKind::BooleanLiteral(true) => OptionalOperationKind::SimplePropertyAccess { |
||||||
|
field: PropertyAccessField::Const(Sym::TRUE), |
||||||
|
}, |
||||||
|
TokenKind::BooleanLiteral(false) => OptionalOperationKind::SimplePropertyAccess { |
||||||
|
field: PropertyAccessField::Const(Sym::FALSE), |
||||||
|
}, |
||||||
|
TokenKind::NullLiteral => OptionalOperationKind::SimplePropertyAccess { |
||||||
|
field: PropertyAccessField::Const(Sym::NULL), |
||||||
|
}, |
||||||
|
TokenKind::PrivateIdentifier(name) => { |
||||||
|
cursor.push_used_private_identifier(*name, token.span().start())?; |
||||||
|
OptionalOperationKind::PrivatePropertyAccess { field: *name } |
||||||
|
} |
||||||
|
_ => { |
||||||
|
return Err(ParseError::expected( |
||||||
|
["identifier".to_owned()], |
||||||
|
token.to_string(interner), |
||||||
|
token.span(), |
||||||
|
"optional chain", |
||||||
|
)) |
||||||
|
} |
||||||
|
}; |
||||||
|
Ok(item) |
||||||
|
} |
||||||
|
let _timer = Profiler::global().start_event("OptionalExpression", "Parsing"); |
||||||
|
|
||||||
|
let mut items = Vec::new(); |
||||||
|
|
||||||
|
while let Some(token) = cursor.peek(0, interner)? { |
||||||
|
let shorted = match token.kind() { |
||||||
|
TokenKind::Punctuator(Punctuator::Optional) => { |
||||||
|
cursor.next(interner).expect("token disappeared"); |
||||||
|
true |
||||||
|
} |
||||||
|
TokenKind::Punctuator(Punctuator::OpenParen | Punctuator::OpenBracket) => false, |
||||||
|
TokenKind::Punctuator(Punctuator::Dot) => { |
||||||
|
cursor.next(interner).expect("token disappeared"); |
||||||
|
let field = cursor.next(interner)?.ok_or(ParseError::AbruptEnd)?; |
||||||
|
|
||||||
|
let item = parse_const_access(cursor, &field, interner)?; |
||||||
|
|
||||||
|
items.push(OptionalOperation::new(item, false)); |
||||||
|
continue; |
||||||
|
} |
||||||
|
TokenKind::TemplateMiddle(_) | TokenKind::TemplateNoSubstitution(_) => { |
||||||
|
return Err(ParseError::general( |
||||||
|
"Invalid tagged template on optional chain", |
||||||
|
token.span().start(), |
||||||
|
)) |
||||||
|
} |
||||||
|
_ => break, |
||||||
|
}; |
||||||
|
|
||||||
|
let token = cursor.peek(0, interner)?.ok_or(ParseError::AbruptEnd)?; |
||||||
|
|
||||||
|
let item = match token.kind() { |
||||||
|
TokenKind::Punctuator(Punctuator::OpenParen) => { |
||||||
|
let args = Arguments::new(self.allow_yield, self.allow_await) |
||||||
|
.parse(cursor, interner)?; |
||||||
|
OptionalOperationKind::Call { args } |
||||||
|
} |
||||||
|
TokenKind::Punctuator(Punctuator::OpenBracket) => { |
||||||
|
cursor |
||||||
|
.next(interner)? |
||||||
|
.expect("open bracket punctuator token disappeared"); // We move the parser forward.
|
||||||
|
let idx = Expression::new(None, true, self.allow_yield, self.allow_await) |
||||||
|
.parse(cursor, interner)?; |
||||||
|
cursor.expect(Punctuator::CloseBracket, "optional chain", interner)?; |
||||||
|
OptionalOperationKind::SimplePropertyAccess { |
||||||
|
field: PropertyAccessField::Expr(Box::new(idx)), |
||||||
|
} |
||||||
|
} |
||||||
|
TokenKind::TemplateMiddle(_) | TokenKind::TemplateNoSubstitution(_) => { |
||||||
|
return Err(ParseError::general( |
||||||
|
"Invalid tagged template on optional chain", |
||||||
|
token.span().start(), |
||||||
|
)) |
||||||
|
} |
||||||
|
_ => { |
||||||
|
let token = cursor.next(interner)?.expect("token disappeared"); |
||||||
|
parse_const_access(cursor, &token, interner)? |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
items.push(OptionalOperation::new(item, shorted)); |
||||||
|
} |
||||||
|
|
||||||
|
Ok(Optional::new(self.target, items.into())) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,93 @@ |
|||||||
|
use boa_interner::Interner; |
||||||
|
use boa_macros::utf16; |
||||||
|
|
||||||
|
use crate::syntax::{ |
||||||
|
ast::{ |
||||||
|
expression::{ |
||||||
|
access::PropertyAccessField, literal::Literal, Identifier, Optional, OptionalOperation, |
||||||
|
OptionalOperationKind, |
||||||
|
}, |
||||||
|
Expression, Statement, |
||||||
|
}, |
||||||
|
parser::tests::{check_invalid, check_parser}, |
||||||
|
}; |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn simple() { |
||||||
|
let mut interner = Interner::default(); |
||||||
|
|
||||||
|
check_parser( |
||||||
|
r#"5?.name"#, |
||||||
|
vec![Statement::Expression( |
||||||
|
Optional::new( |
||||||
|
Literal::Int(5).into(), |
||||||
|
vec![OptionalOperation::new( |
||||||
|
OptionalOperationKind::SimplePropertyAccess { |
||||||
|
field: PropertyAccessField::Const( |
||||||
|
interner.get_or_intern_static("name", utf16!("name")), |
||||||
|
), |
||||||
|
}, |
||||||
|
true, |
||||||
|
)] |
||||||
|
.into(), |
||||||
|
) |
||||||
|
.into(), |
||||||
|
) |
||||||
|
.into()], |
||||||
|
interner, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn complex_chain() { |
||||||
|
let mut interner = Interner::default(); |
||||||
|
|
||||||
|
check_parser( |
||||||
|
r#"a?.b(true)?.["c"]"#, |
||||||
|
vec![Statement::Expression( |
||||||
|
Optional::new( |
||||||
|
Identifier::new(interner.get_or_intern_static("a", utf16!("a"))).into(), |
||||||
|
vec![ |
||||||
|
OptionalOperation::new( |
||||||
|
OptionalOperationKind::SimplePropertyAccess { |
||||||
|
field: PropertyAccessField::Const( |
||||||
|
interner.get_or_intern_static("b", utf16!("b")), |
||||||
|
), |
||||||
|
}, |
||||||
|
true, |
||||||
|
), |
||||||
|
OptionalOperation::new( |
||||||
|
OptionalOperationKind::Call { |
||||||
|
args: vec![Expression::Literal(Literal::Bool(true))].into(), |
||||||
|
}, |
||||||
|
false, |
||||||
|
), |
||||||
|
OptionalOperation::new( |
||||||
|
OptionalOperationKind::SimplePropertyAccess { |
||||||
|
field: PropertyAccessField::Expr(Box::new( |
||||||
|
Literal::String(interner.get_or_intern_static("c", utf16!("c"))) |
||||||
|
.into(), |
||||||
|
)), |
||||||
|
}, |
||||||
|
true, |
||||||
|
), |
||||||
|
] |
||||||
|
.into(), |
||||||
|
) |
||||||
|
.into(), |
||||||
|
) |
||||||
|
.into()], |
||||||
|
interner, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn reject_templates() { |
||||||
|
check_invalid("console.log?.`Hello`"); |
||||||
|
check_invalid("console?.log`Hello`"); |
||||||
|
check_invalid( |
||||||
|
r#" |
||||||
|
const a = console?.log |
||||||
|
`Hello`"#, |
||||||
|
); |
||||||
|
} |
Loading…
Reference in new issue