1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qqmljavascriptexpression_p.h"
5#include "qqmljavascriptexpression_p.h"
6
7#include <private/qqmlexpression_p.h>
8#include <private/qv4context_p.h>
9#include <private/qv4value_p.h>
10#include <private/qv4functionobject_p.h>
11#include <private/qv4script_p.h>
12#include <private/qv4errorobject_p.h>
13#include <private/qv4scopedvalue_p.h>
14#include <private/qv4jscall_p.h>
15#include <private/qqmlglobal_p.h>
16#include <private/qv4qobjectwrapper_p.h>
17#include <private/qqmlbuiltinfunctions_p.h>
18#include <private/qqmlsourcecoordinate_p.h>
19#include <private/qqmlabstractbinding_p.h>
20#include <private/qqmlpropertybinding_p.h>
21#include <private/qproperty_p.h>
22
23QT_BEGIN_NAMESPACE
24
25bool QQmlDelayedError::addError(QQmlEnginePrivate *e)
26{
27 if (!e) return false;
28
29 if (e->inProgressCreations == 0) return false; // Not in construction
30
31 if (prevError) return true; // Already in error chain
32
33 prevError = &e->erroredBindings;
34 nextError = e->erroredBindings;
35 e->erroredBindings = this;
36 if (nextError) nextError->prevError = &nextError;
37
38 return true;
39}
40
41void QQmlDelayedError::setErrorLocation(const QQmlSourceLocation &sourceLocation)
42{
43 m_error.setUrl(QUrl(sourceLocation.sourceFile));
44 m_error.setLine(qmlConvertSourceCoordinate<quint16, int>(n: sourceLocation.line));
45 m_error.setColumn(qmlConvertSourceCoordinate<quint16, int>(n: sourceLocation.column));
46}
47
48void QQmlDelayedError::setErrorDescription(const QString &description)
49{
50 m_error.setDescription(description);
51}
52
53void QQmlDelayedError::setErrorObject(QObject *object)
54{
55 m_error.setObject(object);
56}
57
58void QQmlDelayedError::catchJavaScriptException(QV4::ExecutionEngine *engine)
59{
60 m_error = engine->catchExceptionAsQmlError();
61}
62
63
64QQmlJavaScriptExpression::QQmlJavaScriptExpression()
65 : m_context(nullptr),
66 m_prevExpression(nullptr),
67 m_nextExpression(nullptr),
68 m_v4Function(nullptr)
69{
70}
71
72QQmlJavaScriptExpression::~QQmlJavaScriptExpression()
73{
74 if (m_prevExpression) {
75 *m_prevExpression = m_nextExpression;
76 if (m_nextExpression)
77 m_nextExpression->m_prevExpression = m_prevExpression;
78 }
79
80 while (qpropertyChangeTriggers) {
81 auto current = qpropertyChangeTriggers;
82 qpropertyChangeTriggers = current->next;
83 QRecyclePool<TriggerList>::Delete(t: current);
84 }
85
86 clearActiveGuards();
87 clearError();
88 if (m_scopeObject.isT2()) // notify DeleteWatcher of our deletion.
89 m_scopeObject.asT2()->_s = nullptr;
90}
91
92QString QQmlJavaScriptExpression::expressionIdentifier() const
93{
94 if (auto f = function()) {
95 QString url = f->sourceFile();
96 uint lineNumber = f->compiledFunction->location.line();
97 uint columnNumber = f->compiledFunction->location.column();
98 return url + QString::asprintf(format: ":%u:%u", lineNumber, columnNumber);
99 }
100
101 return QStringLiteral("[native code]");
102}
103
104void QQmlJavaScriptExpression::setNotifyOnValueChanged(bool v)
105{
106 activeGuards.setTag(v ? NotifyOnValueChanged : NoGuardTag);
107 if (!v)
108 clearActiveGuards();
109}
110
111void QQmlJavaScriptExpression::resetNotifyOnValueChanged()
112{
113 setNotifyOnValueChanged(false);
114}
115
116QQmlSourceLocation QQmlJavaScriptExpression::sourceLocation() const
117{
118 if (m_v4Function)
119 return m_v4Function->sourceLocation();
120 return QQmlSourceLocation();
121}
122
123void QQmlJavaScriptExpression::setContext(const QQmlRefPointer<QQmlContextData> &context)
124{
125 if (m_prevExpression) {
126 *m_prevExpression = m_nextExpression;
127 if (m_nextExpression)
128 m_nextExpression->m_prevExpression = m_prevExpression;
129 m_prevExpression = nullptr;
130 m_nextExpression = nullptr;
131 }
132
133 m_context = context.data();
134
135 if (context)
136 context->addExpression(expression: this);
137}
138
139void QQmlJavaScriptExpression::refresh()
140{
141}
142
143QV4::ReturnedValue QQmlJavaScriptExpression::evaluate(bool *isUndefined)
144{
145 QQmlEngine *qmlengine = engine();
146 if (!qmlengine) {
147 if (isUndefined)
148 *isUndefined = true;
149 return QV4::Encode::undefined();
150 }
151
152 QV4::Scope scope(qmlengine->handle());
153 QV4::JSCallArguments jsCall(scope);
154
155 return evaluate(callData: jsCall.callData(scope), isUndefined);
156}
157
158class QQmlJavaScriptExpressionCapture
159{
160 Q_DISABLE_COPY_MOVE(QQmlJavaScriptExpressionCapture)
161public:
162 QQmlJavaScriptExpressionCapture(QQmlJavaScriptExpression *expression, QQmlEngine *engine)
163 : watcher(expression)
164 , capture(engine, expression, &watcher)
165 , ep(QQmlEnginePrivate::get(e: engine))
166 {
167 Q_ASSERT(expression->notifyOnValueChanged() || expression->activeGuards.isEmpty());
168
169 lastPropertyCapture = ep->propertyCapture;
170 ep->propertyCapture = expression->notifyOnValueChanged() ? &capture : nullptr;
171
172 if (expression->notifyOnValueChanged())
173 capture.guards.copyAndClearPrepend(o&: expression->activeGuards);
174 }
175
176 ~QQmlJavaScriptExpressionCapture()
177 {
178 if (capture.errorString) {
179 for (int ii = 0; ii < capture.errorString->size(); ++ii)
180 qWarning(msg: "%s", qPrintable(capture.errorString->at(ii)));
181 delete capture.errorString;
182 capture.errorString = nullptr;
183 }
184
185 while (QQmlJavaScriptExpressionGuard *g = capture.guards.takeFirst())
186 g->Delete();
187
188 ep->propertyCapture = lastPropertyCapture;
189 }
190
191 bool catchException(const QV4::Scope &scope) const
192 {
193 if (scope.hasException()) {
194 if (watcher.wasDeleted())
195 scope.engine->catchException(); // ignore exception
196 else
197 capture.expression->delayedError()->catchJavaScriptException(engine: scope.engine);
198 return true;
199 }
200
201 if (!watcher.wasDeleted() && capture.expression->hasDelayedError())
202 capture.expression->delayedError()->clearError();
203 return false;
204 }
205
206private:
207 QQmlJavaScriptExpression::DeleteWatcher watcher;
208 QQmlPropertyCapture capture;
209 QQmlEnginePrivate *ep;
210 QQmlPropertyCapture *lastPropertyCapture;
211};
212
213QV4::ReturnedValue QQmlJavaScriptExpression::evaluate(QV4::CallData *callData, bool *isUndefined)
214{
215 QQmlEngine *qmlEngine = engine();
216 QV4::Function *v4Function = function();
217 if (!v4Function || !qmlEngine) {
218 if (isUndefined)
219 *isUndefined = true;
220 return QV4::Encode::undefined();
221 }
222
223 // All code that follows must check with watcher before it accesses data members
224 // incase we have been deleted.
225 QQmlJavaScriptExpressionCapture capture(this, qmlEngine);
226
227 QV4::Scope scope(qmlEngine->handle());
228
229 if (QObject *thisObject = scopeObject()) {
230 callData->thisObject = QV4::QObjectWrapper::wrap(engine: scope.engine, object: thisObject);
231 if (callData->thisObject.isNullOrUndefined())
232 callData->thisObject = scope.engine->globalObject;
233 } else {
234 callData->thisObject = scope.engine->globalObject;
235 }
236
237 Q_ASSERT(m_qmlScope.valueRef());
238 QV4::ScopedValue result(scope, v4Function->call(
239 thisObject: &(callData->thisObject.asValue<QV4::Value>()),
240 argv: callData->argValues<QV4::Value>(), argc: callData->argc(),
241 context: static_cast<QV4::ExecutionContext *>(m_qmlScope.valueRef())));
242
243 if (capture.catchException(scope)) {
244 if (isUndefined)
245 *isUndefined = true;
246 } else if (isUndefined) {
247 *isUndefined = result->isUndefined();
248 }
249
250 return result->asReturnedValue();
251}
252
253bool QQmlJavaScriptExpression::evaluate(void **a, const QMetaType *types, int argc)
254{
255 // All code that follows must check with watcher before it accesses data members
256 // incase we have been deleted.
257 QQmlEngine *qmlEngine = engine();
258
259 // If there is no engine, we have no way to evaluate anything.
260 // This can happen on destruction.
261 if (!qmlEngine)
262 return false;
263
264 QQmlJavaScriptExpressionCapture capture(this, qmlEngine);
265
266 QV4::Scope scope(qmlEngine->handle());
267
268 Q_ASSERT(m_qmlScope.valueRef());
269 Q_ASSERT(function());
270 const bool resultIsDefined = function()->call(
271 thisObject: scopeObject(), a, types, argc,
272 context: static_cast<QV4::ExecutionContext *>(m_qmlScope.valueRef()));
273
274 return !capture.catchException(scope) && resultIsDefined;
275}
276
277void QQmlPropertyCapture::captureProperty(QQmlNotifier *n)
278{
279 if (watcher->wasDeleted())
280 return;
281
282 Q_ASSERT(expression);
283 // Try and find a matching guard
284 while (!guards.isEmpty() && !guards.first()->isConnected(notifier: n))
285 guards.takeFirst()->Delete();
286
287 QQmlJavaScriptExpressionGuard *g = nullptr;
288 if (!guards.isEmpty()) {
289 g = guards.takeFirst();
290 g->cancelNotify();
291 Q_ASSERT(g->isConnected(n));
292 } else {
293 g = QQmlJavaScriptExpressionGuard::New(e: expression, engine);
294 g->connect(notifier: n);
295 }
296
297 expression->activeGuards.prepend(v: g);
298}
299
300/*! \internal
301
302 \a n is in the signal index range (see QObjectPrivate::signalIndex()).
303*/
304void QQmlPropertyCapture::captureProperty(QObject *o, int c, int n, bool doNotify)
305{
306 if (watcher->wasDeleted())
307 return;
308
309 Q_ASSERT(expression);
310
311 // If c < 0 we won't find any property. We better leave the metaobjects alone in that case.
312 // QQmlListModel expects us _not_ to trigger the creation of dynamic metaobjects from here.
313 if (c >= 0) {
314 const QQmlData *ddata = QQmlData::get(object: o, /*create=*/false);
315 const QMetaObject *metaObjectForBindable = nullptr;
316 if (auto const propCache = (ddata ? ddata->propertyCache.data() : nullptr)) {
317 Q_ASSERT(propCache->property(c));
318 if (propCache->property(index: c)->isBindable())
319 metaObjectForBindable = propCache->metaObject();
320 } else {
321 const QMetaObject *m = o->metaObject();
322 if (m->property(index: c).isBindable())
323 metaObjectForBindable = m;
324 }
325 if (metaObjectForBindable) {
326 captureBindableProperty(o, metaObjectForBindable, c);
327 return;
328 }
329 }
330
331 captureNonBindableProperty(o, n, c, doNotify);
332}
333
334void QQmlPropertyCapture::captureProperty(
335 QObject *o, const QQmlPropertyCache *propertyCache, const QQmlPropertyData *propertyData,
336 bool doNotify)
337{
338 if (watcher->wasDeleted())
339 return;
340
341 Q_ASSERT(expression);
342
343 if (propertyData->isBindable()) {
344 if (const QMetaObject *metaObjectForBindable = propertyCache->metaObject()) {
345 captureBindableProperty(o, metaObjectForBindable, c: propertyData->coreIndex());
346 return;
347 }
348 }
349
350 captureNonBindableProperty(o, n: propertyData->notifyIndex(), c: propertyData->coreIndex(), doNotify);
351}
352
353bool QQmlJavaScriptExpression::needsPropertyChangeTrigger(QObject *target, int propertyIndex)
354{
355 TriggerList **prev = &qpropertyChangeTriggers;
356 TriggerList *current = qpropertyChangeTriggers;
357 while (current) {
358 if (!current->target) {
359 *prev = current->next;
360 QRecyclePool<TriggerList>::Delete(t: current);
361 current = *prev;
362 } else if (current->target == target && current->propertyIndex == propertyIndex) {
363 return false; // already installed
364 } else {
365 prev = &current->next;
366 current = current->next;
367 }
368 }
369
370 return true;
371}
372
373void QQmlPropertyCapture::captureTranslation()
374{
375 // use a unique invalid index to avoid needlessly querying the metaobject for
376 // the correct index of of the translationLanguage property
377 int const invalidIndex = -2;
378 if (expression->needsPropertyChangeTrigger(target: engine, propertyIndex: invalidIndex)) {
379 auto trigger = expression->allocatePropertyChangeTrigger(target: engine, propertyIndex: invalidIndex);
380 trigger->setSource(QQmlEnginePrivate::get(e: engine)->translationLanguage);
381 }
382}
383
384void QQmlPropertyCapture::captureBindableProperty(
385 QObject *o, const QMetaObject *metaObjectForBindable, int c)
386{
387 // if the property is a QPropery, and we're binding to a QProperty
388 // the automatic capturing process already takes care of everything
389 if (!expression->mustCaptureBindableProperty())
390 return;
391
392 if (expression->needsPropertyChangeTrigger(target: o, propertyIndex: c)) {
393 auto trigger = expression->allocatePropertyChangeTrigger(target: o, propertyIndex: c);
394 QUntypedBindable bindable;
395 void *argv[] = { &bindable };
396 metaObjectForBindable->metacall(o, QMetaObject::BindableProperty, c, argv);
397 bindable.observe(observer: trigger);
398 }
399}
400
401void QQmlPropertyCapture::captureNonBindableProperty(QObject *o, int n, int c, bool doNotify)
402{
403 if (n == -1) {
404 if (!errorString) {
405 errorString = new QStringList;
406 QString preamble = QLatin1String("QQmlExpression: Expression ") +
407 expression->expressionIdentifier() +
408 QLatin1String(" depends on non-NOTIFYable properties:");
409 errorString->append(t: preamble);
410 }
411
412 const QMetaProperty metaProp = o->metaObject()->property(index: c);
413 QString error = QLatin1String(" ") +
414 QString::fromUtf8(utf8: o->metaObject()->className()) +
415 QLatin1String("::") +
416 QString::fromUtf8(utf8: metaProp.name());
417 errorString->append(t: error);
418 } else {
419
420 // Try and find a matching guard
421 while (!guards.isEmpty() && !guards.first()->isConnected(source: o, sourceSignal: n))
422 guards.takeFirst()->Delete();
423
424 QQmlJavaScriptExpressionGuard *g = nullptr;
425 if (!guards.isEmpty()) {
426 g = guards.takeFirst();
427 g->cancelNotify();
428 Q_ASSERT(g->isConnected(o, n));
429 } else {
430 g = QQmlJavaScriptExpressionGuard::New(e: expression, engine);
431 g->connect(source: o, sourceSignal: n, engine, doNotify);
432 }
433
434 expression->activeGuards.prepend(v: g);
435 }
436}
437
438QQmlError QQmlJavaScriptExpression::error(QQmlEngine *engine) const
439{
440 Q_UNUSED(engine);
441
442 if (m_error)
443 return m_error->error();
444 else
445 return QQmlError();
446}
447
448QQmlDelayedError *QQmlJavaScriptExpression::delayedError()
449{
450 if (!m_error)
451 m_error = new QQmlDelayedError;
452 return m_error.data();
453}
454
455QV4::ReturnedValue
456QQmlJavaScriptExpression::evalFunction(
457 const QQmlRefPointer<QQmlContextData> &ctxt, QObject *scopeObject,
458 const QString &code, const QString &filename, quint16 line)
459{
460 QQmlEngine *engine = ctxt->engine();
461 QQmlEnginePrivate *ep = QQmlEnginePrivate::get(e: engine);
462
463 QV4::ExecutionEngine *v4 = engine->handle();
464 QV4::Scope scope(v4);
465
466 QV4::Scoped<QV4::QmlContext> qmlContext(scope, QV4::QmlContext::create(parent: v4->rootContext(), context: ctxt, scopeObject));
467 QV4::Script script(v4, qmlContext, /*parse as QML binding*/true, code, filename, line);
468 QV4::ScopedValue result(scope);
469 script.parse();
470 if (!v4->hasException)
471 result = script.run();
472 if (v4->hasException) {
473 QQmlError error = v4->catchExceptionAsQmlError();
474 if (error.description().isEmpty())
475 error.setDescription(QLatin1String("Exception occurred during function evaluation"));
476 if (error.line() == -1)
477 error.setLine(line);
478 if (error.url().isEmpty())
479 error.setUrl(QUrl::fromLocalFile(localfile: filename));
480 error.setObject(scopeObject);
481 ep->warning(error);
482 return QV4::Encode::undefined();
483 }
484 return result->asReturnedValue();
485}
486
487void QQmlJavaScriptExpression::createQmlBinding(
488 const QQmlRefPointer<QQmlContextData> &ctxt, QObject *qmlScope, const QString &code,
489 const QString &filename, quint16 line)
490{
491 QQmlEngine *engine = ctxt->engine();
492 QQmlEnginePrivate *ep = QQmlEnginePrivate::get(e: engine);
493
494 QV4::ExecutionEngine *v4 = engine->handle();
495 QV4::Scope scope(v4);
496
497 QV4::Scoped<QV4::QmlContext> qmlContext(scope, QV4::QmlContext::create(parent: v4->rootContext(), context: ctxt, scopeObject: qmlScope));
498 QV4::Script script(v4, qmlContext, /*parse as QML binding*/true, code, filename, line);
499 script.parse();
500 if (v4->hasException) {
501 QQmlDelayedError *error = delayedError();
502 error->catchJavaScriptException(engine: v4);
503 error->setErrorObject(qmlScope);
504 if (!error->addError(e: ep))
505 ep->warning(error->error());
506 return;
507 }
508 setupFunction(qmlContext, f: script.vmFunction);
509}
510
511void QQmlJavaScriptExpression::setupFunction(QV4::ExecutionContext *qmlContext, QV4::Function *f)
512{
513 if (!qmlContext || !f)
514 return;
515 m_qmlScope.set(engine: qmlContext->engine(), value: *qmlContext);
516 m_v4Function = f;
517 m_compilationUnit.reset(t: m_v4Function->executableCompilationUnit());
518}
519
520void QQmlJavaScriptExpression::setCompilationUnit(const QQmlRefPointer<QV4::ExecutableCompilationUnit> &compilationUnit)
521{
522 m_compilationUnit = compilationUnit;
523}
524
525void QPropertyChangeTrigger::trigger(QPropertyObserver *observer, QUntypedPropertyData *) {
526 auto This = static_cast<QPropertyChangeTrigger *>(observer);
527 This->m_expression->expressionChanged();
528}
529
530QMetaProperty QPropertyChangeTrigger::property() const
531{
532 if (!target)
533 return {};
534 auto const mo = target->metaObject();
535 if (!mo)
536 return {};
537 return mo->property(index: propertyIndex);
538}
539
540QPropertyChangeTrigger *QQmlJavaScriptExpression::allocatePropertyChangeTrigger(QObject *target, int propertyIndex)
541{
542 auto trigger = QQmlEnginePrivate::get(e: engine())->qPropertyTriggerPool.New( a: this );
543 trigger->target = target;
544 trigger->propertyIndex = propertyIndex;
545 auto oldHead = qpropertyChangeTriggers;
546 trigger->next = oldHead;
547 qpropertyChangeTriggers = trigger;
548 return trigger;
549}
550
551void QQmlJavaScriptExpression::clearActiveGuards()
552{
553 while (QQmlJavaScriptExpressionGuard *g = activeGuards.takeFirst())
554 g->Delete();
555}
556
557void QQmlJavaScriptExpressionGuard_callback(QQmlNotifierEndpoint *e, void **)
558{
559 QQmlJavaScriptExpression *expression =
560 static_cast<QQmlJavaScriptExpressionGuard *>(e)->expression;
561
562 expression->expressionChanged();
563}
564
565QT_END_NAMESPACE
566

source code of qtdeclarative/src/qml/qml/qqmljavascriptexpression.cpp