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