qqmljsimportvisitor: give base types to attached scopes

It seems that our attached scopes have no base type: they don't inherit
the attached properties or methods because their baseTypeName is not
set.

Therefore, set their baseTypeName, and move the resolveTypes calls
inside of setScopeName() instead of potentially forgetting them after
enterEnvironment/RootScope() calls.

With the (resolved) base type, we know about inherited signals, like
in:
```
Keys.onPressed: { /*here is the "event" argument available*/ }
```
for example where we can insert the "event" JS identifier inside the
QQmlJSScope of the block of the signal handler, now that we now that
"Keys" has a "pressed"-method with one argument "event" on its base
type.

Add a test to make sure that the body of an attached signal handler
contains the JS identifier of the arguments of the attached signal to be
handled. This JS identifier is used later on in qmlls to provide
completions in the body of the attached signal handler.

Also fix LinterVisitor::leaveEnvironment() that starts complaining
about "Component" attached properties that have no child, now that
attached properties have base types.

Pick-to: 6.10 6.9 6.8
Fixes: QTBUG-137736
Change-Id: I8de0158ca9946d5e0e4f4f0a46614385f0edca69
Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
This commit is contained in:
Sami Shalayel 2025-07-04 14:42:12 +02:00
parent d232d16c79
commit 9a907040a0
7 changed files with 65 additions and 8 deletions

View File

