qmllint: Properly handle JavaScript functions with variable arguments

Previously calling a JavaScript function with variable arguments could
cause the linting process to error out because of a lack of matching
function arguments.

This is now handled by defaulting to a JavaScript method if no matching
function signature can be found.

Fixes: QTBUG-98299
Change-Id: I748a60839106243a12bffd8d715b48cbc53d7f57
Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
This commit is contained in:
Maximilian Goldstein 2021-11-25 12:41:44 +01:00
parent 3bb8ef28a7
commit d8e73b588a
10 changed files with 490 additions and 181 deletions

File diff suppressed because it is too large Load Diff

View File

@ -46,4 +46,5 @@ Member {
property bool isConstructor: false
property bool isList: false
property bool isPointer: false
property bool isJavaScriptFunction: false
}

View File

@ -1228,6 +1228,7 @@ void QQmlJSImportVisitor::visitFunctionExpressionHelper(QQmlJS::AST::FunctionExp
if (!name.isEmpty()) {
QQmlJSMetaMethod method(name);
method.setMethodType(QQmlJSMetaMethod::Method);
method.setIsJavaScriptFunction(true);
if (!m_pendingMethodAnnotations.isEmpty()) {
method.setAnnotations(m_pendingMethodAnnotations);

View File

@ -187,6 +187,12 @@ public:
bool isConstructor() const { return m_isConstructor; }
void setIsConstructor(bool isConstructor) { m_isConstructor = isConstructor; }
bool isJavaScriptFunction() const { return m_isJavaScriptFunction; }
void setIsJavaScriptFunction(bool isJavaScriptFunction)
{
m_isJavaScriptFunction = isJavaScriptFunction;
}
bool isValid() const { return !m_name.isEmpty(); }
const QVector<QQmlJSAnnotation>& annotations() const { return m_annotations; }
@ -247,6 +253,7 @@ private:
Access m_methodAccess = Private;
int m_revision = 0;
bool m_isConstructor = false;
bool m_isJavaScriptFunction = false;
};
class QQmlJSMetaProperty

View File

@ -312,6 +312,8 @@ void QQmlJSTypeDescriptionReader::readSignalOrMethod(UiObjectDefinition *ast, bo
metaMethod.setRevision(readIntBinding(script));
} else if (name == QLatin1String("isConstructor")) {
metaMethod.setIsConstructor(true);
} else if (name == QLatin1String("isJavaScriptFunction")) {
metaMethod.setIsJavaScriptFunction(true);
} else if (name == QLatin1String("isList")) {
// TODO: Theoretically this can happen. QQmlJSMetaMethod should store it.
} else if (name == QLatin1String("isPointer")) {
@ -320,8 +322,9 @@ void QQmlJSTypeDescriptionReader::readSignalOrMethod(UiObjectDefinition *ast, bo
// description of the type being referenced has access semantics after all.
} else {
addWarning(script->firstSourceLocation(),
tr("Expected only name, type, revision, isPointer, isList, and "
"isConstructor in script bindings."));
tr("Expected only name, type, revision, isPointer, isList, "
"isConstructor, and "
"isJavaScriptFunction in script bindings."));
}
} else {
addWarning(member->firstSourceLocation(),

View File

@ -793,6 +793,7 @@ void QQmlJSTypePropagator::generate_CallProperty(int nameIndex, int base, int ar
QQmlJSMetaMethod QQmlJSTypePropagator::bestMatchForCall(const QList<QQmlJSMetaMethod> &methods,
int argc, int argv, QStringList *errors)
{
QQmlJSMetaMethod javascriptFunction;
for (const auto &method : methods) {
const auto argumentTypes = method.parameterTypes();
if (argumentTypes.size() == 1
@ -802,6 +803,10 @@ QQmlJSMetaMethod QQmlJSTypePropagator::bestMatchForCall(const QList<QQmlJSMetaMe
continue;
}
// If we encounter a JavaScript function, use this as a fallback if no other method matches
if (method.isJavaScriptFunction())
javascriptFunction = method;
if (argc != argumentTypes.size()) {
errors->append(u"Function expects %1 arguments, but %2 were provided"_qs
.arg(argumentTypes.size())
@ -835,7 +840,7 @@ QQmlJSMetaMethod QQmlJSTypePropagator::bestMatchForCall(const QList<QQmlJSMetaMe
if (matches)
return method;
}
return QQmlJSMetaMethod();
return javascriptFunction;
}
void QQmlJSTypePropagator::propagateCall(const QList<QQmlJSMetaMethod> &methods, int argc, int argv)

View File

@ -262,6 +262,9 @@ void QmlTypesCreator::writeMethods(const QJsonArray &methods, const QString &typ
const auto isConstructor = obj.find(QLatin1String("isConstructor"));
if (isConstructor != obj.constEnd() && isConstructor->toBool())
m_qml.writeScriptBinding(QLatin1String("isConstructor"), QLatin1String("true"));
const auto isJavaScriptFunction = obj.find(QLatin1String("isJavaScriptFunction"));
if (isJavaScriptFunction != obj.constEnd() && isJavaScriptFunction->toBool())
m_qml.writeScriptBinding(QLatin1String("isJavaScriptFunction"), QLatin1String("true"));
for (const QJsonValue argument : arguments) {
const QJsonObject obj = argument.toObject();
m_qml.writeStartObject(QLatin1String("Parameter"));

View File

@ -0,0 +1,9 @@
import QtQml
QtObject {
function varArgs() {}
Component.onCompleted: {
console.log("It works!");
varArgs("This works", 2);
}
}

View File

@ -89,6 +89,7 @@ private Q_SLOTS:
void missingBuiltinsNoCrash();
void absolutePath();
void multiGrouped();
void javascriptVariableArgs();
private:
QString runQmllint(const QString &fileToLint, std::function<void(QProcess &)> handleResult,
@ -1169,5 +1170,10 @@ void TestQmllint::multiGrouped()
QVERIFY(runQmllint("multiGrouped.qml", true, {"--compiler=warning"}).isEmpty());
}
void TestQmllint::javascriptVariableArgs()
{
QVERIFY(runQmllint("javascriptVariableArgs.qml", true, { "--compiler", "warning" }).isEmpty());
}
QTEST_MAIN(TestQmllint)
#include "tst_qmllint.moc"

View File

@ -250,6 +250,7 @@ static QString buildClass(const QJSManagedValue &value, QJsonArray *classes,
methodObject.insert(QStringLiteral("access"), QStringLiteral("public"));
methodObject.insert(QStringLiteral("name"), info.name);
methodObject.insert(QStringLiteral("isJavaScriptFunction"), true);
const int formalParams = propFunction->getLength();
if (propFunction->isConstructor()) {