diff --git a/src/qmlcompiler/qqmljsimportvisitor.cpp b/src/qmlcompiler/qqmljsimportvisitor.cpp index 3850f81979..44b8003c85 100644 --- a/src/qmlcompiler/qqmljsimportvisitor.cpp +++ b/src/qmlcompiler/qqmljsimportvisitor.cpp @@ -1152,6 +1152,20 @@ void QQmlJSImportVisitor::flushPendingSignalParameters() m_pendingSignalHandler = QQmlJS::SourceLocation(); } +/*! \internal + + Records a JS function or a Script binding for a given \a scope. Returns an + index of a just recorded function-or-expression. +*/ +QQmlJSMetaMethod::RelativeFunctionIndex +QQmlJSImportVisitor::addFunctionOrExpression(const QQmlJSScope::ConstPtr &scope, + const QString &name) +{ + auto &array = m_functionsAndExpressions[scope]; + array.emplaceBack(name); + return QQmlJSMetaMethod::RelativeFunctionIndex { int(array.size() - 1) }; +} + bool QQmlJSImportVisitor::visit(QQmlJS::AST::ExpressionStatement *ast) { if (m_pendingSignalHandler.isValid()) { @@ -1428,6 +1442,7 @@ void QQmlJSImportVisitor::visitFunctionExpressionHelper(QQmlJS::AST::FunctionExp else method.setReturnTypeName(QStringLiteral("var")); + method.setJsFunctionIndex(addFunctionOrExpression(m_currentScope, method.methodName())); m_currentScope->addOwnMethod(method); if (m_currentScope->scopeType() != QQmlJSScope::QMLScope) { @@ -1514,13 +1529,29 @@ void handleTranslationBinding(QQmlJSMetaPropertyBinding &binding, QStringView ba #endif } -QQmlJSImportVisitor::LiteralOrScriptParseResult QQmlJSImportVisitor::parseLiteralOrScriptBinding(const QString name, - const QQmlJS::AST::Statement *statement) +QQmlJSImportVisitor::LiteralOrScriptParseResult +QQmlJSImportVisitor::parseLiteralOrScriptBinding(const QString name, + const QQmlJS::AST::Statement *statement) { const auto *exprStatement = cast(statement); - if (exprStatement == nullptr) + if (exprStatement == nullptr) { + if (const auto *blockStatement = cast(statement)) { + // this is a special case of script binding because: + // 1. we are trying to parse a binding (this function's logic) + // 2. we encounter a block statement + Q_ASSERT(blockStatement->statements); + auto first = blockStatement->statements->statement; + if (first == nullptr) + return LiteralOrScriptParseResult::Invalid; + QQmlJSMetaPropertyBinding binding(first->firstSourceLocation(), name); + binding.setScriptBinding(addFunctionOrExpression(m_currentScope, name), + QQmlJSMetaPropertyBinding::Script_PropertyBinding); + m_currentScope->addOwnPropertyBinding(binding); + return LiteralOrScriptParseResult::Script; + } return LiteralOrScriptParseResult::Invalid; + } auto expr = exprStatement->expression; QQmlJSMetaPropertyBinding binding(expr->firstSourceLocation(), name); @@ -1550,7 +1581,8 @@ QQmlJSImportVisitor::LiteralOrScriptParseResult QQmlJSImportVisitor::parseLitera if (templateLit->hasNoSubstitution) { binding.setStringLiteral(templateLit->value); } else { - binding.setScriptBinding(); + binding.setScriptBinding(addFunctionOrExpression(m_currentScope, name), + QQmlJSMetaPropertyBinding::Script_PropertyBinding); } break; } @@ -1565,8 +1597,11 @@ QQmlJSImportVisitor::LiteralOrScriptParseResult QQmlJSImportVisitor::parseLitera break; } - if (!binding.isValid()) // consider this to be a script binding (see IRBuilder::setBindingValue) - binding.setScriptBinding(); + if (!binding.isValid()) { + // consider this to be a script binding (see IRBuilder::setBindingValue) + binding.setScriptBinding(addFunctionOrExpression(m_currentScope, name), + QQmlJSMetaPropertyBinding::Script_PropertyBinding); + } m_currentScope->addOwnPropertyBinding(binding); // always add the binding to the scope if (!QQmlJSMetaPropertyBinding::isLiteralBinding(binding.bindingType())) @@ -1683,6 +1718,7 @@ bool QQmlJSImportVisitor::visit(UiScriptBinding *scriptBinding) if (!signal.has_value()) { m_propertyBindings[m_currentScope].append( { m_savedBindingOuterScope, group->firstSourceLocation(), name.toString() }); + // ### TODO: report Invalid parse status as a warning/error parseLiteralOrScriptBinding(name.toString(), scriptBinding->statement); } else { const auto statement = scriptBinding->statement; @@ -1712,6 +1748,19 @@ bool QQmlJSImportVisitor::visit(UiScriptBinding *scriptBinding) m_pendingSignalHandler = firstSourceLocation; m_signalHandlers.insert(firstSourceLocation, { scopeSignal.parameterNames(), hasMultilineStatementBody }); + + // when encountering a signal handler, add it as a script binding + QQmlJSMetaPropertyBinding::ScriptBindingKind kind = + QQmlJSMetaPropertyBinding::Script_Invalid; + if (!methods.isEmpty()) + kind = QQmlJSMetaPropertyBinding::Script_SignalHandler; + else if (propertyForChangeHandler(m_savedBindingOuterScope, *signal).has_value()) + kind = QQmlJSMetaPropertyBinding::Script_ChangeHandler; + + QString stringName = name.toString(); + QQmlJSMetaPropertyBinding binding(firstSourceLocation, stringName); + binding.setScriptBinding(addFunctionOrExpression(m_currentScope, stringName), kind); + m_currentScope->addOwnPropertyBinding(binding); } // TODO: before leaving the scopes, we must create the binding. diff --git a/src/qmlcompiler/qqmljsimportvisitor_p.h b/src/qmlcompiler/qqmljsimportvisitor_p.h index e9347a49f0..9386c05c86 100644 --- a/src/qmlcompiler/qqmljsimportvisitor_p.h +++ b/src/qmlcompiler/qqmljsimportvisitor_p.h @@ -181,6 +181,12 @@ protected: // A set of all types that have been used during type resolution QSet m_usedTypes; + // stores JS functions and Script bindings per scope (only the name). mimics + // the content of QmlIR::Object::functionsAndExpressions + QHash> m_functionsAndExpressions; + QQmlJSMetaMethod::RelativeFunctionIndex + addFunctionOrExpression(const QQmlJSScope::ConstPtr &scope, const QString &name); + QQmlJSImporter *m_importer; void enterEnvironment(QQmlJSScope::ScopeType type, const QString &name, diff --git a/src/qmlcompiler/qqmljsmetatypes_p.h b/src/qmlcompiler/qqmljsmetatypes_p.h index 5e7c736b02..6365c8e4fe 100644 --- a/src/qmlcompiler/qqmljsmetatypes_p.h +++ b/src/qmlcompiler/qqmljsmetatypes_p.h @@ -138,6 +138,14 @@ public: Public }; + /*! \internal + + Represents a relative JavaScript function/expression index within a type + in a QML document. Used as a typed alternative to int with an explicit + invalid state. + */ + enum class RelativeFunctionIndex : int { Invalid = -1 }; + QQmlJSMetaMethod() = default; explicit QQmlJSMetaMethod(QString name, QString returnType = QString()) : m_name(std::move(name)) @@ -208,6 +216,9 @@ public: const QVector& annotations() const { return m_annotations; } void setAnnotations(QVector annotations) { m_annotations = annotations; } + void setJsFunctionIndex(RelativeFunctionIndex index) { m_jsFunctionIndex = index; } + RelativeFunctionIndex jsFunctionIndex() const { return m_jsFunctionIndex; } + friend bool operator==(const QQmlJSMetaMethod &a, const QQmlJSMetaMethod &b) { return a.m_name == b.m_name @@ -262,6 +273,7 @@ private: Type m_methodType = Signal; Access m_methodAccess = Public; int m_revision = 0; + RelativeFunctionIndex m_jsFunctionIndex = RelativeFunctionIndex::Invalid; bool m_isConstructor = false; bool m_isJavaScriptFunction = false; bool m_isImplicitQmlPropertyChangeSignal = false; @@ -394,6 +406,13 @@ public: GroupProperty, }; + enum ScriptBindingKind : unsigned int { + Script_Invalid, + Script_PropertyBinding, // property int p: 1 + 1 + Script_SignalHandler, // onSignal: { ... } + Script_ChangeHandler, // onXChanged: { ... } + }; + private: // needs to be kept in sync with the BindingType enum @@ -439,8 +458,14 @@ private: QString value; }; struct Script { - friend bool operator==(Script , Script ) { return true; } + friend bool operator==(Script a, Script b) + { + return a.index == b.index && a.kind == b.kind; + } friend bool operator!=(Script a, Script b) { return !(a == b); } + QQmlJSMetaMethod::RelativeFunctionIndex index = + QQmlJSMetaMethod::RelativeFunctionIndex::Invalid; + ScriptBindingKind kind = Script_Invalid; }; struct Object { friend bool operator==(Object a, Object b) { return a.value == b.value && a.typeName == b.typeName; } @@ -568,11 +593,10 @@ public: m_bindingContent = Content::StringLiteral { value.toString() }; } - void setScriptBinding() + void setScriptBinding(QQmlJSMetaMethod::RelativeFunctionIndex value, ScriptBindingKind kind) { - // ### TODO: this does not allow us to do anything interesting with the binding ensureSetBindingTypeOnce(); - m_bindingContent = Content::Script {}; + m_bindingContent = Content::Script { value, kind }; } void setGroupBinding(const QSharedPointer &groupScope) @@ -653,6 +677,22 @@ public: QSharedPointer literalType(const QQmlJSTypeResolver *resolver) const; + QQmlJSMetaMethod::RelativeFunctionIndex scriptIndex() const + { + if (auto *script = std::get_if(&m_bindingContent)) + return script->index; + // warn + return QQmlJSMetaMethod::RelativeFunctionIndex::Invalid; + } + + ScriptBindingKind scriptKind() const + { + if (auto *script = std::get_if(&m_bindingContent)) + return script->kind; + // warn + return ScriptBindingKind::Script_Invalid; + } + QString objectTypeName() const { if (auto *object = std::get_if(&m_bindingContent)) diff --git a/tests/auto/qml/qqmljsscope/data/functionAndBindingIndices.qml b/tests/auto/qml/qqmljsscope/data/functionAndBindingIndices.qml new file mode 100644 index 0000000000..cbda49b748 --- /dev/null +++ b/tests/auto/qml/qqmljsscope/data/functionAndBindingIndices.qml @@ -0,0 +1,88 @@ +import QtQml +import QtQuick + +Text { + id: root + + // js func + function jsFunc() { return true; } + + // js func with typed params + function jsFuncTyped(url: string) { return url + "/"; } + + // script binding (one-line and multi-line) + elide: Text.ElideLeft + color: { + if (root.truncated) return "red"; + else return "blue"; + } + + // group prop script binding (value type and non value type) + font.pixelSize: (40 + 2) / 2 + anchors.topMargin: 44 / 4 + + // attached prop script binding + Keys.enabled: root.truncated ? true : false + + // property change handler ({} and function() {} syntaxes) + property int p0: 42 + property Item p1 + property string p2 + onP0Changed: console.log("p0 changed"); + onP1Changed: { + console.log("p1 changed"); + } + onP2Changed: function() { + console.log("p2 changed:"); + console.log(p2); + } + + // signal handler ({} and function() {} syntaxes) + signal mySignal0(int x) + signal mySignal1(string x) + signal mySignal2(bool x) + onMySignal0: console.log("single line", x); + onMySignal1: { + console.log("mySignal1 emitted:", x); + } + onMySignal2: function (x) { + console.log("mySignal2 emitted:", x); + } + + // var property assigned a js function + property var funcHolder: function(x, y) { return x + y; } + + component InlineType : Item { + function jsFuncInsideInline() { return 42; } + objectName: "inline" + " " + "component" + Item { // inside inline component + y: 40 / 2 + } + } + InlineType { + function jsFuncInsideInlineObject(x: real) { console.log(x); } + Item { // outside inline component + focus: root.jsFunc(); + } + } + + TableView { + delegate: Text { + signal delegateSignal() + onDelegateSignal: { root.jsFunc(); } + } + + property var prop: function(x) { return x * 2; } + } + + ComponentType { + property string foo: "something" + onFooChanged: console.log("foo changed!"); + } + + GridView { + delegate: ComponentType { + function jsFuncInsideDelegate(flag: bool) { return flag ? "true" : "false"; } + } + } +} diff --git a/tests/auto/qml/qqmljsscope/tst_qqmljsscope.cpp b/tests/auto/qml/qqmljsscope/tst_qqmljsscope.cpp index 86a8b5bfb6..bdbf0937c3 100644 --- a/tests/auto/qml/qqmljsscope/tst_qqmljsscope.cpp +++ b/tests/auto/qml/qqmljsscope/tst_qqmljsscope.cpp @@ -64,18 +64,23 @@ class tst_qqmljsscope : public QQmlDataTest } QQmlJSScope::ConstPtr run(QString url) + { + QmlIR::Document document(false); + return run(url, &document); + } + + QQmlJSScope::ConstPtr run(QString url, QmlIR::Document *document) { url = testFile(url); const QString sourceCode = loadUrl(url); if (sourceCode.isEmpty()) return QQmlJSScope::ConstPtr(); - QmlIR::Document document(false); // NB: JS unit generated here is ignored, so use noop function QQmlJSSaveFunction noop([](auto &&...) { return true; }); QQmlJSCompileError error; [&]() { - QVERIFY2(qCompileQmlFile(document, url, noop, nullptr, &error), + QVERIFY2(qCompileQmlFile(*document, url, noop, nullptr, &error), qPrintable(error.message)); }(); if (!error.message.isEmpty()) @@ -89,7 +94,7 @@ class tst_qqmljsscope : public QQmlDataTest QQmlJSScope::Ptr target = QQmlJSScope::create(); QQmlJSImportVisitor visitor(target, &m_importer, &logger, dataDirectory()); QQmlJSTypeResolver typeResolver { &m_importer }; - typeResolver.init(&visitor, document.program); + typeResolver.init(&visitor, document->program); return visitor.result(); } @@ -111,6 +116,7 @@ private Q_SLOTS: void groupedPropertiesConsistency(); void groupedPropertySyntax(); void attachedProperties(); + void relativeScriptIndices(); public: tst_qqmljsscope() @@ -416,5 +422,99 @@ void tst_qqmljsscope::attachedProperties() QQmlJSMetaPropertyBinding::Script); } +inline QString getScopeName(const QQmlJSScope::ConstPtr &scope) +{ + Q_ASSERT(scope); + QQmlJSScope::ScopeType type = scope->scopeType(); + if (type == QQmlJSScope::GroupedPropertyScope || type == QQmlJSScope::AttachedPropertyScope) + return scope->internalName(); + return scope->baseTypeName(); +} + +void tst_qqmljsscope::relativeScriptIndices() +{ + { + QQmlEngine engine; + QQmlComponent component(&engine); + component.loadUrl(testFileUrl(u"functionAndBindingIndices.qml"_qs)); + QVERIFY2(component.isReady(), qPrintable(component.errorString())); + QScopedPointer root(component.create()); + QVERIFY2(root, qPrintable(component.errorString())); + } + + QmlIR::Document document(false); // we need QmlIR information here + QQmlJSScope::ConstPtr root = run(u"functionAndBindingIndices.qml"_qs, &document); + QVERIFY(root); + + using IndexedString = std::pair; + // compare {property, function}Name and relative function table index + // between QQmlJSScope and QmlIR: + QList orderedJSScopeExpressions; + QList orderedQmlIrExpressions; + + QList queue; + queue.push_back(root); + while (!queue.isEmpty()) { + auto current = queue.front(); + queue.pop_front(); + + const auto methods = current->ownMethods(); + for (const auto &method : methods) { + if (method.methodType() == QQmlJSMetaMethod::Signal) + continue; + QString name = method.methodName(); + int index = static_cast(method.jsFunctionIndex()); + QVERIFY2(index >= 0, + qPrintable(QStringLiteral("Method %1 from %2 has no index") + .arg(name, getScopeName(current)))); + orderedJSScopeExpressions.emplaceBack(name, index); + } + + const auto bindings = current->ownPropertyBindings(); + for (const auto &binding : bindings) { + if (binding.bindingType() != QQmlJSMetaPropertyBinding::Script) + continue; + QString name = binding.propertyName(); + int index = static_cast(binding.scriptIndex()); + QVERIFY2(index >= 0, + qPrintable(QStringLiteral("Binding on property %1 from %2 has no index") + .arg(name, getScopeName(current)))); + orderedJSScopeExpressions.emplaceBack(name, index); + } + + const auto children = current->childScopes(); + for (const auto &c : children) + queue.push_back(c); + } + + for (const QmlIR::Object *irObject : qAsConst(document.objects)) { + const QString objectName = document.stringAt(irObject->inheritedTypeNameIndex); + for (auto it = irObject->functionsBegin(); it != irObject->functionsEnd(); ++it) { + QString name = document.stringAt(it->nameIndex); + QVERIFY2(it->index >= 0, + qPrintable(QStringLiteral("(qmlir) Method %1 from %2 has no index") + .arg(name, objectName))); + orderedQmlIrExpressions.emplaceBack(name, it->index); + } + for (auto it = irObject->bindingsBegin(); it != irObject->bindingsEnd(); ++it) { + if (it->type != QmlIR::Binding::Type_Script) + continue; + QString name = document.stringAt(it->propertyNameIndex); + int index = it->value.compiledScriptIndex; + QVERIFY2( + index >= 0, + qPrintable(QStringLiteral("(qmlir) Binding on property %1 from %2 has no index") + .arg(name, objectName))); + orderedQmlIrExpressions.emplaceBack(name, index); + } + } + + auto less = [](const IndexedString &x, const IndexedString &y) { return x.first < y.first; }; + std::sort(orderedJSScopeExpressions.begin(), orderedJSScopeExpressions.end(), less); + std::sort(orderedQmlIrExpressions.begin(), orderedQmlIrExpressions.end(), less); + + QCOMPARE(orderedJSScopeExpressions, orderedQmlIrExpressions); +} + QTEST_MAIN(tst_qqmljsscope) #include "tst_qqmljsscope.moc"