qmllint: Check for existence of property types

For each binding there should be a property and that property should
have a type we recognize.

Enums can be property types in C++. We support this by adding child
scopes for such enums. The child scopes are then referenced by the
QQmlJSMetaEnums and derive from int.

The test then reveals that we were missing a few properties in
QtQuick.tooling. Add those.

Pick-to: 6.1
Change-Id: I1deef94393ee0e17d34c2dc5980ebfbf25417f36
Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
This commit is contained in:
Ulf Hermann 2021-02-18 11:40:31 +01:00
parent 01542a6f25
commit 08c8e8ac3b
12 changed files with 120 additions and 29 deletions

View File

@ -55,4 +55,5 @@ QtObject {
property bool isCreatable: name.length > 0
property bool isComposite: false
property string accessSemantics: "reference"
property string defaultProperty
}

View File

@ -42,4 +42,5 @@ import QML
QtObject {
required property string name
required property string type
property bool isPointer: false
}

View File

@ -235,7 +235,7 @@ void QQmlJSImporter::processImport(
for (auto it = import.objects.begin(); it != import.objects.end(); ++it) {
const auto &val = it.value();
if (val->baseType().isNull()) // Otherwise we have already done it in localFile2ScopeTree()
val->resolveTypes(types->cppNames);
QQmlJSScope::resolveTypes(val, types->cppNames);
}
}

View File

@ -129,13 +129,13 @@ bool QQmlJSImportVisitor::visit(UiObjectDefinition *definition)
if (!m_exportedRootScope)
m_exportedRootScope = m_currentScope;
m_currentScope->resolveTypes(m_rootScopeImports);
QQmlJSScope::resolveTypes(m_currentScope, m_rootScopeImports);
return true;
}
void QQmlJSImportVisitor::endVisit(UiObjectDefinition *)
{
m_currentScope->resolveTypes(m_rootScopeImports);
QQmlJSScope::resolveTypes(m_currentScope, m_rootScopeImports);
leaveEnvironment();
}
@ -494,13 +494,13 @@ bool QQmlJSImportVisitor::visit(QQmlJS::AST::UiObjectBinding *uiob)
enterEnvironment(QQmlJSScope::QMLScope, name,
uiob->qualifiedTypeNameId->identifierToken);
m_currentScope->resolveTypes(m_rootScopeImports);
QQmlJSScope::resolveTypes(m_currentScope, m_rootScopeImports);
return true;
}
void QQmlJSImportVisitor::endVisit(QQmlJS::AST::UiObjectBinding *uiob)
{
m_currentScope->resolveTypes(m_rootScopeImports);
QQmlJSScope::resolveTypes(m_currentScope, m_rootScopeImports);
const QQmlJSScope::ConstPtr childScope = m_currentScope;
leaveEnvironment();
@ -541,7 +541,7 @@ bool QQmlJSImportVisitor::visit(ESModule *module)
void QQmlJSImportVisitor::endVisit(ESModule *)
{
m_exportedRootScope->resolveTypes(m_rootScopeImports);
QQmlJSScope::resolveTypes(m_exportedRootScope, m_rootScopeImports);
}
bool QQmlJSImportVisitor::visit(Program *)
@ -556,7 +556,7 @@ bool QQmlJSImportVisitor::visit(Program *)
void QQmlJSImportVisitor::endVisit(Program *)
{
m_exportedRootScope->resolveTypes(m_rootScopeImports);
QQmlJSScope::resolveTypes(m_exportedRootScope, m_rootScopeImports);
}
QT_END_NAMESPACE

View File

@ -62,6 +62,7 @@ class QQmlJSMetaEnum
QList<int> m_values; // empty if values unknown.
QString m_name;
QString m_alias;
QSharedPointer<const QQmlJSScope> m_type;
bool m_isFlag = false;
public:
@ -89,13 +90,17 @@ public:
int value(const QString &key) const { return m_values.value(m_keys.indexOf(key)); }
bool hasKey(const QString &key) const { return m_keys.indexOf(key) != -1; }
QSharedPointer<const QQmlJSScope> type() const { return m_type; }
void setType(const QSharedPointer<const QQmlJSScope> &type) { m_type = type; }
friend bool operator==(const QQmlJSMetaEnum &a, const QQmlJSMetaEnum &b)
{
return a.m_keys == b.m_keys
&& a.m_values == b.m_values
&& a.m_name == b.m_name
&& a.m_alias == b.m_alias
&& a.m_isFlag == b.m_isFlag;
&& a.m_isFlag == b.m_isFlag
&& a.m_type == b.m_type;
}
friend bool operator!=(const QQmlJSMetaEnum &a, const QQmlJSMetaEnum &b)
@ -105,7 +110,7 @@ public:
friend size_t qHash(const QQmlJSMetaEnum &e, size_t seed = 0)
{
return qHashMulti(seed, e.m_keys, e.m_values, e.m_name, e.m_alias, e.m_isFlag);
return qHashMulti(seed, e.m_keys, e.m_values, e.m_name, e.m_alias, e.m_isFlag, e.m_type);
}
};

