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 | |
23 | QT_BEGIN_NAMESPACE |
24 | |
25 | bool 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 | |
41 | void 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 | |
48 | void QQmlDelayedError::setErrorDescription(const QString &description) |
49 | { |
50 | m_error.setDescription(description); |
51 | } |
52 | |
53 | void QQmlDelayedError::setErrorObject(QObject *object) |
54 | { |
55 | m_error.setObject(object); |
56 | } |
57 | |
58 | void QQmlDelayedError::catchJavaScriptException(QV4::ExecutionEngine *engine) |
59 | { |
60 | m_error = engine->catchExceptionAsQmlError(); |
61 | } |
62 | |
63 | |
64 | QQmlJavaScriptExpression::QQmlJavaScriptExpression() |
65 | : m_context(nullptr), |
66 | m_prevExpression(nullptr), |
67 | m_nextExpression(nullptr), |
68 | m_v4Function(nullptr) |
69 | { |
70 | } |
71 | |
72 | QQmlJavaScriptExpression::~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 | |
92 | QString 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 | |
104 | void QQmlJavaScriptExpression::setNotifyOnValueChanged(bool v) |
105 | { |
106 | activeGuards.setTag(v ? NotifyOnValueChanged : NoGuardTag); |
107 | if (!v) |
108 | clearActiveGuards(); |
109 | } |
110 | |
111 | void QQmlJavaScriptExpression::resetNotifyOnValueChanged() |
112 | { |
113 | setNotifyOnValueChanged(false); |
114 | } |
115 | |
116 | QQmlSourceLocation QQmlJavaScriptExpression::sourceLocation() const |
117 | { |
118 | if (m_v4Function) |
119 | return m_v4Function->sourceLocation(); |
120 | return QQmlSourceLocation(); |
121 | } |
122 | |
123 | void 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 | |
139 | void QQmlJavaScriptExpression::refresh() |
140 | { |
141 | } |
142 | |
143 | QV4::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 | |
158 | class QQmlJavaScriptExpressionCapture |
159 | { |
160 | Q_DISABLE_COPY_MOVE(QQmlJavaScriptExpressionCapture) |
161 | public: |
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 | |
206 | private: |
207 | QQmlJavaScriptExpression::DeleteWatcher watcher; |
208 | QQmlPropertyCapture capture; |
209 | QQmlEnginePrivate *ep; |
210 | QQmlPropertyCapture *lastPropertyCapture; |
211 | }; |
212 | |
213 | QV4::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 | |
253 | bool 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 | |
277 | void 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 | */ |
304 | void 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 | |
334 | void 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 | |
353 | bool 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 = ¤t->next; |
366 | current = current->next; |
367 | } |
368 | } |
369 | |
370 | return true; |
371 | } |
372 | |
373 | void 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 | |
384 | void 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 | |
401 | void 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 | |
438 | QQmlError 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 | |
448 | QQmlDelayedError *QQmlJavaScriptExpression::delayedError() |
449 | { |
450 | if (!m_error) |
451 | m_error = new QQmlDelayedError; |
452 | return m_error.data(); |
453 | } |
454 | |
455 | QV4::ReturnedValue |
456 | QQmlJavaScriptExpression::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 | |
487 | void 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 | |
511 | void 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 | |
520 | void QQmlJavaScriptExpression::setCompilationUnit(const QQmlRefPointer<QV4::ExecutableCompilationUnit> &compilationUnit) |
521 | { |
522 | m_compilationUnit = compilationUnit; |
523 | } |
524 | |
525 | void QPropertyChangeTrigger::trigger(QPropertyObserver *observer, QUntypedPropertyData *) { |
526 | auto This = static_cast<QPropertyChangeTrigger *>(observer); |
527 | This->m_expression->expressionChanged(); |
528 | } |
529 | |
530 | QMetaProperty 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 | |
540 | QPropertyChangeTrigger *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 | |
551 | void QQmlJavaScriptExpression::clearActiveGuards() |
552 | { |
553 | while (QQmlJavaScriptExpressionGuard *g = activeGuards.takeFirst()) |
554 | g->Delete(); |
555 | } |
556 | |
557 | void QQmlJavaScriptExpressionGuard_callback(QQmlNotifierEndpoint *e, void **) |
558 | { |
559 | QQmlJavaScriptExpression *expression = |
560 | static_cast<QQmlJavaScriptExpressionGuard *>(e)->expression; |
561 | |
562 | expression->expressionChanged(); |
563 | } |
564 | |
565 | QT_END_NAMESPACE |
566 | |