aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/qml/compiler/qv4bytecodegenerator_p.h14
-rw-r--r--src/qml/compiler/qv4codegen.cpp244
-rw-r--r--src/qml/compiler/qv4codegen_p.h21
-rw-r--r--src/qml/compiler/qv4instr_moth.cpp8
-rw-r--r--src/qml/compiler/qv4instr_moth_p.h4
-rw-r--r--src/qml/doc/src/external-resources.qdoc4
-rw-r--r--src/qml/doc/src/javascript/hostenvironment.qdoc2
-rw-r--r--src/qml/jit/qv4baselineassembler.cpp17
-rw-r--r--src/qml/jit/qv4baselineassembler_p.h1
-rw-r--r--src/qml/jit/qv4baselinejit.cpp14
-rw-r--r--src/qml/jit/qv4baselinejit_p.h2
-rw-r--r--src/qml/jsruntime/qv4vme_moth.cpp27
-rw-r--r--src/qml/parser/qqmljs.g74
-rw-r--r--src/qml/parser/qqmljsast.cpp38
-rw-r--r--src/qml/parser/qqmljsast_p.h4
-rw-r--r--src/qml/parser/qqmljslexer.cpp4
m---------tests/auto/qml/ecmascripttests/test2620
-rw-r--r--tests/auto/qml/qmlformat/tst_qmlformat.cpp1
-rw-r--r--tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp87
-rw-r--r--tests/auto/qml/qqmllanguage/data/optionalChaining.LHS.errors.txt48
-rw-r--r--tests/auto/qml/qqmllanguage/data/optionalChaining.LHS.qml58
-rw-r--r--tests/auto/qml/qqmllanguage/data/questionDotEOF.errors.txt1
-rw-r--r--tests/auto/qml/qqmllanguage/data/questionDotEOF.qml3
-rw-r--r--tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp4
-rw-r--r--tools/qmlformat/dumpastvisitor.cpp6
25 files changed, 665 insertions, 21 deletions
diff --git a/src/qml/compiler/qv4bytecodegenerator_p.h b/src/qml/compiler/qv4bytecodegenerator_p.h
index 5244c443c4..4cd3b37ad3 100644
--- a/src/qml/compiler/qv4bytecodegenerator_p.h
+++ b/src/qml/compiler/qv4bytecodegenerator_p.h
@@ -196,6 +196,20 @@ QT_WARNING_POP
return addJumpInstruction(data);
}
+ Q_REQUIRED_RESULT Jump jumpOptionalLookup(int index)
+ {
+ Instruction::GetOptionalLookup data{};
+ data.index = index;
+ return addJumpInstruction(data);
+ }
+
+ Q_REQUIRED_RESULT Jump jumpOptionalProperty(int name)
+ {
+ Instruction::LoadOptionalProperty data{};
+ data.name = name;
+ return addJumpInstruction(data);
+ }
+
void jumpStrictEqual(const StackSlot &lhs, const Label &target)
{
Instruction::CmpStrictEqual cmp;
diff --git a/src/qml/compiler/qv4codegen.cpp b/src/qml/compiler/qv4codegen.cpp
index ab40f0ceba..d13ef1bc86 100644
--- a/src/qml/compiler/qv4codegen.cpp
+++ b/src/qml/compiler/qv4codegen.cpp
@@ -1253,11 +1253,30 @@ bool Codegen::visit(ArrayPattern *ast)
bool Codegen::visit(ArrayMemberExpression *ast)
{
+ auto label = traverseOptionalChain(ast);
+ auto targetLabel = label.has_value() ? label.value() : Moth::BytecodeGenerator::Label();
+
if (hasError())
return false;
+ if (ast->isOptional)
+ Q_ASSERT(m_optionalChainLabels.contains(ast));
+
+
TailCallBlocker blockTailCalls(this);
Reference base = expression(ast->base);
+
+ auto writeSkip = [&]() {
+ auto acc = Reference::fromAccumulator(this).storeOnStack();
+ base.loadInAccumulator();
+ bytecodeGenerator->addInstruction(Instruction::CmpEqNull());
+ auto jumpFalse = bytecodeGenerator->jumpFalse();
+ bytecodeGenerator->addInstruction(Instruction::LoadUndefined());
+ bytecodeGenerator->jump().link(m_optionalChainLabels.take(ast));
+ jumpFalse.link();
+ acc.loadInAccumulator();
+ };
+
if (hasError())
return false;
if (base.isSuper()) {
@@ -1272,17 +1291,31 @@ bool Codegen::visit(ArrayMemberExpression *ast)
QString s = str->value.toString();
uint arrayIndex = stringToArrayIndex(s);
if (arrayIndex == UINT_MAX) {
- setExprResult(Reference::fromMember(base, str->value.toString()));
+ auto jumpLabel = ast->isOptional ? m_optionalChainLabels.take(ast) : Moth::BytecodeGenerator::Label();
+
+ setExprResult(Reference::fromMember(base, str->value.toString(), jumpLabel, targetLabel));
return false;
}
+
+ if (ast->isOptional)
+ writeSkip();
+
Reference index = Reference::fromConst(this, QV4::Encode(arrayIndex));
- setExprResult(Reference::fromSubscript(base, index));
+ setExprResult(Reference::fromSubscript(base, index, targetLabel));
return false;
}
+
+
+ if (ast->isOptional)
+ writeSkip();
+
Reference index = expression(ast->expression);
+
if (hasError())
return false;
- setExprResult(Reference::fromSubscript(base, index));
+
+ setExprResult(Reference::fromSubscript(base, index, targetLabel));
+
return false;
}
@@ -1917,6 +1950,8 @@ bool Codegen::visit(CallExpression *ast)
if (hasError())
return false;
+ auto label = traverseOptionalChain(ast);
+
RegisterScope scope(this);
TailCallBlocker blockTailCalls(this);
@@ -1944,6 +1979,22 @@ bool Codegen::visit(CallExpression *ast)
int thisObject = bytecodeGenerator->newRegister();
int functionObject = bytecodeGenerator->newRegister();
+ if (ast->isOptional || (!base.optionalChainJumpLabel.isNull() && base.optionalChainJumpLabel->isValid())) {
+ if (ast->isOptional)
+ Q_ASSERT(m_optionalChainLabels.contains(ast));
+
+ auto jumpLabel = ast->isOptional ? m_optionalChainLabels.take(ast) : *base.optionalChainJumpLabel.get();
+
+ auto acc = Reference::fromAccumulator(this).storeOnStack();
+ base.loadInAccumulator();
+ bytecodeGenerator->addInstruction(Instruction::CmpEqNull());
+ auto jumpFalse = bytecodeGenerator->jumpFalse();
+ bytecodeGenerator->addInstruction(Instruction::LoadUndefined());
+ bytecodeGenerator->jump().link(jumpLabel);
+ jumpFalse.link();
+ acc.loadInAccumulator();
+ }
+
auto calldata = pushArgs(ast->arguments);
if (hasError())
return false;
@@ -1977,15 +2028,28 @@ bool Codegen::visit(CallExpression *ast)
}
setExprResult(Reference::fromAccumulator(this));
+
+ if (label.has_value())
+ label->link();
+
return false;
}
- handleCall(base, calldata, functionObject, thisObject);
+ handleCall(base, calldata, functionObject, thisObject, ast->isOptional);
+
+ if (label.has_value())
+ label->link();
+
return false;
}
-void Codegen::handleCall(Reference &base, Arguments calldata, int slotForFunction, int slotForThisObject)
+void Codegen::endVisit(CallExpression *ast)
+{
+ m_seenOptionalChainNodes.remove(ast);
+}
+
+void Codegen::handleCall(Reference &base, Arguments calldata, int slotForFunction, int slotForThisObject, bool optional)
{
//### Do we really need all these call instructions? can's we load the callee in a temp?
if (base.type == Reference::Member) {
@@ -2012,7 +2076,7 @@ void Codegen::handleCall(Reference &base, Arguments calldata, int slotForFunctio
call.argv = calldata.argv;
bytecodeGenerator->addInstruction(call);
} else if (base.type == Reference::Name) {
- if (base.name == QStringLiteral("eval")) {
+ if (base.name == QStringLiteral("eval") && !optional) {
Instruction::CallPossiblyDirectEval call;
call.argc = calldata.argc;
call.argv = calldata.argv;
@@ -2171,12 +2235,18 @@ bool Codegen::visit(DeleteExpression *ast)
if (hasError())
return false;
+ auto label = traverseOptionalChain(ast);
+
RegisterScope scope(this);
TailCallBlocker blockTailCalls(this);
Reference expr = expression(ast->expression);
if (hasError())
return false;
+ // If there is a label, there is a chain and that should only be possible with those two kinds of references
+ if (label.has_value())
+ Q_ASSERT(expr.type == Reference::Member || expr.type == Reference::Subscript);
+
switch (expr.type) {
case Reference::SuperProperty:
// ### this should throw a reference error at runtime.
@@ -2207,6 +2277,13 @@ bool Codegen::visit(DeleteExpression *ast)
case Reference::Member: {
//### maybe add a variant where the base can be in the accumulator?
expr = expr.asLValue();
+
+ if (!expr.optionalChainJumpLabel.isNull() && expr.optionalChainJumpLabel->isValid()) {
+ expr.loadInAccumulator();
+ bytecodeGenerator->addInstruction(Instruction::CmpEqNull());
+ bytecodeGenerator->jumpTrue().link(*expr.optionalChainJumpLabel.get());
+ }
+
Instruction::LoadRuntimeString instr;
instr.stringId = expr.propertyNameIndex;
bytecodeGenerator->addInstruction(instr);
@@ -2217,16 +2294,41 @@ bool Codegen::visit(DeleteExpression *ast)
del.index = index.stackSlot();
bytecodeGenerator->addInstruction(del);
setExprResult(Reference::fromAccumulator(this));
+
+ if (label.has_value()) {
+ auto jump = bytecodeGenerator->jump();
+ label->link();
+ Instruction::LoadTrue loadTrue;
+ bytecodeGenerator->addInstruction(loadTrue);
+ jump.link();
+ }
+
return false;
}
case Reference::Subscript: {
//### maybe add a variant where the index can be in the accumulator?
expr = expr.asLValue();
+
+ if (!expr.optionalChainJumpLabel.isNull() && expr.optionalChainJumpLabel->isValid()) {
+ expr.loadInAccumulator();
+ bytecodeGenerator->addInstruction(Instruction::CmpEqNull());
+ bytecodeGenerator->jumpTrue().link(*expr.optionalChainJumpLabel.get());
+ }
+
Instruction::DeleteProperty del;
del.base = expr.elementBase;
del.index = expr.elementSubscript.stackSlot();
bytecodeGenerator->addInstruction(del);
setExprResult(Reference::fromAccumulator(this));
+
+ if (label.has_value()) {
+ auto jump = bytecodeGenerator->jump();
+ label->link();
+ Instruction::LoadTrue loadTrue;
+ bytecodeGenerator->addInstruction(loadTrue);
+ jump.link();
+ }
+
return false;
}
default:
@@ -2237,6 +2339,10 @@ bool Codegen::visit(DeleteExpression *ast)
return false;
}
+void Codegen::endVisit(DeleteExpression *ast) {
+ m_seenOptionalChainNodes.remove(ast);
+}
+
bool Codegen::visit(FalseLiteral *)
{
if (hasError())
@@ -2255,11 +2361,81 @@ bool Codegen::visit(SuperLiteral *)
return false;
}
+std::optional<Moth::BytecodeGenerator::Label> Codegen::traverseOptionalChain(Node *node) {
+ if (m_seenOptionalChainNodes.contains(node))
+ return {};
+
+ auto label = bytecodeGenerator->newLabel();
+
+ auto isOptionalChainNode = [](const Node *node) {
+ return node->kind == Node::Kind_FieldMemberExpression ||
+ node->kind == Node::Kind_CallExpression ||
+ node->kind == Node::Kind_ArrayMemberExpression ||
+ node->kind == Node::Kind_DeleteExpression;
+ };
+
+ bool labelUsed = false;
+
+ while (isOptionalChainNode(node)) {
+ m_seenOptionalChainNodes.insert(node);
+
+ switch (node->kind) {
+ case Node::Kind_FieldMemberExpression: {
+ auto *fme = AST::cast<FieldMemberExpression*>(node);
+
+ if (fme->isOptional) {
+ m_optionalChainLabels.insert(fme, label);
+ labelUsed = true;
+ }
+
+ node = fme->base;
+ break;
+ }
+ case Node::Kind_CallExpression: {
+ auto *ce = AST::cast<CallExpression*>(node);
+
+ if (ce->isOptional) {
+ m_optionalChainLabels.insert(ce, label);
+ labelUsed = true;
+ }
+
+ node = ce->base;
+ break;
+ }
+ case Node::Kind_ArrayMemberExpression: {
+ auto *ame = AST::cast<ArrayMemberExpression*>(node);
+
+ if (ame->isOptional) {
+ m_optionalChainLabels.insert(ame, label);
+ labelUsed = true;
+ }
+
+ node = ame->base;
+ break;
+ }
+ case Node::Kind_DeleteExpression: {
+ auto *de = AST::cast<DeleteExpression*>(node);
+ node = de->expression;
+ break;
+ }
+ }
+ }
+
+ if (!labelUsed) {
+ label.link(); // If we don't link the unused label here, we would hit an assert later.
+ return {};
+ }
+
+ return label;
+}
+
bool Codegen::visit(FieldMemberExpression *ast)
{
if (hasError())
return false;
+ auto label = traverseOptionalChain(ast);
+
TailCallBlocker blockTailCalls(this);
if (AST::IdentifierExpression *id = AST::cast<AST::IdentifierExpression *>(ast->base)) {
if (id->name == QLatin1String("new")) {
@@ -2270,16 +2446,28 @@ bool Codegen::visit(FieldMemberExpression *ast)
Reference r = referenceForName(QStringLiteral("new.target"), false);
r.isReadonly = true;
setExprResult(r);
+
+ if (label.has_value())
+ label->link();
+
return false;
}
Reference r = Reference::fromStackSlot(this, CallData::NewTarget);
setExprResult(r);
+
+ if (label.has_value())
+ label->link();
+
return false;
}
}
Reference base = expression(ast->base);
+
+ if (ast->isOptional)
+ Q_ASSERT(m_optionalChainLabels.contains(ast));
+
if (hasError())
return false;
if (base.isSuper()) {
@@ -2288,12 +2476,25 @@ bool Codegen::visit(FieldMemberExpression *ast)
bytecodeGenerator->addInstruction(load);
Reference property = Reference::fromAccumulator(this).storeOnStack();
setExprResult(Reference::fromSuperProperty(property));
+
+ if (label.has_value())
+ label->link();
+
return false;
}
- setExprResult(Reference::fromMember(base, ast->name.toString()));
+
+ setExprResult(Reference::fromMember(base, ast->name.toString(),
+ ast->isOptional ? m_optionalChainLabels.take(ast) : Moth::BytecodeGenerator::Label(),
+ label.has_value() ? label.value() : Moth::BytecodeGenerator::Label()));
+
return false;
}
+void Codegen::endVisit(FieldMemberExpression *ast)
+{
+ m_seenOptionalChainNodes.remove(ast);
+}
+
bool Codegen::visit(TaggedTemplate *ast)
{
if (hasError())
@@ -4454,13 +4655,26 @@ QT_WARNING_POP
propertyBase.loadInAccumulator();
tdzCheck(requiresTDZCheck);
if (!disable_lookups && codegen->useFastLookups) {
- Instruction::GetLookup load;
- load.index = codegen->registerGetterLookup(propertyNameIndex);
- codegen->bytecodeGenerator->addInstruction(load);
+ if (optionalChainJumpLabel->isValid()) { // If we got a valid jump label, this means it's an optional lookup
+ auto jump = codegen->bytecodeGenerator->jumpOptionalLookup(codegen->registerGetterLookup(propertyNameIndex));
+ jump.link(*optionalChainJumpLabel.get());
+ } else {
+ Instruction::GetLookup load;
+ load.index = codegen->registerGetterLookup(propertyNameIndex);
+ codegen->bytecodeGenerator->addInstruction(load);
+ }
} else {
- Instruction::LoadProperty load;
- load.name = propertyNameIndex;
- codegen->bytecodeGenerator->addInstruction(load);
+ if (optionalChainJumpLabel->isValid()) {
+ auto jump = codegen->bytecodeGenerator->jumpOptionalProperty(propertyNameIndex);
+ jump.link(*optionalChainJumpLabel.get());
+ } else {
+ Instruction::LoadProperty load;
+ load.name = propertyNameIndex;
+ codegen->bytecodeGenerator->addInstruction(load);
+ }
+ }
+ if (optionalChainTargetLabel->isValid()) {
+ optionalChainTargetLabel->link();
}
return;
case Import: {
@@ -4476,6 +4690,10 @@ QT_WARNING_POP
Instruction::LoadElement load;
load.base = elementBase;
codegen->bytecodeGenerator->addInstruction(load);
+
+ if (optionalChainTargetLabel->isValid()) {
+ optionalChainTargetLabel->link();
+ }
} return;
case Invalid:
break;
diff --git a/src/qml/compiler/qv4codegen_p.h b/src/qml/compiler/qv4codegen_p.h
index e7d7a21294..e720cb2973 100644
--- a/src/qml/compiler/qv4codegen_p.h
+++ b/src/qml/compiler/qv4codegen_p.h
@@ -60,6 +60,8 @@
#include <private/qv4bytecodegenerator_p.h>
#include <private/qv4calldata_p.h>
+#include <QtCore/qsharedpointer.h>
+
QT_BEGIN_NAMESPACE
namespace QV4 {
@@ -277,11 +279,15 @@ public:
r.name = name;
return r;
}
- static Reference fromMember(const Reference &baseRef, const QString &name) {
+ static Reference fromMember(const Reference &baseRef, const QString &name,
+ Moth::BytecodeGenerator::Label jumpLabel = Moth::BytecodeGenerator::Label(),
+ Moth::BytecodeGenerator::Label targetLabel = Moth::BytecodeGenerator::Label()) {
Reference r(baseRef.codegen, Member);
r.propertyBase = baseRef.asRValue();
r.propertyNameIndex = r.codegen->registerString(name);
r.requiresTDZCheck = baseRef.requiresTDZCheck;
+ r.optionalChainJumpLabel.reset(new Moth::BytecodeGenerator::Label(jumpLabel));
+ r.optionalChainTargetLabel.reset(new Moth::BytecodeGenerator::Label(targetLabel));
return r;
}
static Reference fromSuperProperty(const Reference &property) {
@@ -291,13 +297,14 @@ public:
r.subscriptRequiresTDZCheck = property.requiresTDZCheck;
return r;
}
- static Reference fromSubscript(const Reference &baseRef, const Reference &subscript) {
+ static Reference fromSubscript(const Reference &baseRef, const Reference &subscript, Moth::BytecodeGenerator::Label targetLabel = Moth::BytecodeGenerator::Label()) {
Q_ASSERT(baseRef.isStackSlot());
Reference r(baseRef.codegen, Subscript);
r.elementBase = baseRef.stackSlot();
r.elementSubscript = subscript.asRValue();
r.requiresTDZCheck = baseRef.requiresTDZCheck;
r.subscriptRequiresTDZCheck = subscript.requiresTDZCheck;
+ r.optionalChainTargetLabel.reset(new Moth::BytecodeGenerator::Label(targetLabel));
return r;
}
static Reference fromConst(Codegen *cg, QV4::ReturnedValue constant) {
@@ -374,6 +381,8 @@ public:
quint32 isVolatile:1;
quint32 global:1;
quint32 qmlGlobal:1;
+ QSharedPointer<Moth::BytecodeGenerator::Label> optionalChainJumpLabel;
+ QSharedPointer<Moth::BytecodeGenerator::Label> optionalChainTargetLabel;
private:
void storeAccumulator() const;
@@ -598,11 +607,14 @@ protected:
bool visit(QQmlJS::AST::ArrayMemberExpression *ast) override;
bool visit(QQmlJS::AST::BinaryExpression *ast) override;
bool visit(QQmlJS::AST::CallExpression *ast) override;
+ void endVisit(QQmlJS::AST::CallExpression *ast) override;
bool visit(QQmlJS::AST::ConditionalExpression *ast) override;
bool visit(QQmlJS::AST::DeleteExpression *ast) override;
+ void endVisit(QQmlJS::AST::DeleteExpression *ast) override;
bool visit(QQmlJS::AST::FalseLiteral *ast) override;
bool visit(QQmlJS::AST::SuperLiteral *ast) override;
bool visit(QQmlJS::AST::FieldMemberExpression *ast) override;
+ void endVisit(QQmlJS::AST::FieldMemberExpression *ast) override;
bool visit(QQmlJS::AST::TaggedTemplate *ast) override;
bool visit(QQmlJS::AST::FunctionExpression *ast) override;
bool visit(QQmlJS::AST::IdentifierExpression *ast) override;
@@ -686,7 +698,7 @@ public:
Reference jumpBinop(QSOperator::Op oper, Reference &left, Reference &right);
struct Arguments { int argc; int argv; bool hasSpread; };
Arguments pushArgs(QQmlJS::AST::ArgumentList *args);
- void handleCall(Reference &base, Arguments calldata, int slotForFunction, int slotForThisObject);
+ void handleCall(Reference &base, Arguments calldata, int slotForFunction, int slotForThisObject, bool optional = false);
Arguments pushTemplateArgs(QQmlJS::AST::TemplateLiteral *args);
bool handleTaggedTemplate(Reference base, QQmlJS::AST::TaggedTemplate *ast);
@@ -776,6 +788,8 @@ protected:
bool functionEndsWithReturn = false;
bool _tailCallsAreAllowed = true;
QSet<QString> m_globalNames;
+ QSet<QQmlJS::AST::Node*> m_seenOptionalChainNodes;
+ QHash<QQmlJS::AST::Node*, Moth::BytecodeGenerator::Label> m_optionalChainLabels;
ControlFlow *controlFlow = nullptr;
@@ -812,6 +826,7 @@ private:
void handleConstruct(const Reference &base, QQmlJS::AST::ArgumentList *args);
void throwError(ErrorType errorType, const QQmlJS::SourceLocation &loc,
const QString &detail);
+ std::optional<Moth::BytecodeGenerator::Label> traverseOptionalChain(QQmlJS::AST::Node *node);
};
}
diff --git a/src/qml/compiler/qv4instr_moth.cpp b/src/qml/compiler/qv4instr_moth.cpp
index 640a908dd3..c791790cba 100644
--- a/src/qml/compiler/qv4instr_moth.cpp
+++ b/src/qml/compiler/qv4instr_moth.cpp
@@ -280,10 +280,18 @@ void dumpBytecode(const char *code, int len, int nLocals, int nFormals, int /*st
d << "acc[" << name << "]";
MOTH_END_INSTR(LoadProperty)
+ MOTH_BEGIN_INSTR(LoadOptionalProperty)
+ d << "acc[" << name << "], jump(" << ABSOLUTE_OFFSET() << ")";
+ MOTH_END_INSTR(LoadOptionalProperty)
+
MOTH_BEGIN_INSTR(GetLookup)
d << "acc(" << index << ")";
MOTH_END_INSTR(GetLookup)
+ MOTH_BEGIN_INSTR(GetOptionalLookup)
+ d << "acc(" << index << "), jump(" << ABSOLUTE_OFFSET() << ")";
+ MOTH_END_INSTR(GetOptionalLookup)
+
MOTH_BEGIN_INSTR(StoreProperty)
d << dumpRegister(base, nFormals) << "[" << name<< "]";
MOTH_END_INSTR(StoreProperty)
diff --git a/src/qml/compiler/qv4instr_moth_p.h b/src/qml/compiler/qv4instr_moth_p.h
index 254e1c46e9..69e4eb26f3 100644
--- a/src/qml/compiler/qv4instr_moth_p.h
+++ b/src/qml/compiler/qv4instr_moth_p.h
@@ -89,7 +89,9 @@ QT_BEGIN_NAMESPACE
#define INSTR_StoreNameSloppy(op) INSTRUCTION(op, StoreNameSloppy, 1, name)
#define INSTR_StoreNameStrict(op) INSTRUCTION(op, StoreNameStrict, 1, name)
#define INSTR_LoadProperty(op) INSTRUCTION(op, LoadProperty, 1, name)
+#define INSTR_LoadOptionalProperty(op) INSTRUCTION(op, LoadOptionalProperty, 2, name, offset)
#define INSTR_GetLookup(op) INSTRUCTION(op, GetLookup, 1, index)
+#define INSTR_GetOptionalLookup(op) INSTRUCTION(op, GetOptionalLookup, 2, index, offset)
#define INSTR_LoadIdObject(op) INSTRUCTION(op, LoadIdObject, 2, index, base)
#define INSTR_Yield(op) INSTRUCTION(op, Yield, 0)
#define INSTR_YieldStar(op) INSTRUCTION(op, YieldStar, 0)
@@ -229,7 +231,9 @@ QT_BEGIN_NAMESPACE
F(LoadElement) \
F(StoreElement) \
F(LoadProperty) \
+ F(LoadOptionalProperty) \
F(GetLookup) \
+ F(GetOptionalLookup) \
F(StoreProperty) \
F(SetLookup) \
F(LoadSuperProperty) \
diff --git a/src/qml/doc/src/external-resources.qdoc b/src/qml/doc/src/external-resources.qdoc
index c11174db43..b4a0903f5d 100644
--- a/src/qml/doc/src/external-resources.qdoc
+++ b/src/qml/doc/src/external-resources.qdoc
@@ -80,3 +80,7 @@
\externalpage https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator
\title Nullish Coalescing
*/
+/*!
+ \externalpage https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining
+ \title Optional Chaining
+*/
diff --git a/src/qml/doc/src/javascript/hostenvironment.qdoc b/src/qml/doc/src/javascript/hostenvironment.qdoc
index bc75f843fb..0c495fdaa9 100644
--- a/src/qml/doc/src/javascript/hostenvironment.qdoc
+++ b/src/qml/doc/src/javascript/hostenvironment.qdoc
@@ -42,7 +42,7 @@ Like a browser or server-side JavaScript environment, the QML runtime implements
all of the built-in types and functions defined by the standard, such as Object, Array, and Math.
The QML runtime implements the 7th edition of the standard.
-Since Qt 5.15 \l{Nullish Coalescing} is also implemented in the QML runtime.
+\l{Nullish Coalescing} (since Qt 5.15) and \l{Optional Chaining} (since Qt 6.2) are also implemented in the QML runtime.
The standard ECMAScript built-ins are not explicitly documented in the QML documentation. For more
information on their use, please refer to the ECMA-262 7th edition standard or one of the many online
diff --git a/src/qml/jit/qv4baselineassembler.cpp b/src/qml/jit/qv4baselineassembler.cpp
index f0f20d4669..661da879f2 100644
--- a/src/qml/jit/qv4baselineassembler.cpp
+++ b/src/qml/jit/qv4baselineassembler.cpp
@@ -1415,6 +1415,23 @@ int BaselineAssembler::jumpNotUndefined(int offset)
return offset;
}
+int BaselineAssembler::jumpEqNull(int offset)
+{
+ saveAccumulatorInFrame();
+ cmpeqNull();
+
+ pasm()->toBoolean([this, offset](PlatformAssembler::RegisterID resultReg) {
+ auto isFalse = pasm()->branch32(PlatformAssembler::Equal, TrustedImm32(0), resultReg);
+ loadValue(Encode::undefined());
+ pasm()->addJumpToOffset(pasm()->jump(), offset);
+ isFalse.link(pasm());
+ loadAccumulatorFromFrame();
+ });
+
+ return offset;
+}
+
+
void BaselineAssembler::prepareCallWithArgCount(int argc)
{
pasm()->prepareCallWithArgCount(argc);
diff --git a/src/qml/jit/qv4baselineassembler_p.h b/src/qml/jit/qv4baselineassembler_p.h
index c6fdab51c5..e0a1bd3fce 100644
--- a/src/qml/jit/qv4baselineassembler_p.h
+++ b/src/qml/jit/qv4baselineassembler_p.h
@@ -137,6 +137,7 @@ public:
Q_REQUIRED_RESULT int jumpFalse(int offset);
Q_REQUIRED_RESULT int jumpNoException(int offset);
Q_REQUIRED_RESULT int jumpNotUndefined(int offset);
+ Q_REQUIRED_RESULT int jumpEqNull(int offset);
// stuff for runtime calls
void prepareCallWithArgCount(int argc);
diff --git a/src/qml/jit/qv4baselinejit.cpp b/src/qml/jit/qv4baselinejit.cpp
index 22cbfd1536..f660e363ea 100644
--- a/src/qml/jit/qv4baselinejit.cpp
+++ b/src/qml/jit/qv4baselinejit.cpp
@@ -283,6 +283,13 @@ void BaselineJIT::generate_LoadProperty(int name)
BASELINEJIT_GENERATE_RUNTIME_CALL(LoadProperty, CallResultDestination::InAccumulator);
}
+void BaselineJIT::generate_LoadOptionalProperty(int name, int offset)
+{
+ labels.insert(as->jumpEqNull(absoluteOffset(offset)));
+
+ generate_LoadProperty(name);
+}
+
void BaselineJIT::generate_GetLookup(int index)
{
STORE_IP();
@@ -295,6 +302,13 @@ void BaselineJIT::generate_GetLookup(int index)
BASELINEJIT_GENERATE_RUNTIME_CALL(GetLookup, CallResultDestination::InAccumulator);
}
+void BaselineJIT::generate_GetOptionalLookup(int index, int offset)
+{
+ labels.insert(as->jumpEqNull(absoluteOffset(offset)));
+
+ generate_GetLookup(index);
+}
+
void BaselineJIT::generate_StoreProperty(int name, int base)
{
STORE_IP();
diff --git a/src/qml/jit/qv4baselinejit_p.h b/src/qml/jit/qv4baselinejit_p.h
index 2b0913169e..1fa20d5902 100644
--- a/src/qml/jit/qv4baselinejit_p.h
+++ b/src/qml/jit/qv4baselinejit_p.h
@@ -102,7 +102,9 @@ public:
void generate_LoadElement(int base) override;
void generate_StoreElement(int base, int index) override;
void generate_LoadProperty(int name) override;
+ void generate_LoadOptionalProperty(int name, int offset) override;
void generate_GetLookup(int index) override;
+ void generate_GetOptionalLookup(int index, int offset) override;
void generate_StoreProperty(int name, int base) override;
void generate_SetLookup(int index, int base) override;
void generate_LoadSuperProperty(int property) override;
diff --git a/src/qml/jsruntime/qv4vme_moth.cpp b/src/qml/jsruntime/qv4vme_moth.cpp
index 9077529277..db353c1834 100644
--- a/src/qml/jsruntime/qv4vme_moth.cpp
+++ b/src/qml/jsruntime/qv4vme_moth.cpp
@@ -700,6 +700,18 @@ QV4::ReturnedValue VME::interpret(JSTypesStackFrame *frame, ExecutionEngine *eng
CHECK_EXCEPTION;
MOTH_END_INSTR(LoadProperty)
+ MOTH_BEGIN_INSTR(LoadOptionalProperty)
+ STORE_IP();
+ STORE_ACC();
+ if (accumulator.isNullOrUndefined()) {
+ acc = Encode::undefined();
+ code += offset;
+ } else {
+ acc = Runtime::LoadProperty::call(engine, accumulator, name);
+ }
+ CHECK_EXCEPTION;
+ MOTH_END_INSTR(LoadOptionalProperty)
+
MOTH_BEGIN_INSTR(GetLookup)
STORE_IP();
STORE_ACC();
@@ -718,6 +730,21 @@ QV4::ReturnedValue VME::interpret(JSTypesStackFrame *frame, ExecutionEngine *eng
CHECK_EXCEPTION;
MOTH_END_INSTR(GetLookup)
+ MOTH_BEGIN_INSTR(GetOptionalLookup)
+ STORE_IP();
+ STORE_ACC();
+
+ QV4::Lookup *l = function->executableCompilationUnit()->runtimeLookups + index;
+
+ if (accumulator.isNullOrUndefined()) {
+ acc = Encode::undefined();
+ code += offset;
+ } else {
+ acc = l->getter(l, engine, accumulator);
+ }
+ CHECK_EXCEPTION;
+ MOTH_END_INSTR(GetOptionalLookup)
+
MOTH_BEGIN_INSTR(StoreProperty)
STORE_IP();
STORE_ACC();
diff --git a/src/qml/parser/qqmljs.g b/src/qml/parser/qqmljs.g
index 3243dea2fe..2716983f42 100644
--- a/src/qml/parser/qqmljs.g
+++ b/src/qml/parser/qqmljs.g
@@ -81,6 +81,7 @@
%token T_COMPATIBILITY_SEMICOLON
%token T_ARROW "=>"
%token T_QUESTION_QUESTION "??"
+%token T_QUESTION_DOT "?."
%token T_ENUM "enum"
%token T_ELLIPSIS "..."
%token T_YIELD "yield"
@@ -2301,7 +2302,16 @@ MemberExpression: MemberExpression T_LBRACKET Expression_In T_RBRACKET;
sym(1).Node = node;
} break;
./
-
+MemberExpression: MemberExpression T_QUESTION_DOT T_LBRACKET Expression_In T_RBRACKET;
+/.
+ case $rule_number: {
+ AST::ArrayMemberExpression *node = new (pool) AST::ArrayMemberExpression(sym(1).Expression, sym(4).Expression);
+ node->lbracketToken = loc(3);
+ node->rbracketToken = loc(5);
+ node->isOptional = true;
+ sym(1).Node = node;
+ } break;
+./
-- the identifier has to be "target", catched at codegen time
NewTarget: T_NEW T_DOT T_IDENTIFIER;
@@ -2324,6 +2334,17 @@ MemberExpression: MemberExpression T_DOT IdentifierName;
} break;
./
+MemberExpression: MemberExpression T_QUESTION_DOT IdentifierName;
+/.
+ case $rule_number: {
+ AST::FieldMemberExpression *node = new (pool) AST::FieldMemberExpression(sym(1).Expression, stringRef(3));
+ node->dotToken = loc(2);
+ node->identifierToken = loc(3);
+ node->isOptional = true;
+ sym(1).Node = node;
+ } break;
+./
+
MemberExpression: MetaProperty;
MemberExpression: T_NEW MemberExpression T_LPAREN Arguments T_RPAREN;
@@ -2372,6 +2393,17 @@ CallExpression: MemberExpression T_LPAREN Arguments T_RPAREN;
} break;
./
+CallExpression: MemberExpression T_QUESTION_DOT T_LPAREN Arguments T_RPAREN;
+/.
+ case $rule_number: {
+ AST::CallExpression *node = new (pool) AST::CallExpression(sym(1).Expression, sym(4).ArgumentList);
+ node->lparenToken = loc(3);
+ node->rparenToken = loc(5);
+ node->isOptional = true;
+ sym(1).Node = node;
+ } break;
+./
+
CallExpression: Super T_LPAREN Arguments T_RPAREN;
/. case $rule_number: Q_FALLTHROUGH(); ./
CallExpression: CallExpression T_LPAREN Arguments T_RPAREN;
@@ -2384,6 +2416,18 @@ CallExpression: CallExpression T_LPAREN Arguments T_RPAREN;
} break;
./
+CallExpression: CallExpression T_QUESTION_DOT T_LPAREN Arguments T_RPAREN;
+/.
+ case $rule_number: {
+ AST::CallExpression *node = new (pool) AST::CallExpression(sym(1).Expression, sym(4).ArgumentList);
+ node->lparenToken = loc(3);
+ node->rparenToken = loc(5);
+ node->isOptional = true;
+ sym(1).Node = node;
+ } break;
+./
+
+
CallExpression: CallExpression T_LBRACKET Expression_In T_RBRACKET;
/.
case $rule_number: {
@@ -2394,6 +2438,17 @@ CallExpression: CallExpression T_LBRACKET Expression_In T_RBRACKET;
} break;
./
+CallExpression: CallExpression T_QUESTION_DOT T_LBRACKET Expression_In T_RBRACKET;
+/.
+ case $rule_number: {
+ AST::ArrayMemberExpression *node = new (pool) AST::ArrayMemberExpression(sym(1).Expression, sym(4).Expression);
+ node->lbracketToken = loc(3);
+ node->rbracketToken = loc(5);
+ node->isOptional = true;
+ sym(1).Node = node;
+ } break;
+./
+
CallExpression: CallExpression T_DOT IdentifierName;
/.
case $rule_number: {
@@ -2404,6 +2459,17 @@ CallExpression: CallExpression T_DOT IdentifierName;
} break;
./
+CallExpression: CallExpression T_QUESTION_DOT IdentifierName;
+/.
+ case $rule_number: {
+ AST::FieldMemberExpression *node = new (pool) AST::FieldMemberExpression(sym(1).Expression, stringRef(3));
+ node->dotToken = loc(2);
+ node->identifierToken = loc(3);
+ node->isOptional = true;
+ sym(1).Node = node;
+ } break;
+./
+
Arguments: ;
/.
case $rule_number: {
@@ -2897,6 +2963,9 @@ AssignmentExpression: LeftHandSideExpression T_EQ AssignmentExpression;
AssignmentExpression_In: LeftHandSideExpression T_EQ AssignmentExpression_In;
/.
case $rule_number: {
+ if (sym(1).Expression->containsOptionalChain()) {
+ syntaxError(loc(1), QStringLiteral("Optional chains are not permitted on the left-hand-side in assignments"));
+ }
// need to convert the LHS to an AssignmentPattern if it was an Array/ObjectLiteral
if (AST::Pattern *p = sym(1).Expression->patternCast()) {
SourceLocation errorLoc;
@@ -2927,6 +2996,9 @@ AssignmentExpression: LeftHandSideExpression AssignmentOperator AssignmentExpres
AssignmentExpression_In: LeftHandSideExpression AssignmentOperator AssignmentExpression_In;
/.
case $rule_number: {
+ if (sym(1).Expression->containsOptionalChain()) {
+ syntaxError(loc(1), QStringLiteral("Optional chains are not permitted on the left-hand-side in assignments"));
+ }
AST::BinaryExpression *node = new (pool) AST::BinaryExpression(sym(1).Expression, sym(2).ival, sym(3).Expression);
node->operatorToken = loc(2);
sym(1).Node = node;
diff --git a/src/qml/parser/qqmljsast.cpp b/src/qml/parser/qqmljsast.cpp
index 4b5d866662..a068a412df 100644
--- a/src/qml/parser/qqmljsast.cpp
+++ b/src/qml/parser/qqmljsast.cpp
@@ -118,6 +118,44 @@ ExpressionNode *ExpressionNode::expressionCast()
return this;
}
+bool ExpressionNode::containsOptionalChain() const
+{
+ for (const Node *node = this;;) {
+ switch (node->kind) {
+ case Kind_FieldMemberExpression: {
+ const auto *fme = AST::cast<const FieldMemberExpression*>(node);
+ if (fme->isOptional)
+ return true;
+ node = fme->base;
+ break;
+ }
+ case Kind_ArrayMemberExpression: {
+ const auto *ame = AST::cast<const ArrayMemberExpression*>(node);
+ if (ame->isOptional)
+ return true;
+ node = ame->base;
+ break;
+ }
+ case Kind_CallExpression: {
+ const auto *ce = AST::cast<const CallExpression*>(node);
+ if (ce->isOptional)
+ return true;
+ node = ce->base;
+ break;
+ }
+ case Kind_NestedExpression: {
+ const auto *ne = AST::cast<const NestedExpression*>(node);
+ node = ne->expression;
+ break;
+ }
+ default:
+ // These unhandled nodes lead to invalid lvalues anyway, so they do not need to be handled here.
+ return false;
+ }
+ }
+ return false;
+}
+
FormalParameterList *ExpressionNode::reparseAsFormalParameterList(MemoryPool *pool)
{
AST::ExpressionNode *expr = this;
diff --git a/src/qml/parser/qqmljsast_p.h b/src/qml/parser/qqmljsast_p.h
index 6836579209..d32595429e 100644
--- a/src/qml/parser/qqmljsast_p.h
+++ b/src/qml/parser/qqmljsast_p.h
@@ -460,6 +460,7 @@ public:
ExpressionNode() {}
ExpressionNode *expressionCast() override;
+ bool containsOptionalChain() const;
AST::FormalParameterList *reparseAsFormalParameterList(MemoryPool *pool);
@@ -1198,6 +1199,7 @@ public:
ExpressionNode *expression;
SourceLocation lbracketToken;
SourceLocation rbracketToken;
+ bool isOptional = false;
};
class QML_PARSER_EXPORT FieldMemberExpression: public LeftHandSideExpression
@@ -1222,6 +1224,7 @@ public:
QStringView name;
SourceLocation dotToken;
SourceLocation identifierToken;
+ bool isOptional = false;
};
class QML_PARSER_EXPORT TaggedTemplate : public LeftHandSideExpression
@@ -1314,6 +1317,7 @@ public:
ArgumentList *arguments;
SourceLocation lparenToken;
SourceLocation rparenToken;
+ bool isOptional = false;
};
class QML_PARSER_EXPORT ArgumentList: public Node
diff --git a/src/qml/parser/qqmljslexer.cpp b/src/qml/parser/qqmljslexer.cpp
index 29faa69587..1b58f687e2 100644
--- a/src/qml/parser/qqmljslexer.cpp
+++ b/src/qml/parser/qqmljslexer.cpp
@@ -591,6 +591,10 @@ again:
scanChar();
return T_QUESTION_QUESTION;
}
+ if (_char == u'.' && !peekChar().isDigit()) {
+ scanChar();
+ return T_QUESTION_DOT;
+ }
return T_QUESTION;
}
diff --git a/tests/auto/qml/ecmascripttests/test262 b/tests/auto/qml/ecmascripttests/test262
-Subproject 5a105134b9fdf9abc0dc12dc3e4402752963471
+Subproject 4dad98d63c279b989fdb48006fbd1db8ee27bc7
diff --git a/tests/auto/qml/qmlformat/tst_qmlformat.cpp b/tests/auto/qml/qmlformat/tst_qmlformat.cpp
index 165be7e973..a208b37fff 100644
--- a/tests/auto/qml/qmlformat/tst_qmlformat.cpp
+++ b/tests/auto/qml/qmlformat/tst_qmlformat.cpp
@@ -101,6 +101,7 @@ void TestQmlformat::initTestCase()
m_invalidFiles << "tests/auto/qml/qqmllanguage/data/invalidRoot.1.qml";
m_invalidFiles << "tests/auto/qml/qqmllanguage/data/invalidQmlEnumValue.1.qml";
m_invalidFiles << "tests/auto/qml/qqmllanguage/data/invalidQmlEnumValue.2.qml";
+ m_invalidFiles << "tests/auto/qml/qqmllanguage/data/questionDotEOF.qml";
m_invalidFiles << "tests/auto/qml/qquickfolderlistmodel/data/dummy.qml";
m_invalidFiles << "tests/auto/qml/qqmlecmascript/data/stringParsing_error.1.qml";
m_invalidFiles << "tests/auto/qml/qqmlecmascript/data/stringParsing_error.2.qml";
diff --git a/tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp b/tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp
index a518959b8a..d6776b6f1c 100644
--- a/tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp
+++ b/tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp
@@ -410,6 +410,9 @@ private slots:
void frozenQObject();
void constPointer();
+ void optionalChainEval();
+ void optionalChainDelete();
+ void optionalChainNull();
private:
// static void propertyVarWeakRefCallback(v8::Persistent<v8::Value> object, void* parameter);
static void verifyContextLifetime(const QQmlRefPointer<QQmlContextData> &ctxt);
@@ -9861,6 +9864,90 @@ void tst_qqmlecmascript::constPointer()
QVERIFY(root->property("propertyOk").toBool());
}
+void tst_qqmlecmascript::optionalChainEval()
+{
+ QQmlEngine qmlengine;
+
+ QObject *o = new QObject(&qmlengine);
+
+ QV4::ExecutionEngine *engine = qmlengine.handle();
+ QV4::Scope scope(engine);
+
+ QV4::ScopedValue object(scope, QV4::QObjectWrapper::wrap(engine, o));
+
+ EVALUATE("this.optional = { fnc: function(x) { this.counter++; return x; }, counter: 0};");
+ EVALUATE("this.slow = { counter: 0, thing: function(x) { this.counter++; return x; }};");
+
+ QVERIFY(EVALUATE_VALUE("eval('this.optional?.fnc?.(this.slow.thing([1,2,3]))?.[2]');", QV4::Primitive::fromInt32(3)));
+ QVERIFY(EVALUATE_VALUE("optional.counter", QV4::Primitive::fromInt32(1)));
+ QVERIFY(EVALUATE_VALUE("slow.counter", QV4::Primitive::fromInt32(1)));
+
+ EVALUATE("this.optional.fnc = undefined;");
+ QVERIFY(EVALUATE_VALUE("eval('this.optional?.fnc?.(this.slow.thing([1,2,3]))?.[2]');", QV4::Primitive::undefinedValue()));
+ QVERIFY(EVALUATE_VALUE("optional.counter", QV4::Primitive::fromInt32(1)));
+ QVERIFY(EVALUATE_VALUE("slow.counter", QV4::Primitive::fromInt32(1)));
+
+ EVALUATE("this.optional = undefined;");
+ QVERIFY(EVALUATE_VALUE("eval('this.optional?.fnc?.(this.slow.thing([1,2,3]))?.[2]');", QV4::Primitive::undefinedValue()));
+ QVERIFY(EVALUATE_VALUE("slow.counter", QV4::Primitive::fromInt32(1)));
+}
+
+void tst_qqmlecmascript::optionalChainDelete()
+{
+ QQmlEngine qmlengine;
+
+ QObject *o = new QObject(&qmlengine);
+
+ QV4::ExecutionEngine *engine = qmlengine.handle();
+ QV4::Scope scope(engine);
+
+ QV4::ScopedValue object(scope, QV4::QObjectWrapper::wrap(engine, o));
+ EVALUATE("this.object = { a: { b: {c: 6}}, x: Object.freeze({y: {z: 3}})};");
+ EVALUATE("this.slow = { counter: 0, thing: function(x) { this.counter++; return x; }};");
+
+ QVERIFY(EVALUATE_VALUE("'use strict'; delete object.a.b?.c", QV4::Primitive::fromBoolean(true)));
+ QVERIFY(EVALUATE_VALUE("'use strict'; delete object?.x?.y?.z", QV4::Primitive::fromBoolean(true)));
+ QVERIFY(EVALUATE_VALUE("'use strict'; delete object?.x?.y", QV4::Primitive::fromBoolean(false)));
+ QVERIFY(EVALUATE_VALUE("'use strict'; delete object?.x?.[slow.thing('y')]", QV4::Primitive::fromBoolean(false)));
+ QVERIFY(EVALUATE_VALUE("slow.counter", QV4::Primitive::fromInt32(1)));
+
+ QVERIFY(EVALUATE("this.object = {a: {}};"));
+ QVERIFY(EVALUATE("object.x = {};"));
+
+ QVERIFY(EVALUATE_VALUE("'use strict'; delete object.a.b?.c", QV4::Primitive::fromBoolean(true)));
+ QVERIFY(EVALUATE_VALUE("'use strict'; delete object?.x?.y?.z", QV4::Primitive::fromBoolean(true)));
+ QVERIFY(EVALUATE_VALUE("'use strict'; delete object?.x?.y", QV4::Primitive::fromBoolean(true)));
+ QVERIFY(EVALUATE_VALUE("'use strict'; delete object?.x?.[slow.thing('y')]", QV4::Primitive::fromBoolean(true)));
+ QVERIFY(EVALUATE_VALUE("slow.counter", QV4::Primitive::fromInt32(1)));
+}
+
+void tst_qqmlecmascript::optionalChainNull()
+{
+ QQmlEngine qmlengine;
+
+ QObject *o = new QObject(&qmlengine);
+
+ QV4::ExecutionEngine *engine = qmlengine.handle();
+ QV4::Scope scope(engine);
+
+ QV4::ScopedValue object(scope, QV4::QObjectWrapper::wrap(engine, o));
+ EVALUATE("this.object = { a: { b: {c: 6}}, x: {y: {z: 3}}};");
+ EVALUATE("this.slow = { counter: 0, thing: function(x) { this.counter++; return x; }};");
+
+ QVERIFY(EVALUATE_VALUE("this.object.a.b?.c", QV4::Primitive::fromInt32(6)));
+ QVERIFY(EVALUATE_VALUE("this.object?.x?.y?.z", QV4::Primitive::fromInt32(3)));
+ QVERIFY(EVALUATE_VALUE("this.object?.x?.[slow.thing('y')]?.z", QV4::Primitive::fromInt32(3)));
+ QVERIFY(EVALUATE_VALUE("this.slow.counter", QV4::Primitive::fromInt32(1)));
+
+ QVERIFY(EVALUATE("this.object.a.b = null;"));
+ QVERIFY(EVALUATE("this.object.x.y = null;"));
+
+ QVERIFY(EVALUATE_VALUE("this.object.a.b?.c", QV4::Primitive::undefinedValue()));
+ QVERIFY(EVALUATE_VALUE("this.object?.x?.y?.z", QV4::Primitive::undefinedValue()));
+ QVERIFY(EVALUATE_VALUE("this.object?.x?.y?.[slow.thing('z')]", QV4::Primitive::undefinedValue()));
+ QVERIFY(EVALUATE_VALUE("this.slow.counter", QV4::Primitive::fromInt32(1)));
+}
+
QTEST_MAIN(tst_qqmlecmascript)
#include "tst_qqmlecmascript.moc"
diff --git a/tests/auto/qml/qqmllanguage/data/optionalChaining.LHS.errors.txt b/tests/auto/qml/qqmllanguage/data/optionalChaining.LHS.errors.txt
new file mode 100644
index 0000000000..110d9f902a
--- /dev/null
+++ b/tests/auto/qml/qqmllanguage/data/optionalChaining.LHS.errors.txt
@@ -0,0 +1,48 @@
+5:9:Optional chains are not permitted on the left-hand-side in assignments
+6:2:Optional chains are not permitted on the left-hand-side in assignments
+7:2:Optional chains are not permitted on the left-hand-side in assignments
+8:2:Optional chains are not permitted on the left-hand-side in assignments
+9:9:Optional chains are not permitted on the left-hand-side in assignments
+10:9:Optional chains are not permitted on the left-hand-side in assignments
+11:2:Optional chains are not permitted on the left-hand-side in assignments
+12:2:Optional chains are not permitted on the left-hand-side in assignments
+13:2:Optional chains are not permitted on the left-hand-side in assignments
+14:2:Optional chains are not permitted on the left-hand-side in assignments
+15:2:Optional chains are not permitted on the left-hand-side in assignments
+16:2:Optional chains are not permitted on the left-hand-side in assignments
+18:9:Optional chains are not permitted on the left-hand-side in assignments
+19:9:Optional chains are not permitted on the left-hand-side in assignments
+20:9:Optional chains are not permitted on the left-hand-side in assignments
+21:9:Optional chains are not permitted on the left-hand-side in assignments
+22:9:Optional chains are not permitted on the left-hand-side in assignments
+23:9:Optional chains are not permitted on the left-hand-side in assignments
+24:9:Optional chains are not permitted on the left-hand-side in assignments
+25:9:Optional chains are not permitted on the left-hand-side in assignments
+26:9:Optional chains are not permitted on the left-hand-side in assignments
+27:9:Optional chains are not permitted on the left-hand-side in assignments
+28:9:Optional chains are not permitted on the left-hand-side in assignments
+29:9:Optional chains are not permitted on the left-hand-side in assignments
+31:9:Optional chains are not permitted on the left-hand-side in assignments
+32:9:Optional chains are not permitted on the left-hand-side in assignments
+33:9:Optional chains are not permitted on the left-hand-side in assignments
+34:9:Optional chains are not permitted on the left-hand-side in assignments
+35:9:Optional chains are not permitted on the left-hand-side in assignments
+36:9:Optional chains are not permitted on the left-hand-side in assignments
+37:9:Optional chains are not permitted on the left-hand-side in assignments
+38:9:Optional chains are not permitted on the left-hand-side in assignments
+39:9:Optional chains are not permitted on the left-hand-side in assignments
+40:9:Optional chains are not permitted on the left-hand-side in assignments
+41:9:Optional chains are not permitted on the left-hand-side in assignments
+42:9:Optional chains are not permitted on the left-hand-side in assignments
+44:9:Optional chains are not permitted on the left-hand-side in assignments
+45:9:Optional chains are not permitted on the left-hand-side in assignments
+46:9:Optional chains are not permitted on the left-hand-side in assignments
+47:9:Optional chains are not permitted on the left-hand-side in assignments
+48:9:Optional chains are not permitted on the left-hand-side in assignments
+49:9:Optional chains are not permitted on the left-hand-side in assignments
+50:9:Optional chains are not permitted on the left-hand-side in assignments
+51:9:Optional chains are not permitted on the left-hand-side in assignments
+52:9:Optional chains are not permitted on the left-hand-side in assignments
+53:9:Optional chains are not permitted on the left-hand-side in assignments
+54:9:Optional chains are not permitted on the left-hand-side in assignments
+55:9:Optional chains are not permitted on the left-hand-side in assignments
diff --git a/tests/auto/qml/qqmllanguage/data/optionalChaining.LHS.qml b/tests/auto/qml/qqmllanguage/data/optionalChaining.LHS.qml
new file mode 100644
index 0000000000..66643ceaca
--- /dev/null
+++ b/tests/auto/qml/qqmllanguage/data/optionalChaining.LHS.qml
@@ -0,0 +1,58 @@
+import QtQml
+
+Item {
+ Component.onCompleted: {
+ x.y?.z = 3;
+ x.y?.z += 3;
+ x.y?.z -= 3;
+ x.y?.z *= 3;
+ x.y?.z %= 3;
+ x.y?.z /= 3;
+ x.y?.z |= 3;
+ x.y?.z ^= 3;
+ x.y?.z &= 3;
+ x.y?.z <<= 3;
+ x.y?.z >>= 3;
+ x.y?.z >>>= 3;
+
+ (x.y?.z) = 3;
+ (x.y?.z) += 3;
+ (x.y?.z) -= 3;
+ (x.y?.z) *= 3;
+ (x.y?.z) %= 3;
+ (x.y?.z) /= 3;
+ (x.y?.z) |= 3;
+ (x.y?.z) ^= 3;
+ (x.y?.z) &= 3;
+ (x.y?.z) <<= 3;
+ (x.y?.z) >>= 3;
+ (x.y?.z) >>>= 3;
+
+ x.y?.().z = 3;
+ x.y?.().z += 3;
+ x.y?.().z -= 3;
+ x.y?.().z *= 3;
+ x.y?.().z %= 3;
+ x.y?.().z /= 3;
+ x.y?.().z |= 3;
+ x.y?.().z ^= 3;
+ x.y?.().z &= 3;
+ x.y?.().z <<= 3;
+ x.y?.().z >>= 3;
+ x.y?.().z >>>= 3;
+
+ x.y?.["z"] = 3;
+ x.y?.["z"] += 3;
+ x.y?.["z"] -= 3;
+ x.y?.["z"] *= 3;
+ x.y?.["z"] %= 3;
+ x.y?.["z"] /= 3;
+ x.y?.["z"] |= 3;
+ x.y?.["z"] ^= 3;
+ x.y?.["z"] &= 3;
+ x.y?.["z"] <<= 3;
+ x.y?.["z"] >>= 3;
+ x.y?.["z"] >>>= 3;
+ }
+}
+
diff --git a/tests/auto/qml/qqmllanguage/data/questionDotEOF.errors.txt b/tests/auto/qml/qqmllanguage/data/questionDotEOF.errors.txt
new file mode 100644
index 0000000000..3fc9ec8949
--- /dev/null
+++ b/tests/auto/qml/qqmllanguage/data/questionDotEOF.errors.txt
@@ -0,0 +1 @@
+3:19:Expected token `identifier'
diff --git a/tests/auto/qml/qqmllanguage/data/questionDotEOF.qml b/tests/auto/qml/qqmllanguage/data/questionDotEOF.qml
new file mode 100644
index 0000000000..8994442612
--- /dev/null
+++ b/tests/auto/qml/qqmllanguage/data/questionDotEOF.qml
@@ -0,0 +1,3 @@
+Component {
+ Component.onComplete {
+ undefined.? \ No newline at end of file
diff --git a/tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp b/tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp
index 0faf4c8677..868b3ad707 100644
--- a/tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp
+++ b/tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp
@@ -528,6 +528,10 @@ void tst_qqmllanguage::errors_data()
QTest::newRow("nullishCoalescing_RHS_Or") << "nullishCoalescing_RHS_Or.qml" << "nullishCoalescing_RHS_Or.errors.txt" << false;
QTest::newRow("nullishCoalescing_RHS_And") << "nullishCoalescing_RHS_And.qml" << "nullishCoalescing_RHS_And.errors.txt" << false;
+ QTest::newRow("questionDotEOF") << "questionDotEOF.qml" << "questionDotEOF.errors.txt" << false;
+ QTest::newRow("optionalChaining.LHS") << "optionalChaining.LHS.qml" << "optionalChaining.LHS.errors.txt" << false;
+
+
QTest::newRow("invalidGroupedProperty.1") << "invalidGroupedProperty.1.qml" << "invalidGroupedProperty.1.errors.txt" << false;
QTest::newRow("invalidGroupedProperty.2") << "invalidGroupedProperty.2.qml" << "invalidGroupedProperty.2.errors.txt" << false;
QTest::newRow("invalidGroupedProperty.3") << "invalidGroupedProperty.3.qml" << "invalidGroupedProperty.3.errors.txt" << false;
diff --git a/tools/qmlformat/dumpastvisitor.cpp b/tools/qmlformat/dumpastvisitor.cpp
index 04967ed0af..baf6651b20 100644
--- a/tools/qmlformat/dumpastvisitor.cpp
+++ b/tools/qmlformat/dumpastvisitor.cpp
@@ -405,14 +405,14 @@ QString DumpAstVisitor::parseExpression(ExpressionNode *expression)
if (fieldMemberExpr->base->kind == Node::Kind_NumericLiteral)
result = "(" + result + ")";
- result += "." + fieldMemberExpr->name.toString();
+ result += (fieldMemberExpr->isOptional ? "?." : ".") + fieldMemberExpr->name.toString();
return result;
}
case Node::Kind_ArrayMemberExpression: {
auto *arrayMemberExpr = cast<ArrayMemberExpression *>(expression);
return parseExpression(arrayMemberExpr->base)
- + "[" + parseExpression(arrayMemberExpr->expression) + "]";
+ + (arrayMemberExpr->isOptional ? "?." : "") + "[" + parseExpression(arrayMemberExpr->expression) + "]";
}
case Node::Kind_NestedExpression:
return "("+parseExpression(cast<NestedExpression *>(expression)->expression)+")";
@@ -458,7 +458,7 @@ QString DumpAstVisitor::parseExpression(ExpressionNode *expression)
case Node::Kind_CallExpression: {
auto *callExpr = cast<CallExpression *>(expression);
- return parseExpression(callExpr->base) + "(" + parseArgumentList(callExpr->arguments) + ")";
+ return parseExpression(callExpr->base) + (callExpr->isOptional ? "?." : "") + "(" + parseArgumentList(callExpr->arguments) + ")";
}
case Node::Kind_NewExpression:
return "new "+parseExpression(cast<NewExpression *>(expression)->expression);