View File

@ -190,7 +190,8 @@ QQmlJSScope::findJSIdentifier(const QString &id) const
return std::optional<JavaScriptIdentifier>{};
}
void QQmlJSScope::resolveTypes(const QHash<QString, QQmlJSScope::ConstPtr> &contextualTypes)
void QQmlJSScope::resolveTypes(const QQmlJSScope::Ptr &self,
const QHash<QString, QQmlJSScope::ConstPtr> &contextualTypes)
{
auto findType = [&](const QString &name) {
auto type = contextualTypes.constFind(name);
@ -200,25 +201,46 @@ void QQmlJSScope::resolveTypes(const QHash<QString, QQmlJSScope::ConstPtr> &cont
return QQmlJSScope::ConstPtr();
};
if (!m_baseType && !m_baseTypeName.isEmpty())
m_baseType = findType(m_baseTypeName);
if (!self->m_baseType && !self->m_baseTypeName.isEmpty())
self->m_baseType = findType(self->m_baseTypeName);
if (!m_attachedType && !m_attachedTypeName.isEmpty())
m_attachedType = findType(m_attachedTypeName);
if (!self->m_attachedType && !self->m_attachedTypeName.isEmpty())
self->m_attachedType = findType(self->m_attachedTypeName);
if (!m_valueType && !m_valueTypeName.isEmpty())
m_valueType = findType(m_valueTypeName);
if (!self->m_valueType && !self->m_valueTypeName.isEmpty())
self->m_valueType = findType(self->m_valueTypeName);
if (!m_extensionType && !m_extensionTypeName.isEmpty())
m_extensionType = findType(m_extensionTypeName);
if (!self->m_extensionType && !self->m_extensionTypeName.isEmpty())
self->m_extensionType = findType(self->m_extensionTypeName);
for (auto it = m_properties.begin(), end = m_properties.end(); it != end; ++it) {
const QString typeName = it->typeName();
if (!it->type() && !typeName.isEmpty())
it->setType(findType(typeName));
const auto intType = findType(QStringLiteral("int"));
Q_ASSERT(intType); // There always has to be a builtin "int" type
for (auto it = self->m_enumerations.begin(), end = self->m_enumerations.end();
it != end; ++it) {
auto enumScope = QQmlJSScope::create(EnumScope, self);
enumScope->m_baseTypeName = QStringLiteral("int");
enumScope->m_baseType = intType;
enumScope->m_semantics = AccessSemantics::Value;
enumScope->m_internalName = self->internalName() + QStringLiteral("::") + it->name();
it->setType(ConstPtr(enumScope));
}
for (auto it = m_methods.begin(), end = m_methods.end(); it != end; ++it) {
for (auto it = self->m_properties.begin(), end = self->m_properties.end(); it != end; ++it) {
const QString typeName = it->typeName();
if (it->type() || typeName.isEmpty())
continue;
if (const auto type = findType(typeName)) {
it->setType(type);
continue;
}
const auto enumeration = self->m_enumerations.find(typeName);
if (enumeration != self->m_enumerations.end())
it->setType(enumeration->type());
}
for (auto it = self->m_methods.begin(), end = self->m_methods.end(); it != end; ++it) {
const QString returnTypeName = it->returnTypeName();
if (!it->returnType() && !returnTypeName.isEmpty())
it->setReturnType(findType(returnTypeName));
@ -238,11 +260,11 @@ void QQmlJSScope::resolveTypes(const QHash<QString, QQmlJSScope::ConstPtr> &cont
it->setParameterTypes(paramTypes);
}
for (auto it = m_childScopes.begin(), end = m_childScopes.end(); it != end; ++it) {
for (auto it = self->m_childScopes.begin(), end = self->m_childScopes.end(); it != end; ++it) {
QQmlJSScope::Ptr childScope = *it;
switch (childScope->scopeType()) {
case QQmlJSScope::GroupedPropertyScope:
searchBaseAndExtensionTypes(this, [&](const QQmlJSScope *type) {
searchBaseAndExtensionTypes(self.data(), [&](const QQmlJSScope *type) {
const auto propertyIt = type->m_properties.find(childScope->internalName());
if (propertyIt != type->m_properties.end()) {
childScope->m_baseType = QQmlJSScope::ConstPtr(propertyIt->type());
@ -261,7 +283,7 @@ void QQmlJSScope::resolveTypes(const QHash<QString, QQmlJSScope::ConstPtr> &cont
default:
break;
}
childScope->resolveTypes(contextualTypes);
resolveTypes(childScope, contextualTypes);
}
}

View File

@ -96,7 +96,8 @@ public:
JSLexicalScope,
QMLScope,
GroupedPropertyScope,
AttachedPropertyScope
AttachedPropertyScope,
EnumScope
};
enum class AccessSemantics {
@ -260,7 +261,8 @@ public:
return result;
}
void resolveTypes(const QHash<QString, ConstPtr> &contextualTypes);
static void resolveTypes(const QQmlJSScope::Ptr &self,
const QHash<QString, ConstPtr> &contextualTypes);
void setSourceLocation(const QQmlJS::SourceLocation &sourceLocation)
{

View File

@ -0,0 +1,5 @@
import QtQml
QtObject {
doesNotExist: 12
}

View File

@ -0,0 +1,6 @@
import QtQml
QtObject {
property badtype bad
bad: "abc"
}

View File

@ -0,0 +1,5 @@
import QtQuick.Layouts
GridLayout {
flow: GridLayout.TopToBottom
}

View File

@ -306,6 +306,16 @@ void TestQmllint::dirtyQmlCode_data()
<< QStringLiteral("badAttached.qml")
<< QStringLiteral("unknown attached property scope WrongAttached.")
<< QString();
QTest::newRow("BadBinding")
<< QStringLiteral("badBinding.qml")
<< QStringLiteral("Binding assigned to \"doesNotExist\", but no property "
"\"doesNotExist\" exists in the current element.")
<< QString();
QTest::newRow("BadPropertyType")
<< QStringLiteral("badPropertyType.qml")
<< QStringLiteral("No type found for property \"bad\". This may be due to a missing "
"import statement or incomplete qmltypes files.")
<< QString();
}
void TestQmllint::dirtyQmlCode()
@ -371,6 +381,7 @@ void TestQmllint::cleanQmlCode_data()
QTest::newRow("grouped scope failure") << QStringLiteral("groupedScope.qml");
QTest::newRow("layouts depends quick") << QStringLiteral("layouts.qml");
QTest::newRow("attached") << QStringLiteral("attached.qml");
QTest::newRow("enumProperty") << QStringLiteral("enumProperty.qml");
}
void TestQmllint::cleanQmlCode()

View File

@ -218,8 +218,41 @@ bool FindWarningVisitor::visit(QQmlJS::AST::UiScriptBinding *uisb)
}
const QString signal = signalName(name);
if (signal.isEmpty())
if (signal.isEmpty()) {
for (const auto &childScope : qmlScope->childScopes()) {
if ((childScope->scopeType() == QQmlJSScope::AttachedPropertyScope
|| childScope->scopeType() == QQmlJSScope::GroupedPropertyScope)
&& childScope->internalName() == name) {
return true;
}
}
if (!qmlScope->hasProperty(name.toString())) {
m_errors.append({
QStringLiteral("Binding assigned to \"%1\", but no property \"%1\" "
"exists in the current element.\n").arg(name),
QtWarningMsg,
uisb->firstSourceLocation()
});
m_visitFailed = true;
return true;
}
const auto property = qmlScope->property(name.toString());
if (!property.type()) {
m_errors.append({
QStringLiteral("No type found for property \"%1\". This may be due "
"to a missing import statement or incomplete "
"qmltypes files.\n").arg(name),
QtWarningMsg,
uisb->firstSourceLocation()
});
m_visitFailed = true;
}
return true;
}
if (!qmlScope->hasMethod(signal) && m_warnUnqualified) {