qmlcompiler: Add relative index for scripts and JS functions

Store relative (to scope) index for every Script binding and JavaScript
function into the corresponding data structure within QQmlJSScope. We
need this at a later phase in qmltc when code generating JavaScript
calls into the engine

For now, ignore the logic that converts the relative indices stored into
the indices pointing to the runtime function table

While testing the addition, observe missing cases where we do not record
bindings as such and fix that

Change-Id: I85030b7937c97f83183f88ae242af3a5f223443c
Reviewed-by: Ulf Hermann <ulf.hermann@qt.io>
This commit is contained in:
Andrei Golubev 2022-03-31 17:29:24 +02:00
parent 9eb8f202b6
commit 3340bdf24f
5 changed files with 296 additions and 13 deletions

View File

@ -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<const ExpressionStatement *>(statement);
if (exprStatement == nullptr)
if (exprStatement == nullptr) {
if (const auto *blockStatement = cast<const Block *>(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.

View File

@ -181,6 +181,12 @@ protected:
// A set of all types that have been used during type resolution
QSet<QString> m_usedTypes;
// stores JS functions and Script bindings per scope (only the name). mimics
// the content of QmlIR::Object::functionsAndExpressions
QHash<QQmlJSScope::ConstPtr, QList<QString>> m_functionsAndExpressions;
QQmlJSMetaMethod::RelativeFunctionIndex
addFunctionOrExpression(const QQmlJSScope::ConstPtr &scope, const QString &name);
QQmlJSImporter *m_importer;
void enterEnvironment(QQmlJSScope::ScopeType type, const QString &name,

View File

@ -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<QQmlJSAnnotation>& annotations() const { return m_annotations; }
void setAnnotations(QVector<QQmlJSAnnotation> 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<const QQmlJSScope> &groupScope)
@ -653,6 +677,22 @@ public:
QSharedPointer<const QQmlJSScope> literalType(const QQmlJSTypeResolver *resolver) const;
QQmlJSMetaMethod::RelativeFunctionIndex scriptIndex() const
{
if (auto *script = std::get_if<Content::Script>(&m_bindingContent))
return script->index;
// warn
return QQmlJSMetaMethod::RelativeFunctionIndex::Invalid;
}
ScriptBindingKind scriptKind() const
{
if (auto *script = std::get_if<Content::Script>(&m_bindingContent))
return script->kind;
// warn
return ScriptBindingKind::Script_Invalid;
}
QString objectTypeName() const
{
if (auto *object = std::get_if<Content::Object>(&m_bindingContent))

View File

@ -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"; }
}
}
}

View File

@ -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<QObject> 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<QString, int>;
// compare {property, function}Name and relative function table index
// between QQmlJSScope and QmlIR:
QList<IndexedString> orderedJSScopeExpressions;
QList<IndexedString> orderedQmlIrExpressions;
QList<QQmlJSScope::ConstPtr> 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<int>(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<int>(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"