diff options
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); |