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:
parent
b15103da7f
commit
468fe40071
|
@ -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 ¶meterDefinition, 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 = [¶meters, &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:
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
module completions.CppTypes
|
||||
typeinfo types.qmltypes
|
|
@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,5 +10,7 @@ Item {
|
|||
function g(x: IC) {
|
||||
x.helloProperty
|
||||
let f = () => x.helloProperty;
|
||||
let xxx = 42
|
||||
let g = () => xx
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue