aboutsummaryrefslogtreecommitdiffstats
path: root/tools/qmllint/checkidentifiers.cpp
diff options
context:
space:
mode:
authorUlf Hermann <ulf.hermann@qt.io>2020-03-30 17:42:48 +0200
committerUlf Hermann <ulf.hermann@qt.io>2020-04-01 10:29:29 +0200
commitc38ea80c5dd71a20eade7b3a3b619c1996c6af0b (patch)
treebd7eafa6e0e19f0aca3911c74ab99c7b8062a14a /tools/qmllint/checkidentifiers.cpp
parent4cf0962dc4d8d48aa600c5b56b160c8553782140 (diff)
Move qmllint's metatype support to tools/shared
We want to read qmltypes files and analyze scopes also from other tools. Furthermore, restructure the shared directory, so that each tool only includes what it needs. Change-Id: I96a2dcc8b1c5fac613592fb1867bf51fa5ef3a6e Reviewed-by: Simon Hausmann <simon.hausmann@qt.io>
Diffstat (limited to 'tools/qmllint/checkidentifiers.cpp')
-rw-r--r--tools/qmllint/checkidentifiers.cpp408
1 files changed, 408 insertions, 0 deletions
diff --git a/tools/qmllint/checkidentifiers.cpp b/tools/qmllint/checkidentifiers.cpp
new file mode 100644
index 0000000000..6b9e48ed38
--- /dev/null
+++ b/tools/qmllint/checkidentifiers.cpp
@@ -0,0 +1,408 @@
+/****************************************************************************
+**
+** Copyright (C) 2020 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the tools applications of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL-EXCEPT$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "checkidentifiers.h"
+#include "qcoloroutput.h"
+
+#include <QtCore/qqueue.h>
+#include <QtCore/qsharedpointer.h>
+
+class IssueLocationWithContext
+{
+public:
+ IssueLocationWithContext(const QString &code, const QQmlJS::SourceLocation &location) {
+ int before = std::max(0,code.lastIndexOf(QLatin1Char('\n'), location.offset));
+ m_beforeText = code.midRef(before + 1, int(location.offset - (before + 1)));
+ m_issueText = code.midRef(location.offset, location.length);
+ int after = code.indexOf(QLatin1Char('\n'), int(location.offset + location.length));
+ m_afterText = code.midRef(int(location.offset + location.length),
+ int(after - (location.offset+location.length)));
+ }
+
+ QStringRef beforeText() const { return m_beforeText; }
+ QStringRef issueText() const { return m_issueText; }
+ QStringRef afterText() const { return m_afterText; }
+
+private:
+ QStringRef m_beforeText;
+ QStringRef m_issueText;
+ QStringRef m_afterText;
+};
+
+static void writeWarning(ColorOutput *out)
+{
+ out->write(QLatin1String("Warning: "), Warning);
+}
+
+static const QStringList unknownBuiltins = {
+ // TODO: "string" should be added to builtins.qmltypes, and the special handling below removed
+ QStringLiteral("alias"), // TODO: we cannot properly resolve aliases, yet
+ QStringLiteral("QRectF"), // TODO: should be added to builtins.qmltypes
+ QStringLiteral("QFont"), // TODO: should be added to builtins.qmltypes
+ QStringLiteral("QJSValue"), // We cannot say anything intelligent about untyped JS values.
+ QStringLiteral("variant"), // Same for generic variants
+};
+
+void CheckIdentifiers::printContext(const QQmlJS::SourceLocation &location) const
+{
+ IssueLocationWithContext issueLocationWithContext {m_code, location};
+ m_colorOut->write(issueLocationWithContext.beforeText().toString(), Normal);
+ m_colorOut->write(issueLocationWithContext.issueText().toString(), Error);
+ m_colorOut->write(issueLocationWithContext.afterText().toString() + QLatin1Char('\n'), Normal);
+ int tabCount = issueLocationWithContext.beforeText().count(QLatin1Char('\t'));
+ m_colorOut->write(QString::fromLatin1(" ").repeated(
+ issueLocationWithContext.beforeText().length() - tabCount)
+ + QString::fromLatin1("\t").repeated(tabCount)
+ + QString::fromLatin1("^").repeated(location.length)
+ + QLatin1Char('\n'), Normal);
+}
+
+bool CheckIdentifiers::checkMemberAccess(const QVector<ScopeTree::FieldMember> &members,
+ const ScopeTree *scope) const
+{
+ QStringList expectedNext;
+ QString detectedRestrictiveName;
+ QString detectedRestrictiveKind;
+
+ for (const ScopeTree::FieldMember &access : members) {
+ if (scope == nullptr) {
+ writeWarning(m_colorOut);
+ m_colorOut->write(
+ QString::fromLatin1("Type \"%1\" of base \"%2\" not found when accessing member \"%3\" at %4:%5.\n")
+ .arg(detectedRestrictiveKind)
+ .arg(detectedRestrictiveName)
+ .arg(access.m_name)
+ .arg(access.m_location.startLine)
+ .arg(access.m_location.startColumn), Normal);
+ printContext(access.m_location);
+ return false;
+ }
+
+ const QString scopeName = scope->name().isEmpty() ? scope->className() : scope->name();
+
+ if (!detectedRestrictiveKind.isEmpty()) {
+ if (expectedNext.contains(access.m_name)) {
+ expectedNext.clear();
+ continue;
+ }
+
+ writeWarning(m_colorOut);
+ m_colorOut->write(QString::fromLatin1(
+ "\"%1\" is a %2. You cannot access \"%3\" on it at %4:%5\n")
+ .arg(detectedRestrictiveName)
+ .arg(detectedRestrictiveKind)
+ .arg(access.m_name)
+ .arg(access.m_location.startLine)
+ .arg(access.m_location.startColumn), Normal);
+ printContext(access.m_location);
+ return false;
+ }
+
+ const auto properties = scope->properties();
+ const auto scopeIt = properties.find(access.m_name);
+ if (scopeIt != properties.end()) {
+ const QString typeName = access.m_parentType.isEmpty() ? scopeIt->typeName()
+ : access.m_parentType;
+ if (scopeIt->isList()) {
+ detectedRestrictiveKind = QLatin1String("list");
+ detectedRestrictiveName = access.m_name;
+ expectedNext.append(QLatin1String("length"));
+ continue;
+ }
+
+ if (typeName == QLatin1String("string")) {
+ detectedRestrictiveKind = typeName;
+ detectedRestrictiveName = access.m_name;
+ expectedNext.append(QLatin1String("length"));
+ continue;
+ }
+
+ if (const ScopeTree *type = scopeIt->type()) {
+ if (access.m_parentType.isEmpty()) {
+ scope = type;
+ continue;
+ }
+ }
+
+ if (unknownBuiltins.contains(typeName))
+ return true;
+
+ const auto it = m_types.find(typeName);
+ if (it == m_types.end()) {
+ detectedRestrictiveKind = typeName;
+ detectedRestrictiveName = access.m_name;
+ scope = nullptr;
+ } else {
+ scope = it->get();
+ }
+ continue;
+ }
+
+ const auto methods = scope->methods();
+ const auto scopeMethodIt = methods.find(access.m_name);
+ if (scopeMethodIt != methods.end())
+ return true; // Access to property of JS function
+
+ const auto enums= scope->enums();
+ for (const auto enumerator : enums) {
+ for (const QString &key : enumerator.keys()) {
+ if (access.m_name == key) {
+ detectedRestrictiveKind = QLatin1String("enum");
+ detectedRestrictiveName = access.m_name;
+ break;
+ }
+ }
+ if (!detectedRestrictiveName.isEmpty())
+ break;
+ }
+ if (!detectedRestrictiveName.isEmpty())
+ continue;
+
+ auto type = m_types.value(scopeName);
+ bool typeFound = false;
+ while (type) {
+ const auto typeProperties = type->properties();
+ const auto typeIt = typeProperties.find(access.m_name);
+ if (typeIt != typeProperties.end()) {
+ const ScopeTree *propType = access.m_parentType.isEmpty()
+ ? typeIt->type()
+ : m_types.value(access.m_parentType).get();
+ scope = propType ? propType : m_types.value(typeIt->typeName()).get();
+ typeFound = true;
+ break;
+ }
+
+ const auto typeMethods = type->methods();
+ const auto typeMethodIt = typeMethods.find(access.m_name);
+ if (typeMethodIt != typeMethods.end()) {
+ detectedRestrictiveName = access.m_name;
+ detectedRestrictiveKind = QLatin1String("method");
+ typeFound = true;
+ break;
+ }
+
+ type = m_types.value(type->superclassName());
+ }
+ if (typeFound)
+ continue;
+
+ if (access.m_name.front().isUpper() && scope->scopeType() == ScopeType::QMLScope) {
+ // may be an attached type
+ const auto it = m_types.find(access.m_name);
+ if (it != m_types.end() && !(*it)->attachedTypeName().isEmpty()) {
+ const auto attached = m_types.find((*it)->attachedTypeName());
+ if (attached != m_types.end()) {
+ scope = attached->get();
+ continue;
+ }
+ }
+ }
+
+ writeWarning(m_colorOut);
+ m_colorOut->write(QString::fromLatin1(
+ "Property \"%1\" not found on type \"%2\" at %3:%4\n")
+ .arg(access.m_name)
+ .arg(scopeName)
+ .arg(access.m_location.startLine)
+ .arg(access.m_location.startColumn), Normal);
+ printContext(access.m_location);
+ return false;
+ }
+
+ return true;
+}
+
+bool CheckIdentifiers::operator()(const QHash<QString, const ScopeTree *> &qmlIDs,
+ const ScopeTree *root, const QString &rootId) const
+{
+ bool noUnqualifiedIdentifier = true;
+
+ // revisit all scopes
+ QQueue<const ScopeTree *> workQueue;
+ workQueue.enqueue(root);
+ while (!workQueue.empty()) {
+ const ScopeTree *currentScope = workQueue.dequeue();
+ const auto unmatchedSignalHandlers = currentScope->unmatchedSignalHandlers();
+ for (const auto &handler : unmatchedSignalHandlers) {
+ writeWarning(m_colorOut);
+ m_colorOut->write(QString::fromLatin1(
+ "no matching signal found for handler \"%1\" at %2:%3\n")
+ .arg(handler.first).arg(handler.second.startLine)
+ .arg(handler.second.startColumn), Normal);
+ printContext(handler.second);
+ }
+
+ const auto memberAccessChains = currentScope->memberAccessChains();
+ for (auto memberAccessChain : memberAccessChains) {
+ if (memberAccessChain.isEmpty())
+ continue;
+
+ const auto memberAccessBase = memberAccessChain.takeFirst();
+ if (currentScope->isIdInCurrentJSScopes(memberAccessBase.m_name))
+ continue;
+
+ auto it = qmlIDs.find(memberAccessBase.m_name);
+ if (it != qmlIDs.end()) {
+ if (*it != nullptr) {
+ if (!checkMemberAccess(memberAccessChain, *it))
+ noUnqualifiedIdentifier = false;
+ continue;
+ } else if (!memberAccessChain.isEmpty()) {
+ // It could be a qualified type name
+ const QString scopedName = memberAccessChain.first().m_name;
+ if (scopedName.front().isUpper()) {
+ const QString qualified = memberAccessBase.m_name + QLatin1Char('.')
+ + scopedName;
+ const auto typeIt = m_types.find(qualified);
+ if (typeIt != m_types.end()) {
+ memberAccessChain.takeFirst();
+ if (!checkMemberAccess(memberAccessChain, typeIt->get()))
+ noUnqualifiedIdentifier = false;
+ continue;
+ }
+ }
+ }
+ }
+
+ auto qmlScope = currentScope->currentQMLScope();
+ if (qmlScope->methods().contains(memberAccessBase.m_name)) {
+ // a property of a JavaScript function
+ continue;
+ }
+
+ const auto properties = qmlScope->properties();
+ const auto qmlIt = properties.find(memberAccessBase.m_name);
+ if (qmlIt != properties.end()) {
+ if (memberAccessChain.isEmpty() || unknownBuiltins.contains(qmlIt->typeName()))
+ continue;
+
+ if (!qmlIt->type()) {
+ writeWarning(m_colorOut);
+ m_colorOut->write(QString::fromLatin1(
+ "Type of property \"%2\" not found at %3:%4\n")
+ .arg(memberAccessBase.m_name)
+ .arg(memberAccessBase.m_location.startLine)
+ .arg(memberAccessBase.m_location.startColumn), Normal);
+ printContext(memberAccessBase.m_location);
+ noUnqualifiedIdentifier = false;
+ } else if (!checkMemberAccess(memberAccessChain, qmlIt->type())) {
+ noUnqualifiedIdentifier = false;
+ }
+
+ continue;
+ }
+
+ // TODO: Lots of builtins are missing
+ if (memberAccessBase.m_name == QLatin1String("Qt"))
+ continue;
+
+ const auto typeIt = m_types.find(memberAccessBase.m_name);
+ if (typeIt != m_types.end()) {
+ if (!checkMemberAccess(memberAccessChain, typeIt->get()))
+ noUnqualifiedIdentifier = false;
+ continue;
+ }
+
+ noUnqualifiedIdentifier = false;
+ writeWarning(m_colorOut);
+ const auto location = memberAccessBase.m_location;
+ m_colorOut->write(QString::fromLatin1("unqualified access at %1:%2\n")
+ .arg(location.startLine).arg(location.startColumn),
+ Normal);
+
+ printContext(location);
+
+ // root(JS) --> program(qml) --> (first element)
+ const auto firstElement = root->childScopes()[0]->childScopes()[0];
+ if (firstElement->properties().contains(memberAccessBase.m_name)
+ || firstElement->methods().contains(memberAccessBase.m_name)
+ || firstElement->enums().contains(memberAccessBase.m_name)) {
+ m_colorOut->write(QLatin1String("Note: "), Info);
+ m_colorOut->write(memberAccessBase.m_name + QLatin1String(" is a meber of the root element\n"), Normal );
+ m_colorOut->write(QLatin1String(" You can qualify the access with its id to avoid this warning:\n"), Normal);
+ if (rootId == QLatin1String("<id>")) {
+ m_colorOut->write(QLatin1String("Note: "), Warning);
+ m_colorOut->write(QLatin1String("You first have to give the root element an id\n"));
+ }
+ IssueLocationWithContext issueLocationWithContext {m_code, location};
+ m_colorOut->write(issueLocationWithContext.beforeText().toString(), Normal);
+ m_colorOut->write(rootId + QLatin1Char('.'), Hint);
+ m_colorOut->write(issueLocationWithContext.issueText().toString(), Normal);
+ m_colorOut->write(issueLocationWithContext.afterText() + QLatin1Char('\n'), Normal);
+ } else if (currentScope->isIdInjectedFromSignal(memberAccessBase.m_name)) {
+ auto methodUsages = currentScope->currentQMLScope()->injectedSignalIdentifiers()
+ .values(memberAccessBase.m_name);
+ auto location = memberAccessBase.m_location;
+ // sort the list of signal handlers by their occurrence in the source code
+ // then, we select the first one whose location is after the unqualified id
+ // and go one step backwards to get the one which we actually need
+ std::sort(methodUsages.begin(), methodUsages.end(),
+ [](const MethodUsage &m1, const MethodUsage &m2) {
+ return m1.loc.startLine < m2.loc.startLine
+ || (m1.loc.startLine == m2.loc.startLine
+ && m1.loc.startColumn < m2.loc.startColumn);
+ });
+ auto oneBehindIt = std::find_if(methodUsages.begin(), methodUsages.end(),
+ [&location](const MethodUsage &methodUsage) {
+ return location.startLine < methodUsage.loc.startLine
+ || (location.startLine == methodUsage.loc.startLine
+ && location.startColumn < methodUsage.loc.startColumn);
+ });
+ auto methodUsage = *(--oneBehindIt);
+ m_colorOut->write(QLatin1String("Note: "), Info);
+ m_colorOut->write(
+ memberAccessBase.m_name + QString::fromLatin1(
+ " is accessible in this scope because "
+ "you are handling a signal at %1:%2\n")
+ .arg(methodUsage.loc.startLine).arg(methodUsage.loc.startColumn),
+ Normal);
+ m_colorOut->write(QLatin1String("Consider using a function instead\n"), Normal);
+ IssueLocationWithContext context {m_code, methodUsage.loc};
+ m_colorOut->write(context.beforeText() + QLatin1Char(' '));
+ m_colorOut->write(QLatin1String(methodUsage.hasMultilineHandlerBody
+ ? "function("
+ : "("),
+ Hint);
+ const auto parameters = methodUsage.method.parameterNames();
+ for (int numParams = parameters.size(); numParams > 0; --numParams) {
+ m_colorOut->write(parameters.at(parameters.size() - numParams), Hint);
+ if (numParams > 1)
+ m_colorOut->write(QLatin1String(", "), Hint);
+ }
+ m_colorOut->write(QLatin1String(methodUsage.hasMultilineHandlerBody ? ")" : ") => "),
+ Hint);
+ m_colorOut->write(QLatin1String(" {..."), Normal);
+ }
+ m_colorOut->write(QLatin1String("\n\n\n"), Normal);
+ }
+ const auto childScopes = currentScope->childScopes();
+ for (auto const &childScope : childScopes)
+ workQueue.enqueue(childScope.get());
+ }
+ return noUnqualifiedIdentifier;
+}