diff options
Diffstat (limited to 'tools')
-rw-r--r-- | tools/qmllint/componentversion.cpp | 123 | ||||
-rw-r--r-- | tools/qmllint/componentversion.h | 73 | ||||
-rw-r--r-- | tools/qmllint/fakemetaobject.cpp | 600 | ||||
-rw-r--r-- | tools/qmllint/fakemetaobject.h | 236 | ||||
-rw-r--r-- | tools/qmllint/findunqualified.cpp | 772 | ||||
-rw-r--r-- | tools/qmllint/findunqualified.h | 129 | ||||
-rw-r--r-- | tools/qmllint/main.cpp | 42 | ||||
-rw-r--r-- | tools/qmllint/qcoloroutput.cpp | 342 | ||||
-rw-r--r-- | tools/qmllint/qcoloroutput_p.h | 110 | ||||
-rw-r--r-- | tools/qmllint/qmljstypedescriptionreader.cpp | 704 | ||||
-rw-r--r-- | tools/qmllint/qmljstypedescriptionreader.h | 103 | ||||
-rw-r--r-- | tools/qmllint/qmllint.pro | 16 | ||||
-rw-r--r-- | tools/qmllint/scopetree.cpp | 255 | ||||
-rw-r--r-- | tools/qmllint/scopetree.h | 105 |
14 files changed, 3606 insertions, 4 deletions
diff --git a/tools/qmllint/componentversion.cpp b/tools/qmllint/componentversion.cpp new file mode 100644 index 0000000000..3dc4ac37d0 --- /dev/null +++ b/tools/qmllint/componentversion.cpp @@ -0,0 +1,123 @@ +/**************************************************************************** +** +** Copyright (C) 2019 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 "componentversion.h" + +#include <QString> +#include <QCryptographicHash> + +#include <limits> + +using namespace LanguageUtils; + +const int ComponentVersion::NoVersion = -1; +const int ComponentVersion::MaxVersion = std::numeric_limits<int>::max(); + +ComponentVersion::ComponentVersion() + : _major(NoVersion), _minor(NoVersion) +{ +} + +ComponentVersion::ComponentVersion(int major, int minor) + : _major(major), _minor(minor) +{ +} + +ComponentVersion::ComponentVersion(const QString &versionString) + : _major(NoVersion), _minor(NoVersion) +{ + int dotIdx = versionString.indexOf(QLatin1Char('.')); + if (dotIdx == -1) + return; + bool ok = false; + int maybeMajor = versionString.leftRef(dotIdx).toInt(&ok); + if (!ok) + return; + int maybeMinor = versionString.midRef(dotIdx + 1).toInt(&ok); + if (!ok) + return; + _major = maybeMajor; + _minor = maybeMinor; +} + +ComponentVersion::~ComponentVersion() +{ +} + +bool ComponentVersion::isValid() const +{ + return _major >= 0 && _minor >= 0; +} + +QString ComponentVersion::toString() const +{ + return QString::fromLatin1("%1.%2").arg(QString::number(_major), + QString::number(_minor)); +} + +void ComponentVersion::addToHash(QCryptographicHash &hash) const +{ + hash.addData(reinterpret_cast<const char *>(&_major), sizeof(_major)); + hash.addData(reinterpret_cast<const char *>(&_minor), sizeof(_minor)); +} + +namespace LanguageUtils { + +bool operator<(const ComponentVersion &lhs, const ComponentVersion &rhs) +{ + return lhs.majorVersion() < rhs.majorVersion() + || (lhs.majorVersion() == rhs.majorVersion() && lhs.minorVersion() < rhs.minorVersion()); +} + +bool operator<=(const ComponentVersion &lhs, const ComponentVersion &rhs) +{ + return lhs.majorVersion() < rhs.majorVersion() + || (lhs.majorVersion() == rhs.majorVersion() && lhs.minorVersion() <= rhs.minorVersion()); +} + +bool operator>(const ComponentVersion &lhs, const ComponentVersion &rhs) +{ + return rhs < lhs; +} + +bool operator>=(const ComponentVersion &lhs, const ComponentVersion &rhs) +{ + return rhs <= lhs; +} + +bool operator==(const ComponentVersion &lhs, const ComponentVersion &rhs) +{ + return lhs.majorVersion() == rhs.majorVersion() && lhs.minorVersion() == rhs.minorVersion(); +} + +bool operator!=(const ComponentVersion &lhs, const ComponentVersion &rhs) +{ + return !(lhs == rhs); +} + +} diff --git a/tools/qmllint/componentversion.h b/tools/qmllint/componentversion.h new file mode 100644 index 0000000000..9d079f1d30 --- /dev/null +++ b/tools/qmllint/componentversion.h @@ -0,0 +1,73 @@ +/**************************************************************************** +** +** Copyright (C) 2019 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$ +** +****************************************************************************/ + +#ifndef COMPONENTVERSION_H +#define COMPONENTVERSION_H + +#include <qglobal.h> + +QT_BEGIN_NAMESPACE +class QCryptographicHash; +QT_END_NAMESPACE + +namespace LanguageUtils { + +class ComponentVersion +{ + int _major; + int _minor; + +public: + static const int NoVersion; + static const int MaxVersion; + + ComponentVersion(); + ComponentVersion(int major, int minor); + explicit ComponentVersion(const QString &versionString); + ~ComponentVersion(); + + int majorVersion() const + { return _major; } + int minorVersion() const + { return _minor; } + + bool isValid() const; + QString toString() const; + void addToHash(QCryptographicHash &hash) const; +}; + +bool operator<(const ComponentVersion &lhs, const ComponentVersion &rhs); +bool operator<=(const ComponentVersion &lhs, const ComponentVersion &rhs); +bool operator>(const ComponentVersion &lhs, const ComponentVersion &rhs); +bool operator>=(const ComponentVersion &lhs, const ComponentVersion &rhs); +bool operator==(const ComponentVersion &lhs, const ComponentVersion &rhs); +bool operator!=(const ComponentVersion &lhs, const ComponentVersion &rhs); + +} // namespace LanguageUtils + +#endif // COMPONENTVERSION_H diff --git a/tools/qmllint/fakemetaobject.cpp b/tools/qmllint/fakemetaobject.cpp new file mode 100644 index 0000000000..514bb2fe42 --- /dev/null +++ b/tools/qmllint/fakemetaobject.cpp @@ -0,0 +1,600 @@ +/**************************************************************************** +** +** Copyright (C) 2019 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 "fakemetaobject.h" +#include <QCryptographicHash> + +using namespace LanguageUtils; + +FakeMetaEnum::FakeMetaEnum() +{} + +FakeMetaEnum::FakeMetaEnum(const QString &name) + : m_name(name) +{} + +bool FakeMetaEnum::isValid() const +{ return !m_name.isEmpty(); } + +QString FakeMetaEnum::name() const +{ return m_name; } + +void FakeMetaEnum::setName(const QString &name) +{ m_name = name; } + +void FakeMetaEnum::addKey(const QString &key, int value) +{ m_keys.append(key); m_values.append(value); } + +QString FakeMetaEnum::key(int index) const +{ return m_keys.at(index); } + +int FakeMetaEnum::keyCount() const +{ return m_keys.size(); } + +QStringList FakeMetaEnum::keys() const +{ return m_keys; } + +bool FakeMetaEnum::hasKey(const QString &key) const +{ return m_keys.contains(key); } + +void FakeMetaEnum::addToHash(QCryptographicHash &hash) const +{ + int len = m_name.size(); + hash.addData(reinterpret_cast<const char *>(&len), sizeof(len)); + hash.addData(reinterpret_cast<const char *>(m_name.constData()), len * sizeof(QChar)); + len = m_keys.size(); + hash.addData(reinterpret_cast<const char *>(&len), sizeof(len)); + foreach (const QString &key, m_keys) { + len = key.size(); + hash.addData(reinterpret_cast<const char *>(&len), sizeof(len)); + hash.addData(reinterpret_cast<const char *>(key.constData()), len * sizeof(QChar)); + } + len = m_values.size(); + hash.addData(reinterpret_cast<const char *>(&len), sizeof(len)); + foreach (int value, m_values) + hash.addData(reinterpret_cast<const char *>(&value), sizeof(value)); +} + +QString FakeMetaEnum::describe(int baseIndent) const +{ + QString newLine = QString::fromLatin1("\n") + QString::fromLatin1(" ").repeated(baseIndent); + QString res = QLatin1String("Enum "); + res += name(); + res += QLatin1String(":{"); + for (int i = 0; i < keyCount(); ++i) { + res += newLine; + res += QLatin1String(" "); + res += key(i); + res += QLatin1String(": "); + res += QString::number(m_values.value(i, -1)); + } + res += newLine; + res += QLatin1Char('}'); + return res; +} + +QString FakeMetaEnum::toString() const +{ + return describe(); +} + +FakeMetaMethod::FakeMetaMethod(const QString &name, const QString &returnType) + : m_name(name) + , m_returnType(returnType) + , m_methodTy(FakeMetaMethod::Method) + , m_methodAccess(FakeMetaMethod::Public) + , m_revision(0) +{} + +FakeMetaMethod::FakeMetaMethod() + : m_methodTy(FakeMetaMethod::Method) + , m_methodAccess(FakeMetaMethod::Public) + , m_revision(0) +{} + +QString FakeMetaMethod::methodName() const +{ return m_name; } + +void FakeMetaMethod::setMethodName(const QString &name) +{ m_name = name; } + +void FakeMetaMethod::setReturnType(const QString &type) +{ m_returnType = type; } + +QStringList FakeMetaMethod::parameterNames() const +{ return m_paramNames; } + +QStringList FakeMetaMethod::parameterTypes() const +{ return m_paramTypes; } + +void FakeMetaMethod::addParameter(const QString &name, const QString &type) +{ m_paramNames.append(name); m_paramTypes.append(type); } + +int FakeMetaMethod::methodType() const +{ return m_methodTy; } + +void FakeMetaMethod::setMethodType(int methodType) +{ m_methodTy = methodType; } + +int FakeMetaMethod::access() const +{ return m_methodAccess; } + +int FakeMetaMethod::revision() const +{ return m_revision; } + +void FakeMetaMethod::setRevision(int r) +{ m_revision = r; } + +void FakeMetaMethod::addToHash(QCryptographicHash &hash) const +{ + int len = m_name.size(); + hash.addData(reinterpret_cast<const char *>(&len), sizeof(len)); + hash.addData(reinterpret_cast<const char *>(m_name.constData()), len * sizeof(QChar)); + hash.addData(reinterpret_cast<const char *>(&m_methodAccess), sizeof(m_methodAccess)); + hash.addData(reinterpret_cast<const char *>(&m_methodTy), sizeof(m_methodTy)); + hash.addData(reinterpret_cast<const char *>(&m_revision), sizeof(m_revision)); + len = m_paramNames.size(); + hash.addData(reinterpret_cast<const char *>(&len), sizeof(len)); + foreach (const QString &pName, m_paramNames) { + len = pName.size(); + hash.addData(reinterpret_cast<const char *>(&len), sizeof(len)); + hash.addData(reinterpret_cast<const char *>(pName.constData()), len * sizeof(QChar)); + } + len = m_paramTypes.size(); + hash.addData(reinterpret_cast<const char *>(&len), sizeof(len)); + foreach (const QString &pType, m_paramTypes) { + len = pType.size(); + hash.addData(reinterpret_cast<const char *>(&len), sizeof(len)); + hash.addData(reinterpret_cast<const char *>(pType.constData()), len * sizeof(QChar)); + } + len = m_returnType.size(); + hash.addData(reinterpret_cast<const char *>(&len), sizeof(len)); + hash.addData(reinterpret_cast<const char *>(m_returnType.constData()), len * sizeof(QChar)); +} + +QString FakeMetaMethod::describe(int baseIndent) const +{ + QString newLine = QString::fromLatin1("\n") + QString::fromLatin1(" ").repeated(baseIndent); + QString res = QLatin1String("Method {"); + res += newLine; + res += QLatin1String(" methodName:"); + res += methodName(); + res += newLine; + res += QLatin1String(" methodType:"); + res += methodType(); + res += newLine; + res += QLatin1String(" parameterNames:["); + foreach (const QString &pName, parameterNames()) { + res += newLine; + res += QLatin1String(" "); + res += pName; + } + res += QLatin1Char(']'); + res += newLine; + res += QLatin1String(" parameterTypes:["); + foreach (const QString &pType, parameterTypes()) { + res += newLine; + res += QLatin1String(" "); + res += pType; + } + res += QLatin1Char(']'); + res += newLine; + res += QLatin1Char('}'); + return res; +} + +QString FakeMetaMethod::toString() const +{ + return describe(); +} + + +FakeMetaProperty::FakeMetaProperty(const QString &name, const QString &type, bool isList, + bool isWritable, bool isPointer, int revision) + : m_propertyName(name) + , m_type(type) + , m_isList(isList) + , m_isWritable(isWritable) + , m_isPointer(isPointer) + , m_revision(revision) +{} + +QString FakeMetaProperty::name() const +{ return m_propertyName; } + +QString FakeMetaProperty::typeName() const +{ return m_type; } + +bool FakeMetaProperty::isList() const +{ return m_isList; } + +bool FakeMetaProperty::isWritable() const +{ return m_isWritable; } + +bool FakeMetaProperty::isPointer() const +{ return m_isPointer; } + +int FakeMetaProperty::revision() const +{ return m_revision; } + +void FakeMetaProperty::addToHash(QCryptographicHash &hash) const +{ + int len = m_propertyName.size(); + hash.addData(reinterpret_cast<const char *>(&len), sizeof(len)); + hash.addData(reinterpret_cast<const char *>(m_propertyName.constData()), len * sizeof(QChar)); + hash.addData(reinterpret_cast<const char *>(&m_revision), sizeof(m_revision)); + int flags = (m_isList ? (1 << 0) : 0) + + (m_isPointer ? (1 << 1) : 0) + + (m_isWritable ? (1 << 2) : 0); + hash.addData(reinterpret_cast<const char *>(&flags), sizeof(flags)); + len = m_type.size(); + hash.addData(reinterpret_cast<const char *>(&len), sizeof(len)); + hash.addData(reinterpret_cast<const char *>(m_type.constData()), len * sizeof(QChar)); +} + +QString FakeMetaProperty::describe(int baseIndent) const +{ + auto boolStr = [] (bool v) { return v ? QLatin1String("true") : QLatin1String("false"); }; + QString newLine = QString::fromLatin1("\n") + QString::fromLatin1(" ").repeated(baseIndent); + QString res = QLatin1String("Property {"); + res += newLine; + res += QLatin1String(" name:"); + res += name(); + res += newLine; + res += QLatin1String(" typeName:"); + res += typeName(); + res += newLine; + res += QLatin1String(" typeName:"); + res += QString::number(revision()); + res += newLine; + res += QLatin1String(" isList:"); + res += boolStr(isList()); + res += newLine; + res += QLatin1String(" isPointer:"); + res += boolStr(isPointer()); + res += newLine; + res += QLatin1String(" isWritable:"); + res += boolStr(isWritable()); + res += newLine; + res += QLatin1Char('}'); + return res; +} + +QString FakeMetaProperty::toString() const +{ + return describe(); +} + + +FakeMetaObject::FakeMetaObject() : m_isSingleton(false), m_isCreatable(true), m_isComposite(false) +{ +} + +QString FakeMetaObject::className() const +{ return m_className; } +void FakeMetaObject::setClassName(const QString &name) +{ m_className = name; } + +void FakeMetaObject::addExport(const QString &name, const QString &package, ComponentVersion version) +{ + Export exp; + exp.type = name; + exp.package = package; + exp.version = version; + m_exports.append(exp); +} + +void FakeMetaObject::setExportMetaObjectRevision(int exportIndex, int metaObjectRevision) +{ + m_exports[exportIndex].metaObjectRevision = metaObjectRevision; +} + +QList<FakeMetaObject::Export> FakeMetaObject::exports() const +{ return m_exports; } +FakeMetaObject::Export FakeMetaObject::exportInPackage(const QString &package) const +{ + foreach (const Export &exp, m_exports) { + if (exp.package == package) + return exp; + } + return Export(); +} + +void FakeMetaObject::setSuperclassName(const QString &superclass) +{ m_superName = superclass; } +QString FakeMetaObject::superclassName() const +{ return m_superName; } + +void FakeMetaObject::addEnum(const FakeMetaEnum &fakeEnum) +{ m_enumNameToIndex.insert(fakeEnum.name(), m_enums.size()); m_enums.append(fakeEnum); } +int FakeMetaObject::enumeratorCount() const +{ return m_enums.size(); } +int FakeMetaObject::enumeratorOffset() const +{ return 0; } +FakeMetaEnum FakeMetaObject::enumerator(int index) const +{ return m_enums.at(index); } +int FakeMetaObject::enumeratorIndex(const QString &name) const +{ return m_enumNameToIndex.value(name, -1); } + +void FakeMetaObject::addProperty(const FakeMetaProperty &property) +{ m_propNameToIdx.insert(property.name(), m_props.size()); m_props.append(property); } +int FakeMetaObject::propertyCount() const +{ return m_props.size(); } +int FakeMetaObject::propertyOffset() const +{ return 0; } +FakeMetaProperty FakeMetaObject::property(int index) const +{ return m_props.at(index); } +int FakeMetaObject::propertyIndex(const QString &name) const +{ return m_propNameToIdx.value(name, -1); } + +void FakeMetaObject::addMethod(const FakeMetaMethod &method) +{ m_methods.append(method); } +int FakeMetaObject::methodCount() const +{ return m_methods.size(); } +int FakeMetaObject::methodOffset() const +{ return 0; } +FakeMetaMethod FakeMetaObject::method(int index) const +{ return m_methods.at(index); } +int FakeMetaObject::methodIndex(const QString &name) const //If performances becomes an issue, just use a nameToIdx hash +{ + for (int i=0; i<m_methods.count(); i++) + if (m_methods[i].methodName() == name) + return i; + return -1; +} + +QString FakeMetaObject::defaultPropertyName() const +{ return m_defaultPropertyName; } +void FakeMetaObject::setDefaultPropertyName(const QString &defaultPropertyName) +{ m_defaultPropertyName = defaultPropertyName; } + +QString FakeMetaObject::attachedTypeName() const +{ return m_attachedTypeName; } +void FakeMetaObject::setAttachedTypeName(const QString &name) +{ m_attachedTypeName = name; } + +QByteArray FakeMetaObject::calculateFingerprint() const +{ + QCryptographicHash hash(QCryptographicHash::Sha1); + int len = m_className.size(); + hash.addData(reinterpret_cast<const char *>(&len), sizeof(len)); + hash.addData(reinterpret_cast<const char *>(m_className.constData()), len * sizeof(QChar)); + len = m_attachedTypeName.size(); + hash.addData(reinterpret_cast<const char *>(&len), sizeof(len)); + hash.addData(reinterpret_cast<const char *>(m_attachedTypeName.constData()), len * sizeof(QChar)); + len = m_defaultPropertyName.size(); + hash.addData(reinterpret_cast<const char *>(&len), sizeof(len)); + hash.addData(reinterpret_cast<const char *>(m_defaultPropertyName.constData()), len * sizeof(QChar)); + len = m_enumNameToIndex.size(); + hash.addData(reinterpret_cast<const char *>(&len), sizeof(len)); + { + QStringList keys(m_enumNameToIndex.keys()); + keys.sort(); + foreach (const QString &key, keys) { + len = key.size(); + hash.addData(reinterpret_cast<const char *>(&len), sizeof(len)); + hash.addData(reinterpret_cast<const char *>(key.constData()), len * sizeof(QChar)); + int value = m_enumNameToIndex.value(key); + hash.addData(reinterpret_cast<const char *>(&value), sizeof(value)); // avoid? this adds order dependency to fingerprint... + m_enums.at(value).addToHash(hash); + } + } + len = m_exports.size(); + hash.addData(reinterpret_cast<const char *>(&len), sizeof(len)); + foreach (const Export &e, m_exports) + e.addToHash(hash); // normalize order? + len = m_exports.size(); + hash.addData(reinterpret_cast<const char *>(&len), sizeof(len)); + foreach (const FakeMetaMethod &m, m_methods) + m.addToHash(hash); // normalize order? + { + QStringList keys(m_propNameToIdx.keys()); + keys.sort(); + foreach (const QString &key, keys) { + len = key.size(); + hash.addData(reinterpret_cast<const char *>(&len), sizeof(len)); + hash.addData(reinterpret_cast<const char *>(key.constData()), len * sizeof(QChar)); + int value = m_propNameToIdx.value(key); + hash.addData(reinterpret_cast<const char *>(&value), sizeof(value)); // avoid? this adds order dependency to fingerprint... + m_props.at(value).addToHash(hash); + } + } + len = m_superName.size(); + hash.addData(reinterpret_cast<const char *>(&len), sizeof(len)); + hash.addData(reinterpret_cast<const char *>(m_superName.constData()), len * sizeof(QChar)); + + QByteArray res = hash.result(); + res.append('F'); + return res; +} + +void FakeMetaObject::updateFingerprint() +{ + m_fingerprint = calculateFingerprint(); +} + +QByteArray FakeMetaObject::fingerprint() const +{ + return m_fingerprint; +} + +bool FakeMetaObject::isSingleton() const +{ + return m_isSingleton; +} + +bool FakeMetaObject::isCreatable() const +{ + return m_isCreatable; +} + +bool FakeMetaObject::isComposite() const +{ + return m_isComposite; +} + +void FakeMetaObject::setIsSingleton(bool value) +{ + m_isSingleton = value; +} + +void FakeMetaObject::setIsCreatable(bool value) +{ + m_isCreatable = value; +} + +void FakeMetaObject::setIsComposite(bool value) +{ + m_isSingleton = value; +} + +QString FakeMetaObject::toString() const +{ + return describe(); +} + +QString FakeMetaObject::describe(bool printDetails, int baseIndent) const +{ + QString res = QString::fromLatin1("FakeMetaObject@%1") + .arg((quintptr)(void *)this, 0, 16); + if (!printDetails) + return res; + auto boolStr = [] (bool v) { return v ? QLatin1String("true") : QLatin1String("false"); }; + QString newLine = QString::fromLatin1("\n") + QString::fromLatin1(" ").repeated(baseIndent); + res += QLatin1Char('{'); + res += newLine; + res += QLatin1String("className:"); + res += className(); + res += newLine; + res += QLatin1String("superClassName:"); + res += superclassName(); + res += newLine; + res += QLatin1String("isSingleton:"); + res += boolStr(isSingleton()); + res += newLine; + res += QLatin1String("isCreatable:"); + res += boolStr(isCreatable()); + res += newLine; + res += QLatin1String("isComposite:"); + res += boolStr(isComposite()); + res += newLine; + res += QLatin1String("defaultPropertyName:"); + res += defaultPropertyName(); + res += newLine; + res += QLatin1String("attachedTypeName:"); + res += attachedTypeName(); + res += newLine; + res += QLatin1String("fingerprint:"); + res += QString::fromUtf8(fingerprint()); + + res += newLine; + res += QLatin1String("exports:["); + foreach (const Export &e, exports()) { + res += newLine; + res += QLatin1String(" "); + res += e.describe(baseIndent + 2); + } + res += QLatin1Char(']'); + + res += newLine; + res += QLatin1String("enums:["); + for (int iEnum = 0; iEnum < enumeratorCount() ; ++ iEnum) { + FakeMetaEnum e = enumerator(enumeratorOffset() + iEnum); + res += newLine; + res += QLatin1String(" "); + res += e.describe(baseIndent + 2); + } + res += QLatin1Char(']'); + + res += newLine; + res += QLatin1String("properties:["); + for (int iProp = 0; iProp < propertyCount() ; ++ iProp) { + FakeMetaProperty prop = property(propertyOffset() + iProp); + res += newLine; + res += QLatin1String(" "); + res += prop.describe(baseIndent + 2); + } + res += QLatin1Char(']'); + res += QLatin1String("methods:["); + for (int iMethod = 0; iMethod < methodOffset() ; ++ iMethod) { + FakeMetaMethod m = method(methodOffset() + iMethod); + res += newLine; + res += QLatin1String(" "); + m.describe(baseIndent + 2); + } + res += QLatin1Char(']'); + res += newLine; + res += QLatin1Char('}'); + return res; +} + +FakeMetaObject::Export::Export() + : metaObjectRevision(0) +{} +bool FakeMetaObject::Export::isValid() const +{ return version.isValid() || !package.isEmpty() || !type.isEmpty(); } + +void FakeMetaObject::Export::addToHash(QCryptographicHash &hash) const +{ + int len = package.size(); + hash.addData(reinterpret_cast<const char *>(&len), sizeof(len)); + hash.addData(reinterpret_cast<const char *>(package.constData()), len * sizeof(QChar)); + len = type.size(); + hash.addData(reinterpret_cast<const char *>(&len), sizeof(len)); + hash.addData(reinterpret_cast<const char *>(type.constData()), len * sizeof(QChar)); + version.addToHash(hash); + hash.addData(reinterpret_cast<const char *>(&metaObjectRevision), sizeof(metaObjectRevision)); +} + +QString FakeMetaObject::Export::describe(int baseIndent) const +{ + QString newLine = QString::fromLatin1("\n") + QString::fromLatin1(" ").repeated(baseIndent); + QString res = QLatin1String("Export {"); + res += newLine; + res += QLatin1String(" package:"); + res += package; + res += newLine; + res += QLatin1String(" type:"); + res += type; + res += newLine; + res += QLatin1String(" version:"); + res += version.toString(); + res += newLine; + res += QLatin1String(" metaObjectRevision:"); + res += QString::number(metaObjectRevision); + res += newLine; + res += QLatin1String(" isValid:"); + res += QString::number(isValid()); + res += newLine; + res += QLatin1Char('}'); + return res; +} + +QString FakeMetaObject::Export::toString() const +{ + return describe(); +} diff --git a/tools/qmllint/fakemetaobject.h b/tools/qmllint/fakemetaobject.h new file mode 100644 index 0000000000..4e0ea1f8b3 --- /dev/null +++ b/tools/qmllint/fakemetaobject.h @@ -0,0 +1,236 @@ +/**************************************************************************** +** +** Copyright (C) 2019 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$ +** +****************************************************************************/ + +#ifndef FAKEMETAOBJECT_H +#define FAKEMETAOBJECT_H + +#include <QString> +#include <QStringList> +#include <QList> +#include <QHash> +#include <QSharedPointer> + +#include "componentversion.h" + +QT_BEGIN_NAMESPACE +class QCryptographicHash; +QT_END_NAMESPACE + +namespace LanguageUtils { + +class FakeMetaEnum { + QString m_name; + QStringList m_keys; + QList<int> m_values; + +public: + FakeMetaEnum(); + explicit FakeMetaEnum(const QString &name); + + bool isValid() const; + + QString name() const; + void setName(const QString &name); + + void addKey(const QString &key, int value); + QString key(int index) const; + int keyCount() const; + QStringList keys() const; + bool hasKey(const QString &key) const; + void addToHash(QCryptographicHash &hash) const; + + QString describe(int baseIndent = 0) const; + QString toString() const; +}; + +class FakeMetaMethod { +public: + enum { + Signal, + Slot, + Method + }; + + enum { + Private, + Protected, + Public + }; + +public: + FakeMetaMethod(); + explicit FakeMetaMethod(const QString &name, const QString &returnType = QString()); + + QString methodName() const; + void setMethodName(const QString &name); + + void setReturnType(const QString &type); + + QStringList parameterNames() const; + QStringList parameterTypes() const; + void addParameter(const QString &name, const QString &type); + + int methodType() const; + void setMethodType(int methodType); + + int access() const; + + int revision() const; + void setRevision(int r); + void addToHash(QCryptographicHash &hash) const; + + QString describe(int baseIndent = 0) const; + QString toString() const; +private: + QString m_name; + QString m_returnType; + QStringList m_paramNames; + QStringList m_paramTypes; + int m_methodTy; + int m_methodAccess; + int m_revision; +}; + +class FakeMetaProperty { + QString m_propertyName; + QString m_type; + bool m_isList; + bool m_isWritable; + bool m_isPointer; + int m_revision; + +public: + FakeMetaProperty(const QString &name, const QString &type, bool isList, bool isWritable, bool isPointer, int revision); + + QString name() const; + QString typeName() const; + + bool isList() const; + bool isWritable() const; + bool isPointer() const; + int revision() const; + void addToHash(QCryptographicHash &hash) const; + + QString describe(int baseIndent = 0) const; + QString toString() const; +}; + +class FakeMetaObject { + Q_DISABLE_COPY(FakeMetaObject); + +public: + typedef QSharedPointer<FakeMetaObject> Ptr; + typedef QSharedPointer<const FakeMetaObject> ConstPtr; + + class Export { + public: + Export(); + + QString package; + QString type; + ComponentVersion version; + int metaObjectRevision; + + bool isValid() const; + void addToHash(QCryptographicHash &hash) const; + + QString describe(int baseIndent = 0) const; + QString toString() const; + }; + +private: + QString m_className; + QList<Export> m_exports; + QString m_superName; + QList<FakeMetaEnum> m_enums; + QHash<QString, int> m_enumNameToIndex; + QList<FakeMetaProperty> m_props; + QHash<QString, int> m_propNameToIdx; + QList<FakeMetaMethod> m_methods; + QString m_defaultPropertyName; + QString m_attachedTypeName; + QByteArray m_fingerprint; + bool m_isSingleton; + bool m_isCreatable; + bool m_isComposite; + +public: + FakeMetaObject(); + + QString className() const; + void setClassName(const QString &name); + + void addExport(const QString &name, const QString &package, ComponentVersion version); + void setExportMetaObjectRevision(int exportIndex, int metaObjectRevision); + QList<Export> exports() const; + Export exportInPackage(const QString &package) const; + + void setSuperclassName(const QString &superclass); + QString superclassName() const; + + void addEnum(const FakeMetaEnum &fakeEnum); + int enumeratorCount() const; + int enumeratorOffset() const; + FakeMetaEnum enumerator(int index) const; + int enumeratorIndex(const QString &name) const; + + void addProperty(const FakeMetaProperty &property); + int propertyCount() const; + int propertyOffset() const; + FakeMetaProperty property(int index) const; + int propertyIndex(const QString &name) const; + + void addMethod(const FakeMetaMethod &method); + int methodCount() const; + int methodOffset() const; + FakeMetaMethod method(int index) const; + int methodIndex(const QString &name) const; // Note: Returns any method with that name in case of overloads + + QString defaultPropertyName() const; + void setDefaultPropertyName(const QString &defaultPropertyName); + + QString attachedTypeName() const; + void setAttachedTypeName(const QString &name); + QByteArray calculateFingerprint() const; + void updateFingerprint(); + QByteArray fingerprint() const; + + bool isSingleton() const; + bool isCreatable() const; + bool isComposite() const; + void setIsSingleton(bool value); + void setIsCreatable(bool value); + void setIsComposite(bool value); + + QString describe(bool printDetails = true, int baseIndent = 0) const; + QString toString() const; +}; + +} // namespace LanguageUtils + +#endif // FAKEMETAOBJECT_H diff --git a/tools/qmllint/findunqualified.cpp b/tools/qmllint/findunqualified.cpp new file mode 100644 index 0000000000..3abdfcfa53 --- /dev/null +++ b/tools/qmllint/findunqualified.cpp @@ -0,0 +1,772 @@ +/**************************************************************************** +** +** Copyright (C) 2019 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 "findunqualified.h" +#include "scopetree.h" + +#include "qmljstypedescriptionreader.h" + +#include <QFile> +#include <QDirIterator> +#include <QScopedValueRollback> + +#include <private/qqmljsast_p.h> +#include <private/qqmljslexer_p.h> +#include <private/qqmljsparser_p.h> + +QDebug &operator<<(QDebug dbg, const QQmlJS::AST::SourceLocation &loc); + +static QQmlJS::TypeDescriptionReader createReaderForFile(QString const &filename) +{ + QFile f(filename); + f.open(QFile::ReadOnly); + QQmlJS::TypeDescriptionReader reader { filename, f.readAll() }; + return reader; +} + +void FindUnqualifiedIDVisitor::enterEnvironment(ScopeType type, QString name) +{ + m_currentScope = m_currentScope->createNewChildScope(type, name); +} + +void FindUnqualifiedIDVisitor::leaveEnvironment() +{ + m_currentScope = m_currentScope->parentScope(); +} + +enum ImportVersion { FullyVersioned, PartiallyVersioned, Unversioned }; + +QStringList completeQmltypesPaths(const QString &uri, const QStringList &basePaths, int vmaj, int vmin) +{ + static const QLatin1Char Slash('/'); + static const QLatin1Char Backslash('\\'); + static const QLatin1String SlashPluginsDotQmltypes("/plugins.qmltypes"); + + const QVector<QStringRef> parts = uri.splitRef(QLatin1Char('.'), QString::SkipEmptyParts); + + QStringList qmlDirPathsPaths; + // fully & partially versioned parts + 1 unversioned for each base path + qmlDirPathsPaths.reserve(basePaths.count() * (2 * parts.count() + 1)); + + auto versionString = [](int vmaj, int vmin, ImportVersion version) + { + if (version == FullyVersioned) { + // extension with fully encoded version number (eg. MyModule.3.2) + return QString::asprintf(".%d.%d", vmaj, vmin); + } else if (version == PartiallyVersioned) { + // extension with encoded version major (eg. MyModule.3) + return QString::asprintf(".%d", vmaj); + } // else extension without version number (eg. MyModule) + return QString(); + }; + auto joinStringRefs = [](const QVector<QStringRef> &refs, const QChar &sep) + { + QString str; + for (auto it = refs.cbegin(); it != refs.cend(); ++it) { + if (it != refs.cbegin()) + str += sep; + str += *it; + } + return str; + }; + + for (int version = FullyVersioned; version <= Unversioned; ++version) { + const QString ver = versionString(vmaj, vmin, static_cast<ImportVersion>(version)); + + for (const QString &path : basePaths) { + QString dir = path; + if (!dir.endsWith(Slash) && !dir.endsWith(Backslash)) + dir += Slash; + + // append to the end + qmlDirPathsPaths += dir + joinStringRefs(parts, Slash) + ver + SlashPluginsDotQmltypes; + + if (version != Unversioned) { + // insert in the middle + for (int index = parts.count() - 2; index >= 0; --index) { + qmlDirPathsPaths += dir + joinStringRefs(parts.mid(0, index + 1), Slash) + + ver + Slash + + joinStringRefs(parts.mid(index + 1), Slash) + SlashPluginsDotQmltypes; + } + } + } + } + + return qmlDirPathsPaths; +} + +void FindUnqualifiedIDVisitor::importHelper(QString id, QString prefix, int major, int minor) +{ + QPair<QString, QString> importId { id, prefix }; + if (m_alreadySeenImports.contains(importId)) { + return; + } else { + m_alreadySeenImports.insert(importId); + } + id = id.replace(QLatin1String("/"), QLatin1String(".")); + auto qmltypesPaths = completeQmltypesPaths(id, m_qmltypeDirs, major, minor); + + QHash<QString, LanguageUtils::FakeMetaObject::ConstPtr> objects; + QList<QQmlJS::ModuleApiInfo> moduleApis; + QStringList dependencies; + for (auto const &qmltypesPath : qmltypesPaths) { + if (QFile::exists(qmltypesPath)) { + auto reader = createReaderForFile(qmltypesPath); + auto succ = reader(&objects, &moduleApis, &dependencies); + if (!succ) { + qDebug() << reader.errorMessage(); + } + break; + } + } + for (auto const &dependency : qAsConst(dependencies)) { + auto const split = dependency.split(" "); + auto const id = split.at(0); + auto const major = split.at(1).split('.').at(0).toInt(); + auto const minor = split.at(1).split('.').at(1).toInt(); + importHelper(id, QString(), major, minor); + } + // add objects + for (auto ob_it = objects.begin(); ob_it != objects.end(); ++ob_it) { + auto val = ob_it.value(); + m_exportedName2MetaObject[prefix + val->className()] = val; + for (auto export_ : val->exports()) { + m_exportedName2MetaObject[prefix + export_.type] = val; + } + for (auto enumCount = 0; enumCount < val->enumeratorCount(); ++enumCount) { + m_currentScope->insertQMLIdentifier(val->enumerator(enumCount).name()); + } + } +} + +LanguageUtils::FakeMetaObject * +FindUnqualifiedIDVisitor::localQmlFile2FakeMetaObject(QString filePath) +{ + using namespace QQmlJS::AST; + auto fake = new LanguageUtils::FakeMetaObject; + fake->setClassName(QFileInfo { filePath }.baseName()); + QFile file(filePath); + if (!file.open(QFile::ReadOnly)) { + return fake; + } + QString code = file.readAll(); + file.close(); + + QQmlJS::Engine engine; + QQmlJS::Lexer lexer(&engine); + + lexer.setCode(code, 1, true); + QQmlJS::Parser parser(&engine); + if (!parser.parse()) { + return fake; + } + QQmlJS::AST::UiProgram *program = parser.ast(); + auto header = program->headers; + while (header) { + if (auto import = cast<UiImport *>(header->headerItem)) { + if (import->version) { + QString path; + auto uri = import->importUri; + while (uri) { + path.append(uri->name); + path.append("/"); + uri = uri->next; + } + path.chop(1); + QString prefix = QLatin1String(""); + if (import->asToken.isValid()) { + prefix += import->importId + QLatin1Char('.'); + } + importHelper(path, prefix, import->version->majorVersion, import->version->minorVersion); + } + } + header = header->next; + } + auto member = program->members; + // member should be the sole element + Q_ASSERT(!member->next); + Q_ASSERT(member && member->member->kind == UiObjectMember::Kind_UiObjectDefinition); + auto definition = static_cast<UiObjectDefinition *>(member->member); + auto qualifiedId = definition->qualifiedTypeNameId; + while (qualifiedId && qualifiedId->next) { + qualifiedId = qualifiedId->next; + } + fake->setSuperclassName(qualifiedId->name.toString()); + UiObjectMemberList *initMembers = definition->initializer->members; + while (initMembers) { + switch (initMembers->member->kind) { + case UiObjectMember::Kind_UiArrayBinding: { + // nothing to do + break; + } + case UiObjectMember::Kind_UiEnumDeclaration: { + auto enumDeclaration = static_cast<UiEnumDeclaration *>(initMembers->member); + qDebug() << "enumdecl" << enumDeclaration->name; + break; + } + case UiObjectMember::Kind_UiObjectBinding: { + // nothing to do + break; + } + case UiObjectMember::Kind_UiObjectDefinition: { + // creates nothing accessible + /*auto objectDefinition = static_cast<UiObjectDefinition*>(initMembers->member); + qDebug() << "objdef" << objectDefinition->qualifiedTypeNameId->name;*/ + break; + } + case UiObjectMember::Kind_UiPublicMember: { + auto publicMember = static_cast<UiPublicMember *>(initMembers->member); + switch (publicMember->type) { + case UiPublicMember::Signal: { + UiParameterList *param = publicMember->parameters; + LanguageUtils::FakeMetaMethod method; + method.setMethodType(LanguageUtils::FakeMetaMethod::Signal); + method.setMethodName(publicMember->name.toString()); + while (param) { + method.addParameter(param->name.toString(), param->type->name.toString()); + param = param->next; + } + fake->addMethod(method); + break; + } + case UiPublicMember::Property: { + LanguageUtils::FakeMetaProperty fakeprop { publicMember->name.toString(), + publicMember->typeModifier.toString(), + false, + false, + false, + 0 }; + fake->addProperty(fakeprop); + break; + } + } + break; + } + case UiObjectMember::Kind_UiScriptBinding: { + // does not create anything new, ignore + break; + } + case UiObjectMember::Kind_UiSourceElement: { + auto sourceElement = static_cast<UiSourceElement *>(initMembers->member); + if (FunctionExpression *fexpr = sourceElement->sourceElement->asFunctionDefinition()) { + LanguageUtils::FakeMetaMethod method; + method.setMethodType(LanguageUtils::FakeMetaMethod::Method); + FormalParameterList *parameters = fexpr->formals; + while (parameters) { + method.addParameter(parameters->element->bindingIdentifier.toString(), + ""); + parameters = parameters->next; + } + fake->addMethod(method); + } else if (ClassExpression *clexpr = + sourceElement->sourceElement->asClassDefinition()) { + LanguageUtils::FakeMetaProperty prop { + clexpr->name.toString(), "", false, false, false, 1 + }; + fake->addProperty(prop); + } else if (cast<VariableStatement *>(sourceElement->sourceElement)) { + // nothing to do + } else { + qDebug() << "unsupportedd sourceElement at" << sourceElement->firstSourceLocation() + << sourceElement->sourceElement->kind; + } + break; + } + default: { + qDebug() << "unsupported element of kind" << initMembers->member->kind; + } + } + initMembers = initMembers->next; + } + return fake; +} + +void FindUnqualifiedIDVisitor::importExportedNames(QStringRef prefix, QString name) +{ + for (;;) { + auto metaObject = m_exportedName2MetaObject[m_exportedName2MetaObject.contains(name) + ? name + : prefix + QLatin1Char('.') + name]; + if (metaObject) { + auto propertyCount = metaObject->propertyCount(); + for (auto i = 0; i < propertyCount; ++i) { + m_currentScope->insertPropertyIdentifier(metaObject->property(i).name()); + } + + m_currentScope->addMethodsFromMetaObject(metaObject); + + name = metaObject->superclassName(); + if (name.isEmpty() || name == QLatin1String("QObject")) { + break; + } + } else { + qDebug() << name << "not found"; + break; + } + } +} + +void FindUnqualifiedIDVisitor::throwRecursionDepthError() +{ + return; +} + +bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::UiProgram *) +{ + enterEnvironment(ScopeType::QMLScope, "program"); + QHash<QString, LanguageUtils::FakeMetaObject::ConstPtr> objects; + QList<QQmlJS::ModuleApiInfo> moduleApis; + QStringList dependencies; + for (auto const &dir : m_qmltypeDirs) { + QDirIterator it { dir, QStringList() << QLatin1String("builtins.qmltypes"), QDir::NoFilter, + QDirIterator::Subdirectories }; + while (it.hasNext()) { + auto reader = createReaderForFile(it.next()); + auto succ = reader(&objects, &moduleApis, &dependencies); + if (!succ) { + qDebug() << reader.errorMessage(); + } + } + } + // add builtins + for (auto ob_it = objects.begin(); ob_it != objects.end(); ++ob_it) { + auto val = ob_it.value(); + for (auto export_ : val->exports()) { + m_exportedName2MetaObject[export_.type] = val; + } + for (auto enumCount = 0; enumCount < val->enumeratorCount(); ++enumCount) { + m_currentScope->insertQMLIdentifier(val->enumerator(enumCount).name()); + } + } + // add "self" (as we only ever check the first part of a qualified identifier, we get away with + // using an empty FakeMetaObject + m_exportedName2MetaObject[QFileInfo { m_filePath }.baseName()] = {}; + + // add QML builtins + m_exportedName2MetaObject["QtObject"] = {}; // QtObject contains nothing of interest + + LanguageUtils::FakeMetaObject *meta = new LanguageUtils::FakeMetaObject{}; + meta->addProperty(LanguageUtils::FakeMetaProperty {"enabled", "bool", false, false, false, 0}); + meta->addProperty(LanguageUtils::FakeMetaProperty {"ignoreUnknownSignals", "bool", false, false, false, 0}); + meta->addProperty(LanguageUtils::FakeMetaProperty {"target", "QObject", false, false, false, 0}); + m_exportedName2MetaObject["Connections"] = LanguageUtils::FakeMetaObject::ConstPtr { meta }; + return true; +} + +void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::UiProgram *) +{ + leaveEnvironment(); +} + +bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::ClassExpression *ast) +{ + enterEnvironment(ScopeType::JSFunctionScope, ast->name.toString()); + return true; +} + +void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::ClassExpression *) +{ + leaveEnvironment(); +} + +bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::ClassDeclaration *ast) +{ + enterEnvironment(ScopeType::JSFunctionScope, ast->name.toString()); + return true; +} + +void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::ClassDeclaration *) +{ + leaveEnvironment(); +} + +bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::ForStatement *) +{ + enterEnvironment(ScopeType::JSLexicalScope, "forloop"); + return true; +} + +void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::ForStatement *) +{ + leaveEnvironment(); +} + +bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::ForEachStatement *) +{ + enterEnvironment(ScopeType::JSLexicalScope, "foreachloop"); + return true; +} + +void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::ForEachStatement *) +{ + leaveEnvironment(); +} + +bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::Block *) +{ + enterEnvironment(ScopeType::JSLexicalScope, "block"); + return true; +} + +void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::Block *) +{ + leaveEnvironment(); +} + +bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::CaseBlock *) +{ + enterEnvironment(ScopeType::JSLexicalScope, "case"); + return true; +} + +void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::CaseBlock *) +{ + leaveEnvironment(); +} + +bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::Catch *) +{ + enterEnvironment(ScopeType::JSLexicalScope, "catch"); + return true; +} + +void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::Catch *) +{ + leaveEnvironment(); +} + +bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::WithStatement *withStatement) +{ + m_colorOut.write(QString::asprintf("Warning: "), Warning); + m_colorOut.write(QString::asprintf("%d:%d: with statements are strongly discouraged in QML and might cause false positives when analying unqalified identifiers\n", withStatement->firstSourceLocation().startLine, withStatement->firstSourceLocation().startColumn), Normal); + enterEnvironment(ScopeType::JSLexicalScope, "with"); + return true; +} + +void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::WithStatement *) +{ + leaveEnvironment(); +} + +bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::UiScriptBinding *uisb) +{ + using namespace QQmlJS::AST; + auto name = uisb->qualifiedId->name; + if (name == QLatin1String("id")) { + // found id + auto expstat = static_cast<ExpressionStatement *>(uisb->statement); + auto identexp = static_cast<IdentifierExpression *>(expstat->expression); + QString elementName = m_currentScope->name(); + m_qmlid2meta.insert(identexp->name.toString(), m_exportedName2MetaObject[elementName]); + if (m_currentScope->isVisualRootScope()) { + m_rootId = identexp->name.toString(); + } + } else if (name.startsWith("on") && name.size() > 2 && name.at(2).isUpper()) { + auto statement = uisb->statement; + if (statement->kind == Node::Kind::Kind_ExpressionStatement) { + if (static_cast<ExpressionStatement *>(statement)->expression->asFunctionDefinition()) { + // functions are already handled + // they do not get names inserted according to the signal, but access their formal + // parameters + return true; + } + } + QString signal = name.mid(2).toString(); + signal[0] = signal[0].toLower(); + if (!m_currentScope->methods().contains(signal)) { + qDebug() << "Info file does not contain signal" << signal; + } else { + auto method = m_currentScope->methods()[signal]; + for (auto const ¶m : method.parameterNames()) { + m_currentScope->insertSignalIdentifier(param, method, uisb->statement->firstSourceLocation()); + } + } + return true; + } + return true; +} + +bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::UiPublicMember *uipm) +{ + // property bool inactive: !active + // extract name inactive + m_currentScope->insertPropertyIdentifier(uipm->name.toString()); + return true; +} + +bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::IdentifierExpression *idexp) +{ + auto name = idexp->name; + if (!m_exportedName2MetaObject.contains(name.toString())) { + m_currentScope->addIdToAccssedIfNotInParentScopes( + { name.toString(), idexp->firstSourceLocation() }); + } + return true; +} + +FindUnqualifiedIDVisitor::FindUnqualifiedIDVisitor(QStringList const &qmltypeDirs, + const QString &code, const QString &fileName) + : m_rootScope(new ScopeTree { ScopeType::JSFunctionScope, "global" }), + m_currentScope(m_rootScope.get()), + m_qmltypeDirs(qmltypeDirs), + m_code(code), + m_rootId(QLatin1String("<id>")), + m_filePath(fileName) +{ + // setup color output + m_colorOut.insertColorMapping(Error, ColorOutput::RedForeground); + m_colorOut.insertColorMapping(Warning, ColorOutput::PurpleForeground); + m_colorOut.insertColorMapping(Info, ColorOutput::BlueForeground); + m_colorOut.insertColorMapping(Normal, ColorOutput::DefaultColor); + m_colorOut.insertColorMapping(Hint, ColorOutput::GreenForeground); + QLatin1String jsGlobVars[] = { + QLatin1String ("Array"), QLatin1String("Boolean"), QLatin1String("Date"), QLatin1String("Function"), QLatin1String("Math"), QLatin1String("Number"), QLatin1String("Object"), QLatin1String("RegExp"), QLatin1String("String"), + QLatin1String("Error"), QLatin1String("EvalError"), QLatin1String("RangeError"), QLatin1String("ReferenceError"), QLatin1String("SyntaxError"), QLatin1String("TypeError"), QLatin1String("URIError"), + QLatin1String("encodeURI"), QLatin1String("encodeURIComponent"), QLatin1String("decodeURI"), QLatin1String("decodeURIComponent"), QLatin1String("escape"), QLatin1String("unescape"), + QLatin1String("isFinite"), QLatin1String("isNanN"), QLatin1String("parseFloat"), QLatin1String("parseInt"), + QLatin1String("eval"), QLatin1String("console"), QLatin1String("print"), QLatin1String("gc"), + QLatin1String("qsTr"), QLatin1String("qsTrId"), QLatin1String("QT_TR_NOOP"), QLatin1String("QT_TRANSLATE_NOOP"), QLatin1String("QT_TRID_NOOP"), + QLatin1String("XMLHttpRequest"), QLatin1String("JSON"), QLatin1String("Promise"), + QLatin1String("undefined") + }; + for (const auto& jsGlobVar: jsGlobVars) + m_currentScope->insertJSIdentifier(jsGlobVar, QQmlJS::AST::VariableScope::Const); +} + +FindUnqualifiedIDVisitor::~FindUnqualifiedIDVisitor() = default; + +bool FindUnqualifiedIDVisitor::check() +{ + // now that all ids are known, revisit any Connections whose target were perviously unknown + for (auto const& outstandingConnection: m_outstandingConnections) { + auto metaObject = m_qmlid2meta[outstandingConnection.targetName]; + outstandingConnection.scope->addMethodsFromMetaObject(metaObject); + QScopedValueRollback<ScopeTree*> rollback(m_currentScope, outstandingConnection.scope); + outstandingConnection.uiod->initializer->accept(this); + } + return m_rootScope->recheckIdentifiers(m_code, m_qmlid2meta, m_rootScope.get(), m_rootId, m_colorOut); +} + +bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::VariableDeclarationList *vdl) +{ + while (vdl) { + m_currentScope->insertJSIdentifier(vdl->declaration->bindingIdentifier.toString(), + vdl->declaration->scope); + vdl = vdl->next; + } + return true; +} + +void FindUnqualifiedIDVisitor::visitFunctionExpressionHelper(QQmlJS::AST::FunctionExpression *fexpr) +{ + using namespace QQmlJS::AST; + if (!fexpr->name.isEmpty()) { + auto name = fexpr->name.toString(); + if (m_currentScope->scopeType() == ScopeType::QMLScope) { + m_currentScope->insertQMLIdentifier(name); + } else { + m_currentScope->insertJSIdentifier(name, VariableScope::Const); + } + } + // qDebug() << fexpr->firstSourceLocation() << "function expression" << fexpr->name; + QString name = fexpr->name.toString(); + if (name.isEmpty()) + name = "<anon>"; + enterEnvironment(ScopeType::JSFunctionScope, name); +} + +bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::FunctionExpression *fexpr) +{ + visitFunctionExpressionHelper(fexpr); + return true; +} + +void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::FunctionExpression *) +{ + leaveEnvironment(); +} + +bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::FunctionDeclaration *fdecl) +{ + visitFunctionExpressionHelper(fdecl); + return true; +} + +void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::FunctionDeclaration *) +{ + leaveEnvironment(); +} + +bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::FormalParameterList *fpl) +{ + for (auto const &boundName : fpl->boundNames()) { + m_currentScope->insertJSIdentifier(boundName.id, QQmlJS::AST::VariableScope::Const); + } + return true; +} + +bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::UiImport *import) +{ + // construct path + QString prefix = QLatin1String(""); + if (import->asToken.isValid()) { + prefix += import->importId + QLatin1Char('.'); + } + auto dirname = import->fileName.toString(); + if (!dirname.isEmpty()) { + QFileInfo info { dirname }; + if (info.isRelative()) { + dirname = QDir(QFileInfo { m_filePath }.path()).filePath(dirname); + } + QDirIterator it { dirname, QStringList() << QLatin1String("*.qml"), QDir::NoFilter }; + while (it.hasNext()) { + LanguageUtils::FakeMetaObject *fake = localQmlFile2FakeMetaObject(it.next()); + m_exportedName2MetaObject.insert( + fake->className(), QSharedPointer<const LanguageUtils::FakeMetaObject>(fake)); + } + } + QString path {}; + if (!import->importId.isEmpty()) { + m_qmlid2meta.insert(import->importId.toString(), {}); // TODO: do not put imported ids into the same space as qml IDs + } + if (import->version) { + auto uri = import->importUri; + while (uri) { + path.append(uri->name); + path.append("/"); + uri = uri->next; + } + path.chop(1); + + importHelper(path, prefix, import->version->majorVersion, import->version->minorVersion); + } + return true; +} + +bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::UiEnumDeclaration *uied) +{ + m_currentScope->insertQMLIdentifier(uied->name.toString()); + return true; +} + +bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::UiObjectBinding *uiob) +{ + // property QtObject __styleData: QtObject {...} + m_currentScope->insertPropertyIdentifier(uiob->qualifiedId->name.toString()); + QString name {}; + auto id = uiob->qualifiedTypeNameId; + QStringRef prefix = uiob->qualifiedTypeNameId->name; + while (id) { + name += id->name.toString() + QLatin1Char('.'); + id = id->next; + } + name.chop(1); + enterEnvironment(ScopeType::QMLScope, name); + if (name == QLatin1String("Component") || name == QLatin1String("QtObject")) // there is no typeinfo for Component and QtObject, but they also have no interesting properties + return true; + importExportedNames(prefix, name); + return true; +} + +void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::UiObjectBinding *) +{ + leaveEnvironment(); +} + +bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::UiObjectDefinition *uiod) +{ + QString name {}; + auto id = uiod->qualifiedTypeNameId; + QStringRef prefix = uiod->qualifiedTypeNameId->name; + while (id) { + name += id->name.toString() + QLatin1Char('.'); + id = id->next; + } + name.chop(1); + enterEnvironment(ScopeType::QMLScope, name); + if (name.isLower()) + return false; // Ignore grouped properties for now + if (name == QLatin1String("Component") || name == QLatin1String("QtObject")) // there is no typeinfo for Component + return true; + importExportedNames(prefix, name); + if (name.endsWith("Connections")) { + QString target; + auto member = uiod->initializer->members; + while (member) { + if (member->member->kind == QQmlJS::AST::Node::Kind_UiScriptBinding) { + auto asBinding = static_cast<QQmlJS::AST::UiScriptBinding*>(member->member); + if (asBinding->qualifiedId->name == QLatin1String("target")) { + if (asBinding->statement->kind == QQmlJS::AST::Node::Kind_ExpressionStatement) { + auto expr = static_cast<QQmlJS::AST::ExpressionStatement*>(asBinding->statement)->expression; + if (auto idexpr = QQmlJS::AST::cast<QQmlJS::AST::IdentifierExpression*>(expr)) { + target = idexpr->name.toString(); + } else { + // more complex expressions are not supported + } + } + break; + } + } + member = member->next; + } + LanguageUtils::FakeMetaObject::ConstPtr metaObject {}; + if (target.isEmpty()) { + // no target set, connection comes from parentF + ScopeTree* scope = m_currentScope; + do { + scope = scope->parentScope(); // TODO: rename method + } while (scope->scopeType() != ScopeType::QMLScope); + auto metaObject = m_exportedName2MetaObject[scope->name()]; + } else { + // there was a target, check if we already can find it + auto metaObjectIt = m_qmlid2meta.find(target); + if (metaObjectIt != m_qmlid2meta.end()) { + metaObject = *metaObjectIt; + } else { + m_outstandingConnections.push_back({target, m_currentScope, uiod}); + return false; // visit children later once target is known + } + } + m_currentScope->addMethodsFromMetaObject(metaObject); + } + return true; +} + +void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::UiObjectDefinition *) +{ + leaveEnvironment(); +} + +QDebug &operator<<(QDebug dbg, const QQmlJS::AST::SourceLocation &loc) +{ + QDebugStateSaver saver(dbg); + dbg.nospace() << loc.startLine; + dbg.nospace() << ":"; + dbg.nospace() << loc.startColumn; + return dbg.maybeSpace(); +} diff --git a/tools/qmllint/findunqualified.h b/tools/qmllint/findunqualified.h new file mode 100644 index 0000000000..937194d142 --- /dev/null +++ b/tools/qmllint/findunqualified.h @@ -0,0 +1,129 @@ +/**************************************************************************** +** +** Copyright (C) 2019 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$ +** +****************************************************************************/ + +#ifndef FINDUNQUALIFIED_H +#define FINDUNQUALIFIED_H + +#include "qmljstypedescriptionreader.h" +#include "qcoloroutput_p.h" + +#include <private/qqmljsastvisitor_p.h> +#include <private/qqmljsast_p.h> + +#include <QScopedPointer> + +class ScopeTree; +enum class ScopeType; + +class FindUnqualifiedIDVisitor : public QQmlJS::AST::Visitor { + +public: + explicit FindUnqualifiedIDVisitor(QStringList const &qmltypeDirs, const QString& code, const QString& fileName); + ~FindUnqualifiedIDVisitor() override; + bool check(); + +private: + QScopedPointer<ScopeTree> m_rootScope; + ScopeTree *m_currentScope; + QHash<QString, LanguageUtils::FakeMetaObject::ConstPtr> m_exportedName2MetaObject; + QStringList m_qmltypeDirs; + const QString& m_code; + QHash<QString, LanguageUtils::FakeMetaObject::ConstPtr> m_qmlid2meta; + QString m_rootId; + QString m_filePath; + QSet<QPair<QString, QString>> m_alreadySeenImports; + ColorOutput m_colorOut; + + struct OutstandingConnection {QString targetName; ScopeTree *scope; QQmlJS::AST::UiObjectDefinition *uiod;}; + + QVarLengthArray<OutstandingConnection, 3> m_outstandingConnections; // Connections whose target we have not encountered + + void enterEnvironment(ScopeType type, QString name); + void leaveEnvironment(); + void importHelper(QString id, QString prefix, int major, int minor); + LanguageUtils::FakeMetaObject* localQmlFile2FakeMetaObject(QString filePath); + + + void importExportedNames(QStringRef prefix, QString name); + + void throwRecursionDepthError() override; + + // start block/scope handling + bool visit(QQmlJS::AST::UiProgram *ast) override; + void endVisit(QQmlJS::AST::UiProgram *ast) override; + + bool visit(QQmlJS::AST::ClassExpression *ast) override; + void endVisit(QQmlJS::AST::ClassExpression *ast) override; + + bool visit(QQmlJS::AST::ClassDeclaration *ast) override; + void endVisit(QQmlJS::AST::ClassDeclaration *ast) override; + + bool visit(QQmlJS::AST::ForStatement *ast) override; + void endVisit(QQmlJS::AST::ForStatement *ast) override; + + bool visit(QQmlJS::AST::ForEachStatement *ast) override; + void endVisit(QQmlJS::AST::ForEachStatement *ast) override; + + bool visit(QQmlJS::AST::Block *ast) override; + void endVisit(QQmlJS::AST::Block *ast) override; + + bool visit(QQmlJS::AST::CaseBlock *ast) override; + void endVisit(QQmlJS::AST::CaseBlock *ast) override; + + bool visit(QQmlJS::AST::Catch *ast) override; + void endVisit(QQmlJS::AST::Catch *ast) override; + + bool visit(QQmlJS::AST::WithStatement *withStatement) override; + void endVisit(QQmlJS::AST::WithStatement *ast) override; + + void visitFunctionExpressionHelper(QQmlJS::AST::FunctionExpression *fexpr); + bool visit(QQmlJS::AST::FunctionExpression *fexpr) override; + void endVisit(QQmlJS::AST::FunctionExpression *fexpr) override; + + bool visit(QQmlJS::AST::FunctionDeclaration *fdecl) override; + void endVisit(QQmlJS::AST::FunctionDeclaration *fdecl) override; + /* --- end block handling --- */ + + bool visit(QQmlJS::AST::VariableDeclarationList *vdl) override; + bool visit(QQmlJS::AST::FormalParameterList *fpl) override; + + bool visit(QQmlJS::AST::UiImport *import) override; + bool visit(QQmlJS::AST::UiEnumDeclaration *uied) override; + bool visit(QQmlJS::AST::UiObjectBinding *uiob) override; + void endVisit(QQmlJS::AST::UiObjectBinding *uiob) override; + bool visit(QQmlJS::AST::UiObjectDefinition *uiod) override; + void endVisit(QQmlJS::AST::UiObjectDefinition *) override; + bool visit(QQmlJS::AST::UiScriptBinding *uisb) override; + bool visit(QQmlJS::AST::UiPublicMember *uipm) override; + + // expression handling + bool visit(QQmlJS::AST::IdentifierExpression *idexp) override; +}; + + +#endif // FINDUNQUALIFIED_H diff --git a/tools/qmllint/main.cpp b/tools/qmllint/main.cpp index bf0ceb54b7..235ec16c6e 100644 --- a/tools/qmllint/main.cpp +++ b/tools/qmllint/main.cpp @@ -34,12 +34,19 @@ #endif #include <QCoreApplication> -#include <private/qv4staticvalue_p.h> +#ifndef QT_BOOTSTRAPPED +#include <QLibraryInfo> +#endif + #include <private/qqmljslexer_p.h> #include <private/qqmljsparser_p.h> #include <private/qqmljsengine_p.h> +#include <private/qqmljsastvisitor_p.h> +#include <private/qqmljsast_p.h> + +#include "findunqualified.h" -static bool lint_file(const QString &filename, bool silent) +static bool lint_file(const QString &filename, const bool silent, const bool warnUnqualied, QStringList const &qmltypeDirs) { QFile file(filename); if (!file.open(QFile::ReadOnly)) { @@ -67,6 +74,13 @@ static bool lint_file(const QString &filename, bool silent) } } + if (success && !isJavaScript && warnUnqualied) { + auto root = parser.rootNode(); + FindUnqualifiedIDVisitor v { qmltypeDirs, code, filename}; + root->accept(&v); + success = v.check(); + } + return success; } @@ -80,8 +94,20 @@ int main(int argv, char *argc[]) parser.setApplicationDescription(QLatin1String("QML syntax verifier")); parser.addHelpOption(); parser.addVersionOption(); + QCommandLineOption silentOption(QStringList() << "s" << "silent", QLatin1String("Don't output syntax errors")); parser.addOption(silentOption); + + QCommandLineOption checkUnqualified(QStringList() << "U" << "check-unqualified", QLatin1String("Warn about unqualified identifiers")); + parser.addOption(checkUnqualified); + + QCommandLineOption qmltypesDirsOption( + QStringList() << "I" + << "qmldirs", + QLatin1String("Look for qmltypes files in specified directory"), + QLatin1String("directory")); + parser.addOption(qmltypesDirsOption); + parser.addPositionalArgument(QLatin1String("files"), QLatin1String("list of qml or js files to verify")); parser.process(app); @@ -92,8 +118,18 @@ int main(int argv, char *argc[]) } bool silent = parser.isSet(silentOption); + bool warnUnqualified = parser.isSet(checkUnqualified); + // use host qml import path as a sane default if nothing else has been provided + QStringList qmltypeDirs = parser.isSet(qmltypesDirsOption) ? parser.values(qmltypesDirsOption) +#ifndef QT_BOOTSTRAPPED + : QStringList{QLibraryInfo::location(QLibraryInfo::Qml2ImportsPath)}; +#else + : QStringList{}; +#endif #else bool silent = false; + bool warnUnqualified = false; + QStringList qmltypeDirs {}; #endif bool success = true; #if QT_CONFIG(commandlineparser) @@ -102,7 +138,7 @@ int main(int argv, char *argc[]) const auto arguments = app.arguments(); for (const QString &filename : arguments) #endif - success &= lint_file(filename, silent); + success &= lint_file(filename, silent, warnUnqualified, qmltypeDirs); return success ? 0 : -1; } diff --git a/tools/qmllint/qcoloroutput.cpp b/tools/qmllint/qcoloroutput.cpp new file mode 100644 index 0000000000..d2e723700a --- /dev/null +++ b/tools/qmllint/qcoloroutput.cpp @@ -0,0 +1,342 @@ +/**************************************************************************** +** +** Copyright (C) 2019 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 <QFile> +#include <QHash> +#include <QTextCodec> + +#ifndef Q_OS_WIN +#include <unistd.h> +#endif + +#include "qcoloroutput_p.h" + +class ColorOutputPrivate +{ +public: + ColorOutputPrivate() : currentColorID(-1) + + { + /* - QIODevice::Unbuffered because we want it to appear when the user actually calls, performance + * is considered of lower priority. + */ + m_out.open(stderr, QIODevice::WriteOnly | QIODevice::Unbuffered); + + coloringEnabled = isColoringPossible(); + } + + ColorOutput::ColorMapping colorMapping; + int currentColorID; + bool coloringEnabled; + + static const char *const foregrounds[]; + static const char *const backgrounds[]; + + inline void write(const QString &msg) + { + m_out.write(msg.toLocal8Bit()); + } + + static QString escapeCode(const QString &in) + { + QString result; + result.append(QChar(0x1B)); + result.append(QLatin1Char('[')); + result.append(in); + result.append(QLatin1Char('m')); + return result; + } + +private: + QFile m_out; + + /*! + Returns true if it's suitable to send colored output to \c stderr. + */ + inline bool isColoringPossible() const + { +# if defined(Q_OS_WIN) + /* Windows doesn't at all support ANSI escape codes, unless + * the user install a "device driver". See the Wikipedia links in the + * class documentation for details. */ + return false; +# else + /* We use QFile::handle() to get the file descriptor. It's a bit unsure + * whether it's 2 on all platforms and in all cases, so hopefully this layer + * of abstraction helps handle such cases. */ + return isatty(m_out.handle()); +# endif + } +}; + +const char *const ColorOutputPrivate::foregrounds[] = +{ + "0;30", + "0;34", + "0;32", + "0;36", + "0;31", + "0;35", + "0;33", + "0;37", + "1;30", + "1;34", + "1;32", + "1;36", + "1;31", + "1;35", + "1;33", + "1;37" +}; + +const char *const ColorOutputPrivate::backgrounds[] = +{ + "0;40", + "0;44", + "0;42", + "0;46", + "0;41", + "0;45", + "0;43" +}; + +/*! + \class ColorOutput + \since 4.4 + \nonreentrant + \brief Outputs colored messages to \c stderr. + \internal + + ColorOutput is a convenience class for outputting messages to \c + stderr using color escape codes, as mandated in ECMA-48. ColorOutput + will only color output when it is detected to be suitable. For + instance, if \c stderr is detected to be attached to a file instead + of a TTY, no coloring will be done. + + ColorOutput does its best attempt. but it is generally undefined + what coloring or effect the various coloring flags has. It depends + strongly on what terminal software that is being used. + + When using `echo -e 'my escape sequence'`, \c{\033} works as an + initiator but not when printing from a C++ program, despite having + escaped the backslash. That's why we below use characters with + value 0x1B. + + It can be convenient to subclass ColorOutput with a private scope, + such that the functions are directly available in the class using + it. + + \section1 Usage + + To output messages, call write() or writeUncolored(). write() takes + as second argument an integer, which ColorOutput uses as a lookup + key to find the color it should color the text in. The mapping from + keys to colors is done using insertMapping(). Typically this is used + by having enums for the various kinds of messages, which + subsequently are registered. + + \code + enum MyMessage + { + Error, + Important + }; + + ColorOutput output; + output.insertMapping(Error, ColorOutput::RedForeground); + output.insertMapping(Import, ColorOutput::BlueForeground); + + output.write("This is important", Important); + output.write("Jack, I'm only the selected official!", Error); + \endcode + + \sa {http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x329.html}{Bash Prompt HOWTO, 6.1. Colors}, + {http://linuxgazette.net/issue51/livingston-blade.html}{Linux Gazette, Tweaking Eterm, Edward Livingston-Blade}, + {http://www.ecma-international.org/publications/standards/Ecma-048.htm}{Standard ECMA-48, Control Functions for Coded Character Sets, ECMA International}, + {http://en.wikipedia.org/wiki/ANSI_escape_code}{Wikipedia, ANSI escape code}, + {http://linuxgazette.net/issue65/padala.html}{Linux Gazette, So You Like Color!, Pradeep Padala} + */ + +/*! + \enum ColorOutput::ColorCodeComponent + \value BlackForeground + \value BlueForeground + \value GreenForeground + \value CyanForeground + \value RedForeground + \value PurpleForeground + \value BrownForeground + \value LightGrayForeground + \value DarkGrayForeground + \value LightBlueForeground + \value LightGreenForeground + \value LightCyanForeground + \value LightRedForeground + \value LightPurpleForeground + \value YellowForeground + \value WhiteForeground + \value BlackBackground + \value BlueBackground + \value GreenBackground + \value CyanBackground + \value RedBackground + \value PurpleBackground + \value BrownBackground + + \value DefaultColor ColorOutput performs no coloring. This typically + means black on white or white on black, depending + on the settings of the user's terminal. + */ + +/*! + Sets the color mapping to be \a cMapping. + + Negative values are disallowed. + + \sa colorMapping(), insertMapping() + */ +void ColorOutput::setColorMapping(const ColorMapping &cMapping) +{ + d->colorMapping = cMapping; +} + +/*! + Returns the color mappings in use. + + \sa setColorMapping(), insertMapping() + */ +ColorOutput::ColorMapping ColorOutput::colorMapping() const +{ + return d->colorMapping; +} + +/*! + Constructs a ColorOutput instance, ready for use. + */ +ColorOutput::ColorOutput() : d(new ColorOutputPrivate()) +{ +} + +ColorOutput::~ColorOutput() = default; // must be here so that QScopedPointer has access to the complete type + +/*! + Sends \a message to \c stderr, using the color looked up in colorMapping() using \a colorID. + + If \a color isn't available in colorMapping(), result and behavior is undefined. + + If \a colorID is 0, which is the default value, the previously used coloring is used. ColorOutput + is initialized to not color at all. + + If \a message is empty, effects are undefined. + + \a message will be printed as is. For instance, no line endings will be inserted. + */ +void ColorOutput::write(const QString &message, int colorID) +{ + d->write(colorify(message, colorID)); +} + +/*! + Writes \a message to \c stderr as if for instance + QTextStream would have been used, and adds a line ending at the end. + + This function can be practical to use such that one can use ColorOutput for all forms of writing. + */ +void ColorOutput::writeUncolored(const QString &message) +{ + d->write(message + QLatin1Char('\n')); +} + +/*! + Treats \a message and \a colorID identically to write(), but instead of writing + \a message to \c stderr, it is prepared for being written to \c stderr, but is then + returned. + + This is useful when the colored string is inserted into a translated string(dividing + the string into several small strings prevents proper translation). + */ +QString ColorOutput::colorify(const QString &message, int colorID) const +{ + Q_ASSERT_X(colorID == -1 || d->colorMapping.contains(colorID), Q_FUNC_INFO, + qPrintable(QString::fromLatin1("There is no color registered by id %1").arg(colorID))); + Q_ASSERT_X(!message.isEmpty(), Q_FUNC_INFO, "It makes no sense to attempt to print an empty string."); + + if (colorID != -1) + d->currentColorID = colorID; + + if (d->coloringEnabled && colorID != -1) + { + const int color(d->colorMapping.value(colorID)); + + /* If DefaultColor is set, we don't want to color it. */ + if (color & DefaultColor) + return message; + + const int foregroundCode = (int(color) & ForegroundMask) >> ForegroundShift; + const int backgroundCode = (int(color) & BackgroundMask) >> BackgroundShift; + QString finalMessage; + bool closureNeeded = false; + + if (foregroundCode) + { + finalMessage.append(ColorOutputPrivate::escapeCode(QLatin1String(ColorOutputPrivate::foregrounds[foregroundCode - 1]))); + closureNeeded = true; + } + + if (backgroundCode) + { + finalMessage.append(ColorOutputPrivate::escapeCode(QLatin1String(ColorOutputPrivate::backgrounds[backgroundCode - 1]))); + closureNeeded = true; + } + + finalMessage.append(message); + + if (closureNeeded) + { + finalMessage.append(QChar(0x1B)); + finalMessage.append(QLatin1String("[0m")); + } + + return finalMessage; + } + else + return message; +} + +/*! + Adds a color mapping from \a colorID to \a colorCode, for this ColorOutput instance. + + This is a convenience function for creating a ColorOutput::ColorMapping instance and + calling setColorMapping(). + + \sa colorMapping(), setColorMapping() + */ +void ColorOutput::insertColorMapping(int colorID, const ColorCode colorCode) +{ + d->colorMapping.insert(colorID, colorCode); +} diff --git a/tools/qmllint/qcoloroutput_p.h b/tools/qmllint/qcoloroutput_p.h new file mode 100644 index 0000000000..710bf5db74 --- /dev/null +++ b/tools/qmllint/qcoloroutput_p.h @@ -0,0 +1,110 @@ +/**************************************************************************** +** +** Copyright (C) 2019 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$ +** +****************************************************************************/ + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. + +#ifndef QCOLOROUTPUT_P_H +#define QCOLOROUTPUT_P_H + +#include <QtCore/QtGlobal> +#include <QtCore/QHash> +#include <QScopedPointer> + +class ColorOutputPrivate; + +class ColorOutput +{ + enum + { + ForegroundShift = 10, + BackgroundShift = 20, + SpecialShift = 20, + ForegroundMask = 0x1f << ForegroundShift, + BackgroundMask = 0x7 << BackgroundShift + }; + +public: + enum ColorCodeComponent + { + BlackForeground = 1 << ForegroundShift, + BlueForeground = 2 << ForegroundShift, + GreenForeground = 3 << ForegroundShift, + CyanForeground = 4 << ForegroundShift, + RedForeground = 5 << ForegroundShift, + PurpleForeground = 6 << ForegroundShift, + BrownForeground = 7 << ForegroundShift, + LightGrayForeground = 8 << ForegroundShift, + DarkGrayForeground = 9 << ForegroundShift, + LightBlueForeground = 10 << ForegroundShift, + LightGreenForeground = 11 << ForegroundShift, + LightCyanForeground = 12 << ForegroundShift, + LightRedForeground = 13 << ForegroundShift, + LightPurpleForeground = 14 << ForegroundShift, + YellowForeground = 15 << ForegroundShift, + WhiteForeground = 16 << ForegroundShift, + + BlackBackground = 1 << BackgroundShift, + BlueBackground = 2 << BackgroundShift, + GreenBackground = 3 << BackgroundShift, + CyanBackground = 4 << BackgroundShift, + RedBackground = 5 << BackgroundShift, + PurpleBackground = 6 << BackgroundShift, + BrownBackground = 7 << BackgroundShift, + DefaultColor = 1 << SpecialShift + }; + + typedef QFlags<ColorCodeComponent> ColorCode; + typedef QHash<int, ColorCode> ColorMapping; + + ColorOutput(); + ~ColorOutput(); + + void setColorMapping(const ColorMapping &cMapping); + ColorMapping colorMapping() const; + void insertColorMapping(int colorID, const ColorCode colorCode); + + void writeUncolored(const QString &message); + void write(const QString &message, int color = -1); + QString colorify(const QString &message, int color = -1) const; + +private: + QScopedPointer<ColorOutputPrivate> d; + Q_DISABLE_COPY(ColorOutput) +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(ColorOutput::ColorCode) + +#endif diff --git a/tools/qmllint/qmljstypedescriptionreader.cpp b/tools/qmllint/qmljstypedescriptionreader.cpp new file mode 100644 index 0000000000..542cdf99eb --- /dev/null +++ b/tools/qmllint/qmljstypedescriptionreader.cpp @@ -0,0 +1,704 @@ +/**************************************************************************** +** +** Copyright (C) 2019 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 "qmljstypedescriptionreader.h" + +#include <private/qqmljsparser_p.h> +#include <private/qqmljslexer_p.h> +#include <private/qqmljsengine_p.h> + +#include <QDir> + +#define QTC_ASSERT_STRINGIFY_HELPER(x) #x +#define QTC_ASSERT_STRINGIFY(x) QTC_ASSERT_STRINGIFY_HELPER(x) +#define QTC_ASSERT_STRING(cond) qDebug() << (\ + "\"" cond"\" in file " __FILE__ ", line " QTC_ASSERT_STRINGIFY(__LINE__)) +#define QTC_ASSERT(cond, action) if (Q_LIKELY(cond)) {} else { QTC_ASSERT_STRING(#cond); action; } do {} while (0) + +using namespace QQmlJS; +using namespace QQmlJS::AST; +using namespace LanguageUtils; + +QString toString(const AST::UiQualifiedId *qualifiedId, QChar delimiter = QLatin1Char('.')) +{ + QString result; + + for (const UiQualifiedId *iter = qualifiedId; iter; iter = iter->next) { + if (iter != qualifiedId) + result += delimiter; + + result += iter->name; + } + + return result; +} + +TypeDescriptionReader::TypeDescriptionReader(const QString &fileName, const QString &data) + : _fileName (fileName), _source(data), _objects(0) +{ +} + +TypeDescriptionReader::~TypeDescriptionReader() +{ +} + +bool TypeDescriptionReader::operator()( + QHash<QString, FakeMetaObject::ConstPtr> *objects, + QList<ModuleApiInfo> *moduleApis, + QStringList *dependencies) +{ + Engine engine; + + Lexer lexer(&engine); + Parser parser(&engine); + + lexer.setCode(_source, /*line = */ 1, /*qmlMode = */true); + + if (!parser.parse()) { + _errorMessage = QString::fromLatin1("%1:%2: %3").arg( + QString::number(parser.errorLineNumber()), + QString::number(parser.errorColumnNumber()), + parser.errorMessage()); + return false; + } + + _objects = objects; + _moduleApis = moduleApis; + _dependencies = dependencies; + readDocument(parser.ast()); + + return _errorMessage.isEmpty(); +} + +QString TypeDescriptionReader::errorMessage() const +{ + return _errorMessage; +} + +QString TypeDescriptionReader::warningMessage() const +{ + return _warningMessage; +} + +void TypeDescriptionReader::readDocument(UiProgram *ast) +{ + if (!ast) { + addError(SourceLocation(), tr("Could not parse document.")); + return; + } + + if (!ast->headers || ast->headers->next || !AST::cast<AST::UiImport *>(ast->headers->headerItem)) { + addError(SourceLocation(), tr("Expected a single import.")); + return; + } + + UiImport *import = AST::cast<AST::UiImport *>(ast->headers->headerItem); + if (toString(import->importUri) != QLatin1String("QtQuick.tooling")) { + addError(import->importToken, tr("Expected import of QtQuick.tooling.")); + return; + } + + ComponentVersion version; + const QString versionString = _source.mid(import->versionToken.offset, import->versionToken.length); + const int dotIdx = versionString.indexOf(QLatin1Char('.')); + if (dotIdx != -1) { + version = ComponentVersion(versionString.leftRef(dotIdx).toInt(), + versionString.midRef(dotIdx + 1).toInt()); + } + if (version.majorVersion() != 1) { + addError(import->versionToken, tr("Major version different from 1 not supported.")); + return; + } + + if (!ast->members || !ast->members->member || ast->members->next) { + addError(SourceLocation(), tr("Expected document to contain a single object definition.")); + return; + } + + UiObjectDefinition *module = AST::cast<UiObjectDefinition *>(ast->members->member); + if (!module) { + addError(SourceLocation(), tr("Expected document to contain a single object definition.")); + return; + } + + if (toString(module->qualifiedTypeNameId) != QLatin1String("Module")) { + addError(SourceLocation(), tr("Expected document to contain a Module {} member.")); + return; + } + + readModule(module); +} + +void TypeDescriptionReader::readModule(UiObjectDefinition *ast) +{ + for (UiObjectMemberList *it = ast->initializer->members; it; it = it->next) { + UiObjectMember *member = it->member; + UiObjectDefinition *component = AST::cast<UiObjectDefinition *>(member); + + UiScriptBinding *script = AST::cast<UiScriptBinding *>(member); + if (script && (toString(script->qualifiedId) == QStringLiteral("dependencies"))) { + readDependencies(script); + continue; + } + + QString typeName; + if (component) + typeName = toString(component->qualifiedTypeNameId); + + if (!component || (typeName != QLatin1String("Component") && typeName != QLatin1String("ModuleApi"))) { + continue; + } + + if (typeName == QLatin1String("Component")) + readComponent(component); + else if (typeName == QLatin1String("ModuleApi")) + readModuleApi(component); + } +} + +void TypeDescriptionReader::addError(const SourceLocation &loc, const QString &message) +{ + _errorMessage += QString::fromLatin1("%1:%2:%3: %4\n").arg( + QDir::toNativeSeparators(_fileName), + QString::number(loc.startLine), + QString::number(loc.startColumn), + message); +} + +void TypeDescriptionReader::addWarning(const SourceLocation &loc, const QString &message) +{ + _warningMessage += QString::fromLatin1("%1:%2:%3: %4\n").arg( + QDir::toNativeSeparators(_fileName), + QString::number(loc.startLine), + QString::number(loc.startColumn), + message); +} + +void TypeDescriptionReader::readDependencies(UiScriptBinding *ast) +{ + ExpressionStatement *stmt = AST::cast<ExpressionStatement*>(ast->statement); + if (!stmt) { + addError(ast->statement->firstSourceLocation(), tr("Expected dependency definitions")); + return; + } + ArrayPattern *exp = AST::cast<ArrayPattern *>(stmt->expression); + if (!exp) { + addError(stmt->expression->firstSourceLocation(), tr("Expected dependency definitions")); + return; + } + for (PatternElementList *l = exp->elements; l; l = l->next) { + //StringLiteral *str = AST::cast<StringLiteral *>(l->element->initializer); + StringLiteral *str = AST::cast<StringLiteral *>(l->element->initializer); + *_dependencies << str->value.toString(); + } +} + +void TypeDescriptionReader::readComponent(UiObjectDefinition *ast) +{ + FakeMetaObject::Ptr fmo(new FakeMetaObject); + + for (UiObjectMemberList *it = ast->initializer->members; it; it = it->next) { + UiObjectMember *member = it->member; + UiObjectDefinition *component = AST::cast<UiObjectDefinition *>(member); + UiScriptBinding *script = AST::cast<UiScriptBinding *>(member); + if (component) { + QString name = toString(component->qualifiedTypeNameId); + if (name == QLatin1String("Property")) + readProperty(component, fmo); + else if (name == QLatin1String("Method") || name == QLatin1String("Signal")) + readSignalOrMethod(component, name == QLatin1String("Method"), fmo); + else if (name == QLatin1String("Enum")) + readEnum(component, fmo); + else + addWarning(component->firstSourceLocation(), + tr("Expected only Property, Method, Signal and Enum object definitions, not \"%1\".") + .arg(name)); + } else if (script) { + QString name = toString(script->qualifiedId); + if (name == QLatin1String("name")) { + fmo->setClassName(readStringBinding(script)); + } else if (name == QLatin1String("prototype")) { + fmo->setSuperclassName(readStringBinding(script)); + } else if (name == QLatin1String("defaultProperty")) { + fmo->setDefaultPropertyName(readStringBinding(script)); + } else if (name == QLatin1String("exports")) { + readExports(script, fmo); + } else if (name == QLatin1String("exportMetaObjectRevisions")) { + readMetaObjectRevisions(script, fmo); + } else if (name == QLatin1String("attachedType")) { + fmo->setAttachedTypeName(readStringBinding(script)); + } else if (name == QLatin1String("isSingleton")) { + fmo->setIsSingleton(readBoolBinding(script)); + } else if (name == QLatin1String("isCreatable")) { + fmo->setIsCreatable(readBoolBinding(script)); + } else if (name == QLatin1String("isComposite")) { + fmo->setIsComposite(readBoolBinding(script)); + } else { + addWarning(script->firstSourceLocation(), + tr("Expected only name, prototype, defaultProperty, attachedType, exports, " + "isSingleton, isCreatable, isComposite and exportMetaObjectRevisions " + "script bindings, not \"%1\".").arg(name)); + } + } else { + addWarning(member->firstSourceLocation(), tr("Expected only script bindings and object definitions.")); + } + } + + if (fmo->className().isEmpty()) { + addError(ast->firstSourceLocation(), tr("Component definition is missing a name binding.")); + return; + } + + // ### add implicit export into the package of c++ types + fmo->addExport(fmo->className(), QStringLiteral("<cpp>"), ComponentVersion()); + fmo->updateFingerprint(); + _objects->insert(fmo->className(), fmo); +} + +void TypeDescriptionReader::readModuleApi(UiObjectDefinition *ast) +{ + ModuleApiInfo apiInfo; + + for (UiObjectMemberList *it = ast->initializer->members; it; it = it->next) { + UiObjectMember *member = it->member; + UiScriptBinding *script = AST::cast<UiScriptBinding *>(member); + + if (script) { + const QString name = toString(script->qualifiedId); + if (name == QLatin1String("uri")) { + apiInfo.uri = readStringBinding(script); + } else if (name == QLatin1String("version")) { + apiInfo.version = readNumericVersionBinding(script); + } else if (name == QLatin1String("name")) { + apiInfo.cppName = readStringBinding(script); + } else { + addWarning(script->firstSourceLocation(), + tr("Expected only uri, version and name script bindings.")); + } + } else { + addWarning(member->firstSourceLocation(), tr("Expected only script bindings.")); + } + } + + if (!apiInfo.version.isValid()) { + addError(ast->firstSourceLocation(), tr("ModuleApi definition has no or invalid version binding.")); + return; + } + + if (_moduleApis) + _moduleApis->append(apiInfo); +} + +void TypeDescriptionReader::readSignalOrMethod(UiObjectDefinition *ast, bool isMethod, FakeMetaObject::Ptr fmo) +{ + FakeMetaMethod fmm; + // ### confusion between Method and Slot. Method should be removed. + if (isMethod) + fmm.setMethodType(FakeMetaMethod::Slot); + else + fmm.setMethodType(FakeMetaMethod::Signal); + + for (UiObjectMemberList *it = ast->initializer->members; it; it = it->next) { + UiObjectMember *member = it->member; + UiObjectDefinition *component = AST::cast<UiObjectDefinition *>(member); + UiScriptBinding *script = AST::cast<UiScriptBinding *>(member); + if (component) { + QString name = toString(component->qualifiedTypeNameId); + if (name == QLatin1String("Parameter")) + readParameter(component, &fmm); + else + addWarning(component->firstSourceLocation(), tr("Expected only Parameter object definitions.")); + } else if (script) { + QString name = toString(script->qualifiedId); + if (name == QLatin1String("name")) + fmm.setMethodName(readStringBinding(script)); + else if (name == QLatin1String("type")) + fmm.setReturnType(readStringBinding(script)); + else if (name == QLatin1String("revision")) + fmm.setRevision(readIntBinding(script)); + else + addWarning(script->firstSourceLocation(), tr("Expected only name and type script bindings.")); + + } else { + addWarning(member->firstSourceLocation(), tr("Expected only script bindings and object definitions.")); + } + } + + if (fmm.methodName().isEmpty()) { + addError(ast->firstSourceLocation(), tr("Method or signal is missing a name script binding.")); + return; + } + + fmo->addMethod(fmm); +} + +void TypeDescriptionReader::readProperty(UiObjectDefinition *ast, FakeMetaObject::Ptr fmo) +{ + QString name; + QString type; + bool isPointer = false; + bool isReadonly = false; + bool isList = false; + int revision = 0; + + for (UiObjectMemberList *it = ast->initializer->members; it; it = it->next) { + UiObjectMember *member = it->member; + UiScriptBinding *script = AST::cast<UiScriptBinding *>(member); + if (!script) { + addWarning(member->firstSourceLocation(), tr("Expected script binding.")); + continue; + } + + QString id = toString(script->qualifiedId); + if (id == QLatin1String("name")) + name = readStringBinding(script); + else if (id == QLatin1String("type")) + type = readStringBinding(script); + else if (id == QLatin1String("isPointer")) + isPointer = readBoolBinding(script); + else if (id == QLatin1String("isReadonly")) + isReadonly = readBoolBinding(script); + else if (id == QLatin1String("isList")) + isList = readBoolBinding(script); + else if (id == QLatin1String("revision")) + revision = readIntBinding(script); + else + addWarning(script->firstSourceLocation(), tr("Expected only type, name, revision, isPointer, isReadonly and isList script bindings.")); + } + + if (name.isEmpty() || type.isEmpty()) { + addError(ast->firstSourceLocation(), tr("Property object is missing a name or type script binding.")); + return; + } + + fmo->addProperty(FakeMetaProperty(name, type, isList, !isReadonly, isPointer, revision)); +} + +void TypeDescriptionReader::readEnum(UiObjectDefinition *ast, FakeMetaObject::Ptr fmo) +{ + FakeMetaEnum fme; + + for (UiObjectMemberList *it = ast->initializer->members; it; it = it->next) { + UiObjectMember *member = it->member; + UiScriptBinding *script = AST::cast<UiScriptBinding *>(member); + if (!script) { + addWarning(member->firstSourceLocation(), tr("Expected script binding.")); + continue; + } + + QString name = toString(script->qualifiedId); + if (name == QLatin1String("name")) + fme.setName(readStringBinding(script)); + else if (name == QLatin1String("values")) + readEnumValues(script, &fme); + else + addWarning(script->firstSourceLocation(), tr("Expected only name and values script bindings.")); + } + + fmo->addEnum(fme); +} + +void TypeDescriptionReader::readParameter(UiObjectDefinition *ast, FakeMetaMethod *fmm) +{ + QString name; + QString type; + + for (UiObjectMemberList *it = ast->initializer->members; it; it = it->next) { + UiObjectMember *member = it->member; + UiScriptBinding *script = AST::cast<UiScriptBinding *>(member); + if (!script) { + addWarning(member->firstSourceLocation(), tr("Expected script binding.")); + continue; + } + + const QString id = toString(script->qualifiedId); + if (id == QLatin1String("name")) { + name = readStringBinding(script); + } else if (id == QLatin1String("type")) { + type = readStringBinding(script); + } else if (id == QLatin1String("isPointer")) { + // ### unhandled + } else if (id == QLatin1String("isReadonly")) { + // ### unhandled + } else if (id == QLatin1String("isList")) { + // ### unhandled + } else { + addWarning(script->firstSourceLocation(), tr("Expected only name and type script bindings.")); + } + } + + fmm->addParameter(name, type); +} + +QString TypeDescriptionReader::readStringBinding(UiScriptBinding *ast) +{ + QTC_ASSERT(ast, return QString()); + + if (!ast->statement) { + addError(ast->colonToken, tr("Expected string after colon.")); + return QString(); + } + + ExpressionStatement *expStmt = AST::cast<ExpressionStatement *>(ast->statement); + if (!expStmt) { + addError(ast->statement->firstSourceLocation(), tr("Expected string after colon.")); + return QString(); + } + + StringLiteral *stringLit = AST::cast<StringLiteral *>(expStmt->expression); + if (!stringLit) { + addError(expStmt->firstSourceLocation(), tr("Expected string after colon.")); + return QString(); + } + + return stringLit->value.toString(); +} + +bool TypeDescriptionReader::readBoolBinding(AST::UiScriptBinding *ast) +{ + QTC_ASSERT(ast, return false); + + if (!ast->statement) { + addError(ast->colonToken, tr("Expected boolean after colon.")); + return false; + } + + ExpressionStatement *expStmt = AST::cast<ExpressionStatement *>(ast->statement); + if (!expStmt) { + addError(ast->statement->firstSourceLocation(), tr("Expected boolean after colon.")); + return false; + } + + TrueLiteral *trueLit = AST::cast<TrueLiteral *>(expStmt->expression); + FalseLiteral *falseLit = AST::cast<FalseLiteral *>(expStmt->expression); + if (!trueLit && !falseLit) { + addError(expStmt->firstSourceLocation(), tr("Expected true or false after colon.")); + return false; + } + + return trueLit; +} + +double TypeDescriptionReader::readNumericBinding(AST::UiScriptBinding *ast) +{ + QTC_ASSERT(ast, return qQNaN()); + + if (!ast->statement) { + addError(ast->colonToken, tr("Expected numeric literal after colon.")); + return 0; + } + + ExpressionStatement *expStmt = AST::cast<ExpressionStatement *>(ast->statement); + if (!expStmt) { + addError(ast->statement->firstSourceLocation(), tr("Expected numeric literal after colon.")); + return 0; + } + + NumericLiteral *numericLit = AST::cast<NumericLiteral *>(expStmt->expression); + if (!numericLit) { + addError(expStmt->firstSourceLocation(), tr("Expected numeric literal after colon.")); + return 0; + } + + return numericLit->value; +} + +ComponentVersion TypeDescriptionReader::readNumericVersionBinding(UiScriptBinding *ast) +{ + ComponentVersion invalidVersion; + + if (!ast || !ast->statement) { + addError((ast ? ast->colonToken : SourceLocation()), tr("Expected numeric literal after colon.")); + return invalidVersion; + } + + ExpressionStatement *expStmt = AST::cast<ExpressionStatement *>(ast->statement); + if (!expStmt) { + addError(ast->statement->firstSourceLocation(), tr("Expected numeric literal after colon.")); + return invalidVersion; + } + + NumericLiteral *numericLit = AST::cast<NumericLiteral *>(expStmt->expression); + if (!numericLit) { + addError(expStmt->firstSourceLocation(), tr("Expected numeric literal after colon.")); + return invalidVersion; + } + + return ComponentVersion(_source.mid(numericLit->literalToken.begin(), numericLit->literalToken.length)); +} + +int TypeDescriptionReader::readIntBinding(AST::UiScriptBinding *ast) +{ + double v = readNumericBinding(ast); + int i = static_cast<int>(v); + + if (i != v) { + addError(ast->firstSourceLocation(), tr("Expected integer after colon.")); + return 0; + } + + return i; +} + +void TypeDescriptionReader::readExports(UiScriptBinding *ast, FakeMetaObject::Ptr fmo) +{ + QTC_ASSERT(ast, return); + + if (!ast->statement) { + addError(ast->colonToken, tr("Expected array of strings after colon.")); + return; + } + + ExpressionStatement *expStmt = AST::cast<ExpressionStatement *>(ast->statement); + if (!expStmt) { + addError(ast->statement->firstSourceLocation(), tr("Expected array of strings after colon.")); + return; + } + + ArrayPattern *arrayLit = AST::cast<ArrayPattern *>(expStmt->expression); + if (!arrayLit) { + addError(expStmt->firstSourceLocation(), tr("Expected array of strings after colon.")); + return; + } + + for (PatternElementList *it = arrayLit->elements; it; it = it->next) { + StringLiteral *stringLit = AST::cast<StringLiteral *>(it->element->initializer); + if (!stringLit) { + addError(arrayLit->firstSourceLocation(), tr("Expected array literal with only string literal members.")); + return; + } + QString exp = stringLit->value.toString(); + int slashIdx = exp.indexOf(QLatin1Char('/')); + int spaceIdx = exp.indexOf(QLatin1Char(' ')); + ComponentVersion version(exp.mid(spaceIdx + 1)); + + if (spaceIdx == -1 || !version.isValid()) { + addError(stringLit->firstSourceLocation(), tr("Expected string literal to contain 'Package/Name major.minor' or 'Name major.minor'.")); + continue; + } + QString package; + if (slashIdx != -1) + package = exp.left(slashIdx); + QString name = exp.mid(slashIdx + 1, spaceIdx - (slashIdx+1)); + + // ### relocatable exports where package is empty? + fmo->addExport(name, package, version); + } +} + +void TypeDescriptionReader::readMetaObjectRevisions(UiScriptBinding *ast, FakeMetaObject::Ptr fmo) +{ + QTC_ASSERT(ast, return); + + if (!ast->statement) { + addError(ast->colonToken, tr("Expected array of numbers after colon.")); + return; + } + + ExpressionStatement *expStmt = AST::cast<ExpressionStatement *>(ast->statement); + if (!expStmt) { + addError(ast->statement->firstSourceLocation(), tr("Expected array of numbers after colon.")); + return; + } + + ArrayPattern *arrayLit = AST::cast<ArrayPattern *>(expStmt->expression); + if (!arrayLit) { + addError(expStmt->firstSourceLocation(), tr("Expected array of numbers after colon.")); + return; + } + + int exportIndex = 0; + const int exportCount = fmo->exports().size(); + for (PatternElementList *it = arrayLit->elements; it; it = it->next, ++exportIndex) { + NumericLiteral *numberLit = cast<NumericLiteral *>(it->element->initializer); + if (!numberLit) { + addError(arrayLit->firstSourceLocation(), tr("Expected array literal with only number literal members.")); + return; + } + + if (exportIndex >= exportCount) { + addError(numberLit->firstSourceLocation(), tr("Meta object revision without matching export.")); + return; + } + + const double v = numberLit->value; + const int metaObjectRevision = static_cast<int>(v); + if (metaObjectRevision != v) { + addError(numberLit->firstSourceLocation(), tr("Expected integer.")); + return; + } + + fmo->setExportMetaObjectRevision(exportIndex, metaObjectRevision); + } +} + +void TypeDescriptionReader::readEnumValues(AST::UiScriptBinding *ast, LanguageUtils::FakeMetaEnum *fme) +{ + if (!ast) + return; + if (!ast->statement) { + addError(ast->colonToken, tr("Expected object literal after colon.")); + return; + } + + ExpressionStatement *expStmt = AST::cast<ExpressionStatement *>(ast->statement); + if (!expStmt) { + addError(ast->statement->firstSourceLocation(), tr("Expected object literal after colon.")); + return; + } + + ObjectPattern *objectLit = AST::cast<ObjectPattern *>(expStmt->expression); + if (!objectLit) { + addError(expStmt->firstSourceLocation(), tr("Expected object literal after colon.")); + return; + } + + for (PatternPropertyList *it = objectLit->properties; it; it = it->next) { + PatternProperty *assignement = AST::cast<PatternProperty *>(it->property); + if (assignement) { + StringLiteralPropertyName *propName = AST::cast<StringLiteralPropertyName *>(assignement->name); + NumericLiteral *value = AST::cast<NumericLiteral *>(assignement->initializer); + UnaryMinusExpression *minus = AST::cast<UnaryMinusExpression *>(assignement->initializer); + if (minus) + value = AST::cast<NumericLiteral *>(minus->expression); + if (!propName || !value) { + addError(objectLit->firstSourceLocation(), tr("Expected object literal to contain only 'string: number' elements.")); + continue; + } + + double v = value->value; + if (minus) + v = -v; + fme->addKey(propName->id.toString(), v); + continue; + } + PatternPropertyList *getterSetter = AST::cast<PatternPropertyList *>(it->next); + if (getterSetter) + addError(objectLit->firstSourceLocation(), tr("Enum should not contain getter and setters, but only 'string: number' elements.")); + } +} diff --git a/tools/qmllint/qmljstypedescriptionreader.h b/tools/qmllint/qmljstypedescriptionreader.h new file mode 100644 index 0000000000..df215af8d2 --- /dev/null +++ b/tools/qmllint/qmljstypedescriptionreader.h @@ -0,0 +1,103 @@ +/**************************************************************************** +** +** Copyright (C) 2019 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$ +** +****************************************************************************/ + +#ifndef QMLJSTYPEDESCRIPTIONREADER_H +#define QMLJSTYPEDESCRIPTIONREADER_H + +#include <private/qqmljsastfwd_p.h> +#include "fakemetaobject.h" + +// for Q_DECLARE_TR_FUNCTIONS +#include <QCoreApplication> + +QT_BEGIN_NAMESPACE +class QIODevice; +class QBuffer; + +namespace QQmlJS { + +class ModuleApiInfo +{ +public: + QString uri; + LanguageUtils::ComponentVersion version; + QString cppName; +}; + + +class TypeDescriptionReader +{ + Q_DECLARE_TR_FUNCTIONS(QQmlJS::TypeDescriptionReader) + +public: + explicit TypeDescriptionReader(const QString &fileName, const QString &data); + ~TypeDescriptionReader(); + + bool operator()( + QHash<QString, LanguageUtils::FakeMetaObject::ConstPtr> *objects, + QList<ModuleApiInfo> *moduleApis, + QStringList *dependencies); + QString errorMessage() const; + QString warningMessage() const; + +private: + void readDocument(AST::UiProgram *ast); + void readModule(AST::UiObjectDefinition *ast); + void readDependencies(AST::UiScriptBinding *ast); + void readComponent(AST::UiObjectDefinition *ast); + void readModuleApi(AST::UiObjectDefinition *ast); + void readSignalOrMethod(AST::UiObjectDefinition *ast, bool isMethod, LanguageUtils::FakeMetaObject::Ptr fmo); + void readProperty(AST::UiObjectDefinition *ast, LanguageUtils::FakeMetaObject::Ptr fmo); + void readEnum(AST::UiObjectDefinition *ast, LanguageUtils::FakeMetaObject::Ptr fmo); + void readParameter(AST::UiObjectDefinition *ast, LanguageUtils::FakeMetaMethod *fmm); + + QString readStringBinding(AST::UiScriptBinding *ast); + bool readBoolBinding(AST::UiScriptBinding *ast); + double readNumericBinding(AST::UiScriptBinding *ast); + LanguageUtils::ComponentVersion readNumericVersionBinding(AST::UiScriptBinding *ast); + int readIntBinding(AST::UiScriptBinding *ast); + void readExports(AST::UiScriptBinding *ast, LanguageUtils::FakeMetaObject::Ptr fmo); + void readMetaObjectRevisions(AST::UiScriptBinding *ast, LanguageUtils::FakeMetaObject::Ptr fmo); + void readEnumValues(AST::UiScriptBinding *ast, LanguageUtils::FakeMetaEnum *fme); + + void addError(const AST::SourceLocation &loc, const QString &message); + void addWarning(const AST::SourceLocation &loc, const QString &message); + + QString _fileName; + QString _source; + QString _errorMessage; + QString _warningMessage; + QHash<QString, LanguageUtils::FakeMetaObject::ConstPtr> *_objects; + QList<ModuleApiInfo> *_moduleApis = nullptr; + QStringList *_dependencies = nullptr; +}; + +} // namespace QQmlJS +QT_END_NAMESPACE + +#endif // QMLJSTYPEDESCRIPTIONREADER_H diff --git a/tools/qmllint/qmllint.pro b/tools/qmllint/qmllint.pro index 91ab2f8afc..76363a7cd8 100644 --- a/tools/qmllint/qmllint.pro +++ b/tools/qmllint/qmllint.pro @@ -2,8 +2,22 @@ option(host_build) QT = core qmldevtools-private -SOURCES += main.cpp +SOURCES += main.cpp \ + componentversion.cpp \ + fakemetaobject.cpp \ + findunqualified.cpp \ + qmljstypedescriptionreader.cpp \ + qcoloroutput.cpp \ + scopetree.cpp QMAKE_TARGET_DESCRIPTION = QML Syntax Verifier load(qt_tool) + +HEADERS += \ + componentversion.h \ + fakemetaobject.h \ + findunqualified.h \ + qmljstypedescriptionreader.h \ + qcoloroutput_p.h \ + scopetree.h diff --git a/tools/qmllint/scopetree.cpp b/tools/qmllint/scopetree.cpp new file mode 100644 index 0000000000..b064117dd4 --- /dev/null +++ b/tools/qmllint/scopetree.cpp @@ -0,0 +1,255 @@ +/**************************************************************************** +** +** Copyright (C) 2019 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 "scopetree.h" + +#include "qcoloroutput_p.h" + +#include <QQueue> + +ScopeTree::ScopeTree(ScopeType type, QString name, ScopeTree *parentScope) + : m_parentScope(parentScope), m_name(name), m_scopeType(type) {} + +ScopeTree *ScopeTree::createNewChildScope(ScopeType type, QString name) { + Q_ASSERT(type != ScopeType::QMLScope|| !m_parentScope || m_parentScope->m_scopeType == ScopeType::QMLScope || m_parentScope->m_name == "global"); + auto childScope = new ScopeTree{type, name, this}; + m_childScopes.push_back(childScope); + return childScope; +} + +ScopeTree *ScopeTree::parentScope() { + return m_parentScope; +} + +void ScopeTree::insertJSIdentifier(QString id, QQmlJS::AST::VariableScope scope) +{ + Q_ASSERT(m_scopeType != ScopeType::QMLScope); + if (scope == QQmlJS::AST::VariableScope::Var) { + auto targetScope = this; + while (targetScope->scopeType() != ScopeType::JSFunctionScope) { + targetScope = targetScope->m_parentScope; + } + targetScope->m_currentScopeJSIdentifiers.insert(id); + } else { + m_currentScopeJSIdentifiers.insert(id); + } +} + +void ScopeTree::insertQMLIdentifier(QString id) +{ + Q_ASSERT(m_scopeType == ScopeType::QMLScope); + m_currentScopeQMLIdentifiers.insert(id); +} + +void ScopeTree::insertSignalIdentifier(QString id, LanguageUtils::FakeMetaMethod method, QQmlJS::AST::SourceLocation loc) +{ + Q_ASSERT(m_scopeType == ScopeType::QMLScope); + m_injectedSignalIdentifiers.insert(id, {method, loc}); +} + +void ScopeTree::insertPropertyIdentifier(QString id) +{ + this->insertQMLIdentifier(id); + LanguageUtils::FakeMetaMethod method( id + QLatin1String("Changed"), "void"); + this->addMethod(method); +} + +bool ScopeTree::isIdInCurrentScope(const QString &id) const +{ + return isIdInCurrentQMlScopes(id) || isIdInCurrentJSScopes(id); +} + +void ScopeTree::addIdToAccssedIfNotInParentScopes(const QPair<QString, QQmlJS::AST::SourceLocation> &id_loc_pair) { + if (!isIdInCurrentScope(id_loc_pair.first)) { + m_accessedIdentifiers.push_back(id_loc_pair); + } +} + +bool ScopeTree::isVisualRootScope() const +{ + return m_parentScope && m_parentScope->m_parentScope && m_parentScope->m_parentScope->m_parentScope == nullptr; +} + +QString ScopeTree::name() const +{ + return m_name; +} + +struct IssueLocationWithContext +{ + IssueLocationWithContext(const QString& code, QQmlJS::AST::SourceLocation location) { + int before = std::max(0,code.lastIndexOf('\n', location.offset)); + beforeText = code.midRef(before+1, location.offset - (before+1) ); + issueText = code.midRef(location.offset, location.length); + int after = code.indexOf('\n', location.offset + location.length); + afterText = code.midRef(location.offset+location.length, after - (location.offset+location.length)); + } + + QStringRef beforeText; + QStringRef issueText; + QStringRef afterText; +}; + +bool ScopeTree::recheckIdentifiers(const QString& code, const QHash<QString, LanguageUtils::FakeMetaObject::ConstPtr> &qmlIDs, const ScopeTree *root, const QString& rootId, ColorOutput& colorOut) const +{ + bool noUnqualifiedIdentifier = true; + + // revisit all scopes + QQueue<const ScopeTree*> workQueue; + workQueue.enqueue(this); + while (!workQueue.empty()) { + const ScopeTree* currentScope = workQueue.dequeue(); + for (auto idLocationPair : currentScope->m_accessedIdentifiers) { + if (qmlIDs.contains(idLocationPair.first)) + continue; + if (currentScope->isIdInCurrentScope(idLocationPair.first)) { + continue; + } + noUnqualifiedIdentifier = false; + colorOut.write("Warning: ", Warning); + auto location = idLocationPair.second; + colorOut.write(QString::asprintf("unqualified access at %d:%d\n", location.startLine, location.startColumn), Normal); + IssueLocationWithContext issueLocationWithContext {code, location}; + colorOut.write(issueLocationWithContext.beforeText.toString(), Normal); + colorOut.write(issueLocationWithContext.issueText.toString(), Error); + colorOut.write(issueLocationWithContext.afterText.toString() + QLatin1Char('\n'), Normal); + int tabCount = issueLocationWithContext.beforeText.count(QLatin1Char('\t')); + colorOut.write(QString(" ").repeated(issueLocationWithContext.beforeText.length() - tabCount) + QString("\t").repeated(tabCount) + QString("^").repeated(location.length) + QLatin1Char('\n'), Normal); + // root(JS) --> program(qml) --> (first element) + if (root->m_childScopes[0]->m_childScopes[0]->m_currentScopeQMLIdentifiers.contains(idLocationPair.first)) { + ScopeTree *parentScope = currentScope->m_parentScope; + while (parentScope && parentScope->scopeType() != ScopeType::QMLScope) { + parentScope = parentScope->m_parentScope; + } + bool directChild = parentScope && parentScope->isVisualRootScope(); + colorOut.write("Note: ", Info); + colorOut.write( idLocationPair.first + QLatin1String(" is a meber of the root element\n"), Normal ); + if (directChild) { + colorOut.write(QLatin1String(" As the current element is a direct child of the root element,\n you can qualify the access with parent to avoid this warning:\n"), Normal); + } else { + colorOut.write(QLatin1String(" You can qualify the access with its id to avoid this warning:\n"), Normal); + if (rootId == QLatin1String("<id>")) { + colorOut.write("Note: ", Warning); + colorOut.write(("You first have to give the root element an id\n")); + } + } + colorOut.write(issueLocationWithContext.beforeText.toString(), Normal); + colorOut.write( (directChild ? QLatin1String("parent") : rootId) + QLatin1Char('.'), Hint); + colorOut.write(issueLocationWithContext.issueText.toString(), Normal); + colorOut.write(issueLocationWithContext.afterText + QLatin1Char('\n'), Normal); + } else if (currentScope->isIdInjectedFromSignal(idLocationPair.first)) { + auto qmlScope = currentScope->getCurrentQMLScope(); + MethodUsage methodUsage = qmlScope->m_injectedSignalIdentifiers[idLocationPair.first]; + colorOut.write("Note:", Info); + colorOut.write(idLocationPair.first + QString::asprintf(" is accessible in this scope because you are handling a signal at %d:%d\n", methodUsage.loc.startLine, methodUsage.loc.startColumn), Normal); + colorOut.write("Consider using a function instead\n", Normal); + IssueLocationWithContext context {code, methodUsage.loc}; + colorOut.write(context.beforeText + QLatin1Char(' ')); + colorOut.write("function(", Hint); + const auto parameters = methodUsage.method.parameterNames(); + for (int numParams = parameters.size(); numParams > 0; --numParams) { + colorOut.write(parameters.at(parameters.size() - numParams), Hint); + if (numParams > 1) { + colorOut.write(", ", Hint); + } + } + colorOut.write(")", Hint); + colorOut.write(" {...", Normal); + } + colorOut.write("\n", Normal); + } + for (auto const& childScope: currentScope->m_childScopes) { + workQueue.enqueue(childScope); + } + } + return noUnqualifiedIdentifier; +} + +QMap<QString, LanguageUtils::FakeMetaMethod>const &ScopeTree::methods() const +{ + return m_methods; +} + +bool ScopeTree::isIdInCurrentQMlScopes(QString id) const +{ + auto qmlScope = getCurrentQMLScope(); + return qmlScope->m_currentScopeQMLIdentifiers.contains(id); +} + +bool ScopeTree::isIdInCurrentJSScopes(QString id) const +{ + auto jsScope = this; + while (jsScope) { + if (jsScope->m_scopeType != ScopeType::QMLScope && jsScope->m_currentScopeJSIdentifiers.contains(id)) + return true; + jsScope = jsScope->m_parentScope; + } + return false; +} + +bool ScopeTree::isIdInjectedFromSignal(QString id) const +{ + auto qmlScope = getCurrentQMLScope(); + return qmlScope->m_injectedSignalIdentifiers.contains(id); +} + +const ScopeTree *ScopeTree::getCurrentQMLScope() const +{ + auto qmlScope = this; + while (qmlScope && qmlScope->m_scopeType != ScopeType::QMLScope) { + qmlScope = qmlScope->m_parentScope; + } + return qmlScope; +} + +ScopeTree *ScopeTree::getCurrentQMLScope() +{ + auto qmlScope = this; + while (qmlScope && qmlScope->m_scopeType != ScopeType::QMLScope) { + qmlScope = qmlScope->m_parentScope; + } + return qmlScope; +} + +ScopeType ScopeTree::scopeType() {return m_scopeType;} + +void ScopeTree::addMethod(LanguageUtils::FakeMetaMethod method) +{ + m_methods.insert(method.methodName(), method); +} + +void ScopeTree::addMethodsFromMetaObject(LanguageUtils::FakeMetaObject::ConstPtr metaObject) +{ + if (metaObject) { + auto methodCount = metaObject->methodCount(); + for (auto i = 0; i < methodCount; ++i) { + auto method = metaObject->method(i); + this->addMethod(method); + } + } +} diff --git a/tools/qmllint/scopetree.h b/tools/qmllint/scopetree.h new file mode 100644 index 0000000000..cb495584c1 --- /dev/null +++ b/tools/qmllint/scopetree.h @@ -0,0 +1,105 @@ +/**************************************************************************** +** +** Copyright (C) 2019 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$ +** +****************************************************************************/ + +#ifndef SCOPETREE_H +#define SCOPETREE_H + +#include "fakemetaobject.h" +#include "private/qqmljsast_p.h" +#include "private/qqmljssourcelocation_p.h" + +#include <QSet> +#include <QString> +#include <QMap> + +enum MessageColors{ + Error, + Warning, + Info, + Normal, + Hint +}; + +enum class ScopeType +{ + JSFunctionScope, + JSLexicalScope, + QMLScope +}; + +struct MethodUsage +{ + LanguageUtils::FakeMetaMethod method; + QQmlJS::AST::SourceLocation loc; +}; + +class ColorOutput; + +class ScopeTree { +public: + ScopeTree(ScopeType type, QString name="<none given>", ScopeTree* parentScope=nullptr); + ~ScopeTree() {qDeleteAll(m_childScopes);} + + ScopeTree* createNewChildScope(ScopeType type, QString name); + ScopeTree* parentScope(); + + void insertJSIdentifier(QString id, QQmlJS::AST::VariableScope scope); + void insertQMLIdentifier(QString id); + void insertSignalIdentifier(QString id, LanguageUtils::FakeMetaMethod method, QQmlJS::AST::SourceLocation loc); + void insertPropertyIdentifier(QString id); // inserts property as qml identifier as well as the corresponding + + bool isIdInCurrentScope(QString const &id) const; + void addIdToAccssedIfNotInParentScopes(QPair<QString, QQmlJS::AST::SourceLocation> const& id_loc_pair); + + bool isVisualRootScope() const; + QString name() const; + + bool recheckIdentifiers(const QString &code, const QHash<QString, LanguageUtils::FakeMetaObject::ConstPtr>& qmlIDs, const ScopeTree *root, const QString& rootId, ColorOutput &colorOut) const; + ScopeType scopeType(); + void addMethod(LanguageUtils::FakeMetaMethod); + void addMethodsFromMetaObject(LanguageUtils::FakeMetaObject::ConstPtr metaObject); + QMap<QString, LanguageUtils::FakeMetaMethod>const & methods() const; + +private: + QSet<QString> m_currentScopeJSIdentifiers; + QSet<QString> m_currentScopeQMLIdentifiers; + QHash<QString, MethodUsage> m_injectedSignalIdentifiers; // need iteration in insertion order for hints, rarely more than 4 parameters needed + QMap<QString, LanguageUtils::FakeMetaMethod> m_methods; + QVector<QPair<QString, QQmlJS::AST::SourceLocation>> m_accessedIdentifiers; + QVector<ScopeTree*> m_childScopes; + ScopeTree *m_parentScope; + QString m_name; + ScopeType m_scopeType; + + bool isIdInCurrentQMlScopes(QString id) const; + bool isIdInCurrentJSScopes(QString id) const; + bool isIdInjectedFromSignal(QString id) const; + const ScopeTree* getCurrentQMLScope() const; + ScopeTree* getCurrentQMLScope(); +}; +#endif // SCOPETREE_H |