qmlls: resolve types of signal handler parameters + suggest them

Extend qqmllsutils's resolveExpressionType to resolve signal handler
parameters: they have the specificity that their type has to be searched
from the signal's definition, and can't be added via type annotations.

Implement the special signal handler parameter type resolution in
separate static resolveSignalHandlerParameterType() method.

Add tests for signal handler parameters in
tst_qmlls_utils::resolveExpressionType and tst_qmlls_utils::completions.

Note that implementing the signal handler parameter type resolution is
enough for the completions to work on those types.

Also fix resolveSignalOrPropertyExpressionType() to properly recognize
autogenerated property changed signals (to avoid mismatches with
QQuickWindow's colorChanged signal, for example).

Pick-to: 6.8 6.7
Fixes: QTBUG-127458
Change-Id: I7fc9d198564320485bb8e3527943c4994c02d90f
Reviewed-by: Semih Yavuz <semih.yavuz@qt.io>
This commit is contained in:
Sami Shalayel 2024-07-23 17:16:48 +02:00
parent b15103da7f
commit 468fe40071
8 changed files with 296 additions and 2 deletions

View File

@ -3,6 +3,7 @@
#include "qqmllsutils_p.h"
#include <QtCore/qassert.h>
#include <QtLanguageServer/private/qlanguageserverspectypes_p.h>
#include <QtCore/qthreadpool.h>
#include <QtCore/private/qduplicatetracker_p.h>
@ -657,7 +658,14 @@ static std::optional<SignalOrProperty> resolveNameInQmlScope(const QString &name
if (const auto propertyName = QQmlSignalNames::changedHandlerNameToPropertyName(name)) {
if (owner->hasProperty(*propertyName)) {
return SignalOrProperty{ *propertyName, PropertyChangedHandlerIdentifier };
const QString signalName = *QQmlSignalNames::changedHandlerNameToSignalName(name);
const QQmlJSMetaMethod signal = owner->methods(signalName).front();
// PropertyChangedHandlers don't have parameters: treat all other as regular signal
// handlers. Even if they appear in the notify of the property.
if (signal.parameterNames().size() == 0)
return SignalOrProperty{ *propertyName, PropertyChangedHandlerIdentifier };
else
return SignalOrProperty{ signalName, SignalHandlerIdentifier };
}
}
@ -1398,6 +1406,106 @@ static std::optional<ExpressionType> resolveFieldMemberExpressionType(const DomI
return owner;
}
/*!
\internal
Resolves the expression type of a binding for signal handlers, like the function expression
\c{(x) => ...} in
\qml
onHelloSignal: (x) => ...
\endqml
would be resolved to the \c{onHelloSignal} expression type, for example.
*/
static std::optional<ExpressionType> resolveBindingIfSignalHandler(const DomItem &functionExpression)
{
if (functionExpression.internalKind() != DomType::ScriptFunctionExpression)
return {};
const DomItem parent = functionExpression.directParent();
if (parent.internalKind() != DomType::ScriptExpression)
return {};
const DomItem grandParent = parent.directParent();
if (grandParent.internalKind() != DomType::Binding)
return {};
auto bindingType = resolveExpressionType(grandParent, ResolveOwnerType);
return bindingType;
}
/*!
\internal
In a signal handler
\qml
onSomeSignal: (x, y, z) => ....
\endqml
the parameters \c x, \c y and \c z are not allowed to have type annotations: instead, their type is
defined by the signal definition itself.
This code detects signal handler parameters and resolves their type using the signal's definition.
*/
static std::optional<ExpressionType>
resolveSignalHandlerParameterType(const DomItem &parameterDefinition, const QString &name,
ResolveOptions options)
{
const std::optional<QQmlJSScope::JavaScriptIdentifier> jsIdentifier =
parameterDefinition.semanticScope()->jsIdentifier(name);
if (!jsIdentifier || jsIdentifier->kind != QQmlJSScope::JavaScriptIdentifier::Parameter)
return {};
const DomItem handlerFunctionExpression =
parameterDefinition.internalKind() == DomType::ScriptBlockStatement
? parameterDefinition.directParent()
: parameterDefinition;
const std::optional<ExpressionType> bindingType =
resolveBindingIfSignalHandler(handlerFunctionExpression);
if (!bindingType)
return {};
if (bindingType->type == PropertyChangedHandlerIdentifier)
return ExpressionType{};
if (bindingType->type != SignalHandlerIdentifier)
return {};
const DomItem parameters = handlerFunctionExpression[Fields::parameters];
const int indexOfParameter = [&parameters, &name]() {
for (int i = 0; i < parameters.indexes(); ++i) {
if (parameters[i][Fields::identifier].value().toString() == name)
return i;
}
Q_ASSERT_X(false, "resolveSignalHandlerParameter",
"can't find JS identifier with Parameter kind in the parameters");
Q_UNREACHABLE_RETURN(-1);
}();
const std::optional<QString> signalName =
QQmlSignalNames::handlerNameToSignalName(*bindingType->name);
Q_ASSERT_X(signalName.has_value(), "resolveSignalHandlerParameterType",
"handlerNameToSignalName failed on a SignalHandler");
const QQmlJSMetaMethod signalDefinition =
bindingType->semanticScope->methods(*signalName).front();
const QList<QQmlJSMetaParameter> parameterList = signalDefinition.parameters();
// not a signal handler parameter after all
if (parameterList.size() <= indexOfParameter)
return {};
// now we can return an ExpressionType, even if the indexOfParameter calculation result is only
// needed to check whether this is a signal handler parameter or not.
if (options == ResolveOwnerType)
return ExpressionType{ name, bindingType->semanticScope, JavaScriptIdentifier };
else {
const QQmlJSScope::ConstPtr parameterType = parameterList[indexOfParameter].type();
return ExpressionType{ name, parameterType, JavaScriptIdentifier };
}
}
static std::optional<ExpressionType> resolveIdentifierExpressionType(const DomItem &item,
ResolveOptions options)
{
@ -1413,6 +1521,9 @@ static std::optional<ExpressionType> resolveIdentifierExpressionType(const DomIt
"QQmlLSUtils::findDefinitionOf",
"JS definition does not actually define the JS identifer. "
"It should be empty.");
if (auto parameter = resolveSignalHandlerParameterType(definitionOfItem, name, options))
return parameter;
const auto scope = definitionOfItem.semanticScope();
return ExpressionType{ name,
options == ResolveOwnerType
@ -1507,7 +1618,7 @@ resolveSignalOrPropertyExpressionType(const QString &name, const QQmlJSScope::Co
case MethodIdentifier:
switch (options) {
case ResolveOwnerType: {
return ExpressionType{ name, findDefiningScopeForMethod(scope, name),
return ExpressionType{ name, findDefiningScopeForMethod(scope, signalOrProperty->name),
signalOrProperty->type };
}
case ResolveActualTypeForFieldMemberExpression:

View File

@ -0,0 +1,2 @@
module completions.CppTypes
typeinfo types.qmltypes

View File

@ -0,0 +1,40 @@
import QtQuick.tooling 1.2
Module {
Component {
file: "private/myfile_p.h"
name: "SomeType"
accessSemantics: "reference"
prototype: "QObject"
Property {
name: "helloData"
type: "real"
}
}
Component {
file: "private/myfile_p.h"
name: "WithSignal"
accessSemantics: "reference"
prototype: "QObject"
exports: ["completions.CppTypes/WithSignal 254.0"]
Signal {
name: "helloSignal"
Parameter { name: "data"; type: "SomeType" }
}
}
Component {
file: "private/myfile_p.h"
name: "WithFakePropertyChangedSignal"
accessSemantics: "reference"
prototype: "QObject"
exports: ["completions.CppTypes/WithFakePropertyChangedSignal 254.0"]
Property {
name: "mySomeType"
type: "SomeType"
notify: "mySomeTypeChanged"
}
Signal {
name: "mySomeTypeChanged"
Parameter { name: "data"; type: "SomeType" }
}
}
}

View File

@ -10,5 +10,7 @@ Item {
function g(x: IC) {
x.helloProperty
let f = () => x.helloProperty;
let xxx = 42
let g = () => xx
}
}

View File

@ -0,0 +1,16 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
import QtQuick
import completions.CppTypes
Rectangle {
onColorChanged: (col) => console.log(col.r)
WithSignal {
onHelloSignal: (someType) => console.log(someType.x)
}
WithFakePropertyChangedSignal {
onMySomeTypeChanged: (someType) => console.log(someType.x)
}
}

View File

@ -29,4 +29,41 @@ Module {
type: "var"
}
}
Component {
file: "private/myfile_p.h"
name: "SomeType"
accessSemantics: "reference"
prototype: "QObject"
Property {
name: "helloData"
type: "real"
}
}
Component {
file: "private/myfile_p.h"
name: "WithSignal"
accessSemantics: "reference"
prototype: "QObject"
exports: ["resolveExpressionType.CppTypes/WithSignal 254.0"]
Signal {
name: "helloSignal"
Parameter { name: "data"; type: "SomeType" }
}
}
Component {
file: "private/myfile_p.h"
name: "WithFakePropertyChangedSignal"
accessSemantics: "reference"
prototype: "QObject"
exports: ["completions.CppTypes/WithFakePropertyChangedSignal 254.0"]
Property {
name: "mySomeType"
type: "SomeType"
notify: "mySomeTypeChanged"
}
Signal {
name: "mySomeTypeChanged"
Parameter { name: "data"; type: "SomeType" }
}
}
}

View File

@ -0,0 +1,28 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
import QtQuick
import resolveExpressionType.CppTypes
Item {
// invalid: property changed signals have no parameters!
onColorChanged: (invalid) => console.log(invalid.r)
WithSignal {
onHelloSignal: (parameter, invalid) => console.log(parameter.helloData, invalid.hello)
}
WithSignal {
onHelloSignal: function (parameter) {
let xxx = 42;
console.log(xxx)
}
}
WithSignal {
onHelloSignal: function (parameter) {
console.log(parameter.helloData)
}
}
WithFakePropertyChangedSignal {
onMySomeTypeChanged: (someType) => console.log(someType.x)
}
}

View File

@ -2217,6 +2217,42 @@ void tst_qmlls_utils::resolveExpressionType_data()
QTest::addRow("qualifiedModule4") << file << 4 << 42 << ResolveOwnerType << noFile
<< noLine << QualifiedModuleIdentifier << u"RETM"_s;
}
{
const QString myHeader = u"private/myfile_p.h"_s;
const QString file = testFile(u"resolveExpressionType/parameterTypeFromBinding.qml"_s);
QTest::addRow("invalidPropertyChangedHandlerParameter")
<< file << 9 << 23 << ResolveActualTypeForFieldMemberExpression << noFile << noLine
<< JavaScriptIdentifier << u"invalid"_s;
QTest::addRow("invalidPropertyChangedHandlerParameter2")
<< file << 9 << 49 << ResolveActualTypeForFieldMemberExpression << noFile << noLine
<< JavaScriptIdentifier << u"invalid"_s;
QTest::addRow("signalHandlerParameter")
<< file << 12 << 30 << ResolveActualTypeForFieldMemberExpression << myHeader
<< noLine << JavaScriptIdentifier << u"parameter"_s;
QTest::addRow("signalHandlerParameter2")
<< file << 12 << 63 << ResolveActualTypeForFieldMemberExpression << myHeader
<< noLine << JavaScriptIdentifier << u"parameter"_s;
QTest::addRow("invalidSignalParameter")
<< file << 12 << 39 << ResolveActualTypeForFieldMemberExpression << noFile << noLine
<< JavaScriptIdentifier << u"invalid"_s;
QTest::addRow("invalidSignalParameter2")
<< file << 12 << 85 << ResolveActualTypeForFieldMemberExpression << noFile << noLine
<< JavaScriptIdentifier << u"invalid"_s;
QTest::addRow("unrelatedToHandlerParameter") << file << 16 << 17 << ResolveOwnerType << file
<< 15 << JavaScriptIdentifier << u"xxx"_s;
QTest::addRow("unrelatedToHandlerParameter2")
<< file << 17 << 26 << ResolveOwnerType << file << 15 << JavaScriptIdentifier
<< u"xxx"_s;
QTest::addRow("signalHandlerParameterNonArrow")
<< file << 21 << 37 << ResolveActualTypeForFieldMemberExpression << myHeader
<< noLine << JavaScriptIdentifier << u"parameter"_s;
QTest::addRow("onColorChangedHandlerParameter")
<< file << 12 << 30 << ResolveActualTypeForFieldMemberExpression << myHeader
<< noLine << JavaScriptIdentifier << u"parameter"_s;
QTest::addRow("onFakePropertyChangedSignal")
<< file << 26 << 18 << ResolveOwnerType << myHeader << noLine
<< SignalHandlerIdentifier << u"onMySomeTypeChanged"_s;
}
}
void tst_qmlls_utils::resolveExpressionType()
@ -3940,6 +3976,11 @@ void tst_qmlls_utils::completions_data()
{ forStatementCompletion, CompletionItemKind::Snippet } }
<< QStringList{ u"helloProperty"_s };
QTest::newRow("insideArrowBody")
<< testFile(u"completions/functionBody.qml"_s) << 14 << 24
<< ExpectedCompletions{ { u"xxx"_s, CompletionItemKind::Variable } }
<< QStringList{ u"helloProperty"_s };
QTest::newRow("noBreakInMethodBody")
<< testFile(u"completions/suggestContinueAndBreak.qml"_s) << 8 << 8
<< ExpectedCompletions{ { u"x"_s, CompletionItemKind::Variable } }
@ -4297,6 +4338,23 @@ void tst_qmlls_utils::completions_data()
<< testFile("completions/thisExpression.qml") << 5 << 14
<< ExpectedCompletions{ }
<< QStringList{ forStatementCompletion, u"f"_s };
QTest::newRow("unexistingSignalParameter")
<< testFile("completions/parameterTypeFromBinding.qml") << 8 << 46
<< ExpectedCompletions{} << QStringList{};
QTest::newRow("signalParameter")
<< testFile("completions/parameterTypeFromBinding.qml") << 11 << 59
<< ExpectedCompletions{ { u"helloData"_s, CompletionItemKind::Property } }
<< QStringList{};
QTest::newRow("signalParameter2")
<< testFile("completions/parameterTypeFromBinding.qml") << 11 << 38
<< ExpectedCompletions{ { u"console"_s, CompletionItemKind::Property } }
<< QStringList{};
QTest::newRow("fakePropertyChangedSignal")
<< testFile("completions/parameterTypeFromBinding.qml") << 14 << 65
<< ExpectedCompletions{ { u"helloData"_s, CompletionItemKind::Property } }
<< QStringList{ u"mySomeType"_s };
}
void tst_qmlls_utils::completions()