// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qqmljavascriptexpression_p.h" #include "qqmljavascriptexpression_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE bool QQmlDelayedError::addError(QQmlEnginePrivate *e) { if (!e) return false; if (e->inProgressCreations == 0) return false; // Not in construction if (prevError) return true; // Already in error chain prevError = &e->erroredBindings; nextError = e->erroredBindings; e->erroredBindings = this; if (nextError) nextError->prevError = &nextError; return true; } void QQmlDelayedError::setErrorLocation(const QQmlSourceLocation &sourceLocation) { m_error.setUrl(QUrl(sourceLocation.sourceFile)); m_error.setLine(qmlConvertSourceCoordinate(sourceLocation.line)); m_error.setColumn(qmlConvertSourceCoordinate(sourceLocation.column)); } void QQmlDelayedError::setErrorDescription(const QString &description) { m_error.setDescription(description); } void QQmlDelayedError::setErrorObject(QObject *object) { m_error.setObject(object); } void QQmlDelayedError::catchJavaScriptException(QV4::ExecutionEngine *engine) { m_error = engine->catchExceptionAsQmlError(); } QQmlJavaScriptExpression::QQmlJavaScriptExpression() : m_context(nullptr), m_prevExpression(nullptr), m_nextExpression(nullptr), m_v4Function(nullptr) { } QQmlJavaScriptExpression::~QQmlJavaScriptExpression() { if (m_prevExpression) { *m_prevExpression = m_nextExpression; if (m_nextExpression) m_nextExpression->m_prevExpression = m_prevExpression; } while (qpropertyChangeTriggers) { auto current = qpropertyChangeTriggers; qpropertyChangeTriggers = current->next; QRecyclePool::Delete(current); } clearActiveGuards(); clearError(); if (m_scopeObject.isT2()) // notify DeleteWatcher of our deletion. m_scopeObject.asT2()->_s = nullptr; } QString QQmlJavaScriptExpression::expressionIdentifier() const { if (auto f = function()) { QString url = f->sourceFile(); uint lineNumber = f->compiledFunction->location.line(); uint columnNumber = f->compiledFunction->location.column(); return url + QString::asprintf(":%u:%u", lineNumber, columnNumber); } return QStringLiteral("[native code]"); } void QQmlJavaScriptExpression::setNotifyOnValueChanged(bool v) { activeGuards.setTag(v ? NotifyOnValueChanged : NoGuardTag); if (!v) clearActiveGuards(); } void QQmlJavaScriptExpression::resetNotifyOnValueChanged() { setNotifyOnValueChanged(false); } QQmlSourceLocation QQmlJavaScriptExpression::sourceLocation() const { if (m_v4Function) return m_v4Function->sourceLocation(); return QQmlSourceLocation(); } void QQmlJavaScriptExpression::setContext(const QQmlRefPointer &context) { if (m_prevExpression) { *m_prevExpression = m_nextExpression; if (m_nextExpression) m_nextExpression->m_prevExpression = m_prevExpression; m_prevExpression = nullptr; m_nextExpression = nullptr; } m_context = context.data(); if (context) context->addExpression(this); } void QQmlJavaScriptExpression::refresh() { } QV4::ReturnedValue QQmlJavaScriptExpression::evaluate(bool *isUndefined) { QQmlEngine *qmlengine = engine(); if (!qmlengine) { if (isUndefined) *isUndefined = true; return QV4::Encode::undefined(); } QV4::Scope scope(qmlengine->handle()); QV4::JSCallArguments jsCall(scope); return evaluate(jsCall.callData(scope), isUndefined); } class QQmlJavaScriptExpressionCapture { Q_DISABLE_COPY_MOVE(QQmlJavaScriptExpressionCapture) public: QQmlJavaScriptExpressionCapture(QQmlJavaScriptExpression *expression, QQmlEngine *engine) : watcher(expression) , capture(engine, expression, &watcher) , ep(QQmlEnginePrivate::get(engine)) { Q_ASSERT(expression->notifyOnValueChanged() || expression->activeGuards.isEmpty()); lastPropertyCapture = ep->propertyCapture; ep->propertyCapture = expression->notifyOnValueChanged() ? &capture : nullptr; if (expression->notifyOnValueChanged()) capture.guards.copyAndClearPrepend(expression->activeGuards); } ~QQmlJavaScriptExpressionCapture() { if (capture.errorString) { for (int ii = 0; ii < capture.errorString->size(); ++ii) qWarning("%s", qPrintable(capture.errorString->at(ii))); delete capture.errorString; capture.errorString = nullptr; } while (QQmlJavaScriptExpressionGuard *g = capture.guards.takeFirst()) g->Delete(); ep->propertyCapture = lastPropertyCapture; } bool catchException(const QV4::Scope &scope) const { if (scope.hasException()) { if (watcher.wasDeleted()) scope.engine->catchException(); // ignore exception else capture.expression->delayedError()->catchJavaScriptException(scope.engine); return true; } if (!watcher.wasDeleted() && capture.expression->hasDelayedError()) capture.expression->delayedError()->clearError(); return false; } private: QQmlJavaScriptExpression::DeleteWatcher watcher; QQmlPropertyCapture capture; QQmlEnginePrivate *ep; QQmlPropertyCapture *lastPropertyCapture; }; QV4::ReturnedValue QQmlJavaScriptExpression::evaluate(QV4::CallData *callData, bool *isUndefined) { QQmlEngine *qmlEngine = engine(); QV4::Function *v4Function = function(); if (!v4Function || !qmlEngine) { if (isUndefined) *isUndefined = true; return QV4::Encode::undefined(); } // All code that follows must check with watcher before it accesses data members // incase we have been deleted. QQmlJavaScriptExpressionCapture capture(this, qmlEngine); QV4::Scope scope(qmlEngine->handle()); if (QObject *thisObject = scopeObject()) { callData->thisObject = QV4::QObjectWrapper::wrap(scope.engine, thisObject); if (callData->thisObject.isNullOrUndefined()) callData->thisObject = scope.engine->globalObject; } else { callData->thisObject = scope.engine->globalObject; } Q_ASSERT(m_qmlScope.valueRef()); QV4::ScopedValue result(scope, v4Function->call( &(callData->thisObject.asValue()), callData->argValues(), callData->argc(), static_cast(m_qmlScope.valueRef()))); if (capture.catchException(scope)) { if (isUndefined) *isUndefined = true; } else if (isUndefined) { *isUndefined = result->isUndefined(); } return result->asReturnedValue(); } bool QQmlJavaScriptExpression::evaluate(void **a, const QMetaType *types, int argc) { // All code that follows must check with watcher before it accesses data members // incase we have been deleted. QQmlEngine *qmlEngine = engine(); // If there is no engine, we have no way to evaluate anything. // This can happen on destruction. if (!qmlEngine) return false; QQmlJavaScriptExpressionCapture capture(this, qmlEngine); QV4::Scope scope(qmlEngine->handle()); Q_ASSERT(m_qmlScope.valueRef()); Q_ASSERT(function()); const bool resultIsDefined = function()->call( scopeObject(), a, types, argc, static_cast(m_qmlScope.valueRef())); return !capture.catchException(scope) && resultIsDefined; } void QQmlPropertyCapture::captureProperty(QQmlNotifier *n) { if (watcher->wasDeleted()) return; Q_ASSERT(expression); // Try and find a matching guard while (!guards.isEmpty() && !guards.first()->isConnected(n)) guards.takeFirst()->Delete(); QQmlJavaScriptExpressionGuard *g = nullptr; if (!guards.isEmpty()) { g = guards.takeFirst(); g->cancelNotify(); Q_ASSERT(g->isConnected(n)); } else { g = QQmlJavaScriptExpressionGuard::New(expression, engine); g->connect(n); } expression->activeGuards.prepend(g); } /*! \internal \a n is in the signal index range (see QObjectPrivate::signalIndex()). */ void QQmlPropertyCapture::captureProperty(QObject *o, int c, int n, bool doNotify) { if (watcher->wasDeleted()) return; Q_ASSERT(expression); // If c < 0 we won't find any property. We better leave the metaobjects alone in that case. // QQmlListModel expects us _not_ to trigger the creation of dynamic metaobjects from here. if (c >= 0) { const QQmlData *ddata = QQmlData::get(o, /*create=*/false); const QMetaObject *metaObjectForBindable = nullptr; if (auto const propCache = (ddata ? ddata->propertyCache.data() : nullptr)) { Q_ASSERT(propCache->property(c)); if (propCache->property(c)->isBindable()) metaObjectForBindable = propCache->metaObject(); } else { const QMetaObject *m = o->metaObject(); if (m->property(c).isBindable()) metaObjectForBindable = m; } if (metaObjectForBindable) { captureBindableProperty(o, metaObjectForBindable, c); return; } } captureNonBindableProperty(o, n, c, doNotify); } void QQmlPropertyCapture::captureProperty( QObject *o, const QQmlPropertyCache *propertyCache, const QQmlPropertyData *propertyData, bool doNotify) { if (watcher->wasDeleted()) return; Q_ASSERT(expression); if (propertyData->isBindable()) { if (const QMetaObject *metaObjectForBindable = propertyCache->metaObject()) { captureBindableProperty(o, metaObjectForBindable, propertyData->coreIndex()); return; } } captureNonBindableProperty(o, propertyData->notifyIndex(), propertyData->coreIndex(), doNotify); } bool QQmlJavaScriptExpression::needsPropertyChangeTrigger(QObject *target, int propertyIndex) { TriggerList **prev = &qpropertyChangeTriggers; TriggerList *current = qpropertyChangeTriggers; while (current) { if (!current->target) { *prev = current->next; QRecyclePool::Delete(current); current = *prev; } else if (current->target == target && current->propertyIndex == propertyIndex) { return false; // already installed } else { prev = ¤t->next; current = current->next; } } return true; } void QQmlPropertyCapture::captureTranslation() { // use a unique invalid index to avoid needlessly querying the metaobject for // the correct index of of the translationLanguage property int const invalidIndex = -2; if (expression->needsPropertyChangeTrigger(engine, invalidIndex)) { auto trigger = expression->allocatePropertyChangeTrigger(engine, invalidIndex); trigger->setSource(QQmlEnginePrivate::get(engine)->translationLanguage); } } void QQmlPropertyCapture::captureBindableProperty( QObject *o, const QMetaObject *metaObjectForBindable, int c) { // if the property is a QPropery, and we're binding to a QProperty // the automatic capturing process already takes care of everything if (!expression->mustCaptureBindableProperty()) return; if (expression->needsPropertyChangeTrigger(o, c)) { auto trigger = expression->allocatePropertyChangeTrigger(o, c); QUntypedBindable bindable; void *argv[] = { &bindable }; metaObjectForBindable->metacall(o, QMetaObject::BindableProperty, c, argv); bindable.observe(trigger); } } void QQmlPropertyCapture::captureNonBindableProperty(QObject *o, int n, int c, bool doNotify) { if (n == -1) { if (!errorString) { errorString = new QStringList; QString preamble = QLatin1String("QQmlExpression: Expression ") + expression->expressionIdentifier() + QLatin1String(" depends on non-NOTIFYable properties:"); errorString->append(preamble); } const QMetaProperty metaProp = o->metaObject()->property(c); QString error = QLatin1String(" ") + QString::fromUtf8(o->metaObject()->className()) + QLatin1String("::") + QString::fromUtf8(metaProp.name()); errorString->append(error); } else { // Try and find a matching guard while (!guards.isEmpty() && !guards.first()->isConnected(o, n)) guards.takeFirst()->Delete(); QQmlJavaScriptExpressionGuard *g = nullptr; if (!guards.isEmpty()) { g = guards.takeFirst(); g->cancelNotify(); Q_ASSERT(g->isConnected(o, n)); } else { g = QQmlJavaScriptExpressionGuard::New(expression, engine); g->connect(o, n, engine, doNotify); } expression->activeGuards.prepend(g); } } QQmlError QQmlJavaScriptExpression::error(QQmlEngine *engine) const { Q_UNUSED(engine); if (m_error) return m_error->error(); else return QQmlError(); } QQmlDelayedError *QQmlJavaScriptExpression::delayedError() { if (!m_error) m_error = new QQmlDelayedError; return m_error.data(); } QV4::ReturnedValue QQmlJavaScriptExpression::evalFunction( const QQmlRefPointer &ctxt, QObject *scopeObject, const QString &code, const QString &filename, quint16 line) { QQmlEngine *engine = ctxt->engine(); QQmlEnginePrivate *ep = QQmlEnginePrivate::get(engine); QV4::ExecutionEngine *v4 = engine->handle(); QV4::Scope scope(v4); QV4::Scoped qmlContext(scope, QV4::QmlContext::create(v4->rootContext(), ctxt, scopeObject)); QV4::Script script(v4, qmlContext, /*parse as QML binding*/true, code, filename, line); QV4::ScopedValue result(scope); script.parse(); if (!v4->hasException) result = script.run(); if (v4->hasException) { QQmlError error = v4->catchExceptionAsQmlError(); if (error.description().isEmpty()) error.setDescription(QLatin1String("Exception occurred during function evaluation")); if (error.line() == -1) error.setLine(line); if (error.url().isEmpty()) error.setUrl(QUrl::fromLocalFile(filename)); error.setObject(scopeObject); ep->warning(error); return QV4::Encode::undefined(); } return result->asReturnedValue(); } void QQmlJavaScriptExpression::createQmlBinding( const QQmlRefPointer &ctxt, QObject *qmlScope, const QString &code, const QString &filename, quint16 line) { QQmlEngine *engine = ctxt->engine(); QQmlEnginePrivate *ep = QQmlEnginePrivate::get(engine); QV4::ExecutionEngine *v4 = engine->handle(); QV4::Scope scope(v4); QV4::Scoped qmlContext(scope, QV4::QmlContext::create(v4->rootContext(), ctxt, qmlScope)); QV4::Script script(v4, qmlContext, /*parse as QML binding*/true, code, filename, line); script.parse(); if (v4->hasException) { QQmlDelayedError *error = delayedError(); error->catchJavaScriptException(v4); error->setErrorObject(qmlScope); if (!error->addError(ep)) ep->warning(error->error()); return; } setupFunction(qmlContext, script.vmFunction); } void QQmlJavaScriptExpression::setupFunction(QV4::ExecutionContext *qmlContext, QV4::Function *f) { if (!qmlContext || !f) return; m_qmlScope.set(qmlContext->engine(), *qmlContext); m_v4Function = f; m_compilationUnit.reset(m_v4Function->executableCompilationUnit()); } void QQmlJavaScriptExpression::setCompilationUnit(const QQmlRefPointer &compilationUnit) { m_compilationUnit = compilationUnit; } void QPropertyChangeTrigger::trigger(QPropertyObserver *observer, QUntypedPropertyData *) { auto This = static_cast(observer); This->m_expression->expressionChanged(); } QMetaProperty QPropertyChangeTrigger::property() const { if (!target) return {}; auto const mo = target->metaObject(); if (!mo) return {}; return mo->property(propertyIndex); } QPropertyChangeTrigger *QQmlJavaScriptExpression::allocatePropertyChangeTrigger(QObject *target, int propertyIndex) { auto trigger = QQmlEnginePrivate::get(engine())->qPropertyTriggerPool.New( this ); trigger->target = target; trigger->propertyIndex = propertyIndex; auto oldHead = qpropertyChangeTriggers; trigger->next = oldHead; qpropertyChangeTriggers = trigger; return trigger; } void QQmlJavaScriptExpression::clearActiveGuards() { while (QQmlJavaScriptExpressionGuard *g = activeGuards.takeFirst()) g->Delete(); } void QQmlJavaScriptExpressionGuard_callback(QQmlNotifierEndpoint *e, void **) { QQmlJavaScriptExpression *expression = static_cast(e)->expression; expression->expressionChanged(); } QT_END_NAMESPACE