@ -116,14 +116,31 @@ bool QQmlJSImportVisitor::safeInsertJSIdentifier(QQmlJSScope::Ptr &scope, const
\internal
Sets the name of \a scope to \a name based on \a type.
*/
inline void setScopeName(QQmlJSScope::Ptr &scope, QQmlJSScope::ScopeType type, const QString &name)
void QQmlJSImportVisitor::setScopeName(QQmlJSScope::Ptr &scope, QQmlJSScope::ScopeType type,
const QString &name)
{
Q_ASSERT(scope);
if (type == QQmlSA::ScopeType::GroupedPropertyScope
|| type == QQmlSA::ScopeType::AttachedPropertyScope)
switch (type) {
case QQmlSA::ScopeType::GroupedPropertyScope:
scope->setInternalName(name);
return;
case QQmlSA::ScopeType::AttachedPropertyScope:
scope->setInternalName(name);
else
scope->setBaseTypeName(name);
QQmlJSScope::resolveTypes(scope, m_rootScopeImports.contextualTypes(), &m_usedTypes);
return;
case QQmlSA::ScopeType::QMLScope:
scope->setBaseTypeName(name);
QQmlJSScope::resolveTypes(scope, m_rootScopeImports.contextualTypes(), &m_usedTypes);
return;
case QQmlSA::ScopeType::JSFunctionScope:
case QQmlSA::ScopeType::BindingFunctionScope:
case QQmlSA::ScopeType::SignalHandlerFunctionScope:
case QQmlSA::ScopeType::JSLexicalScope:
case QQmlSA::ScopeType::EnumScope:
scope->setBaseTypeName(name);
return;
};
}
/*!
@ -224,10 +241,10 @@ void QQmlJSImportVisitor::populateCurrentScope(
QQmlJSScope::ScopeType type, const QString &name, const QQmlJS::SourceLocation &location)
{
m_currentScope->setScopeType(type);
setScopeName(m_currentScope, type, name);
m_currentScope->setIsComposite(true);
m_currentScope->setFilePath(m_logger->filePath());
m_currentScope->setSourceLocation(location);
setScopeName(m_currentScope, type, name);
m_scopesByIrLocation.insert({ location.startLine, location.startColumn }, m_currentScope);
}
@ -1754,8 +1771,7 @@ bool QQmlJSImportVisitor::visit(UiObjectDefinition *definition)
m_currentScope->setIsSingleton(m_rootIsSingleton);
}
const QTypeRevision revision = QQmlJSScope::resolveTypes(
m_currentScope, m_rootScopeImports.contextualTypes(), &m_usedTypes);
const QTypeRevision revision = m_currentScope->baseTypeRevision();
if (auto base = m_currentScope->baseType(); base) {
if (isRoot && base->internalName() == u"QQmlComponent") {
m_logger->log(u"Qml top level type cannot be 'Component'."_s, qmlTopLevelComponent,
@ -3034,7 +3050,6 @@ bool QQmlJSImportVisitor::visit(QQmlJS::AST::UiObjectBinding *uiob)
enterEnvironment(QQmlSA::ScopeType::QMLScope, typeName,
uiob->qualifiedTypeNameId->identifierToken);
QQmlJSScope::resolveTypes(m_currentScope, m_rootScopeImports.contextualTypes(), &m_usedTypes);
m_qmlTypes.append(m_currentScope); // new QMLScope is created here, so add it
m_objectBindingScopes << m_currentScope;

View File

@ -160,6 +160,8 @@ protected:
virtual bool checkCustomParser(const QQmlJSScope::ConstPtr &scope);
void setScopeName(QQmlJSScope::Ptr &scope, QQmlJSScope::ScopeType type, const QString &name);
QString m_implicitImportDirectory;
QStringList m_qmldirFiles;
QQmlJSScope::Ptr m_currentScope;

View File

@ -30,6 +30,9 @@ void LinterVisitor::leaveEnvironment()
{
const auto leaveEnv = qScopeGuard([this] { QQmlJSImportVisitor::leaveEnvironment(); });
if (m_currentScope->scopeType() != QQmlSA::ScopeType::QMLScope)
return;
if (auto base = m_currentScope->baseType()) {
if (base->internalName() == u"QQmlComponent"_s) {
const auto nChildren = std::count_if(

View File

@ -0,0 +1,5 @@
import QtQuick
Item {
Keys.onPressed: {}
}

View File

@ -107,6 +107,7 @@ private Q_SLOTS:
void groupedPropertiesConsistency();
void groupedPropertySyntax();
void attachedProperties();
void attachedSignalHandler();
void scriptIndices();
void extensions();
void emptyBlockBinding();
@ -484,6 +485,27 @@ void tst_qqmljsscope::attachedProperties()
QCOMPARE(value(bindingsOfBaseType, u"priority"_s).bindingType(), QQmlSA::BindingType::Script);
}
void tst_qqmljsscope::attachedSignalHandler()
{
QQmlJSScope::ConstPtr root = run(u"attachedSignalHandler.qml"_s);
QVERIFY(root);
const auto binding = std::find_if(root->childScopesBegin(), root->childScopesEnd(),
[](const QQmlJSScope::ConstPtr &child) {
return child->baseTypeName() == "signalHandler"_L1;
});
QCOMPARE_NE(binding, root->childScopesEnd());
const auto block = std::find_if(
(**binding).childScopesBegin(), (**binding).childScopesEnd(),
[](const QQmlJSScope::ConstPtr &child) { return child->baseTypeName() == "block"_L1; });
QCOMPARE_NE(block, (**binding).childScopesEnd());
// note: the event JS identifier is inserted only if the "Keys" basetype of the attached
// property was resolved before the creation of the "block" scope.
QVERIFY((**block).jsIdentifier("event"_L1));
}
inline QString getScopeName(const QQmlJSScope::ConstPtr &scope)
{
Q_ASSERT(scope);

View File

@ -0,0 +1,7 @@
import QtQuick
Item {
Keys.onPressed: {
}
}

View File

@ -4412,6 +4412,9 @@ void tst_qmlls_utils::completions_data()
QTest::newRow("insideComment2")
<< testFile("completions/Comments.qml") << 6 << 9 << ExpectedCompletions{}
<< QStringList{ u"x"_s, u"Item"_s, forStatementCompletion };
QTest::newRow("attachedSignalHandler")
<< testFile("completions/attachedSignalHandler.qml") << 5 << 9
<< ExpectedCompletions{ { "event", CompletionItemKind::Variable } } << QStringList{};
}
void tst_qmlls_utils::completions()