From bbba1e5614cf320efc27783b2e771520cbe53999 Mon Sep 17 00:00:00 2001 From: Maximilian Goldstein Date: Wed, 24 Feb 2021 16:42:51 +0100 Subject: [PATCH] qmllint: Implement deprecation warnings Make qmllint warn about @Deprecated {} annotations. Also adds support for annotations in qmlcompiler. [ChangeLog][QML][qmllint] Add support for deprecation annotations. Task-number: QTBUG-84895 Change-Id: Ia506a6c0077a2b9ab3bf4fdac207bd0540635b30 Reviewed-by: Ulf Hermann Reviewed-by: Fabian Kosmale --- src/qmlcompiler/CMakeLists.txt | 1 + src/qmlcompiler/qqmljsannotation.cpp | 46 +++++++++++++ src/qmlcompiler/qqmljsannotation_p.h | 66 ++++++++++++++++++ src/qmlcompiler/qqmljsimportvisitor.cpp | 67 +++++++++++++++++++ src/qmlcompiler/qqmljsimportvisitor_p.h | 3 + src/qmlcompiler/qqmljsmetatypes_p.h | 6 ++ src/qmlcompiler/qqmljsscope_p.h | 5 ++ .../auto/qml/qmllint/data/TypeDeprecated.qml | 4 ++ .../qml/qmllint/data/TypeDeprecatedReason.qml | 4 ++ .../qml/qmllint/data/deprecatedProperty.qml | 10 +++ .../qmllint/data/deprecatedPropertyReason.qml | 12 ++++ .../auto/qml/qmllint/data/deprecatedType.qml | 3 + .../qml/qmllint/data/deprecatedTypeReason.qml | 3 + tests/auto/qml/qmllint/tst_qmllint.cpp | 16 +++++ tools/qmllint/checkidentifiers.cpp | 21 ++++++ tools/qmllint/findwarnings.cpp | 22 ++++++ 16 files changed, 289 insertions(+) create mode 100644 src/qmlcompiler/qqmljsannotation.cpp create mode 100644 src/qmlcompiler/qqmljsannotation_p.h create mode 100644 tests/auto/qml/qmllint/data/TypeDeprecated.qml create mode 100644 tests/auto/qml/qmllint/data/TypeDeprecatedReason.qml create mode 100644 tests/auto/qml/qmllint/data/deprecatedProperty.qml create mode 100644 tests/auto/qml/qmllint/data/deprecatedPropertyReason.qml create mode 100644 tests/auto/qml/qmllint/data/deprecatedType.qml create mode 100644 tests/auto/qml/qmllint/data/deprecatedTypeReason.qml diff --git a/src/qmlcompiler/CMakeLists.txt b/src/qmlcompiler/CMakeLists.txt index d7eb39ebdb..b3c1f7a5bd 100644 --- a/src/qmlcompiler/CMakeLists.txt +++ b/src/qmlcompiler/CMakeLists.txt @@ -20,6 +20,7 @@ qt_internal_add_module(QmlCompiler qqmljstypedescriptionreader.cpp qqmljstypedescriptionreader_p.h qqmljstypereader.cpp qqmljstypereader_p.h qresourcerelocater.cpp qresourcerelocater_p.h + qqmljsannotation_p.h qqmljsannotation.cpp PUBLIC_LIBRARIES Qt::CorePrivate Qt::QmlDevToolsPrivate diff --git a/src/qmlcompiler/qqmljsannotation.cpp b/src/qmlcompiler/qqmljsannotation.cpp new file mode 100644 index 0000000000..3d16cca5c2 --- /dev/null +++ b/src/qmlcompiler/qqmljsannotation.cpp @@ -0,0 +1,46 @@ +/**************************************************************************** +** +** Copyright (C) 2021 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 "qqmljsannotation_p.h" + +bool QQmlJSAnnotation::isDeprecation() const { return name == QStringLiteral("Deprecated"); } + +QQQmlJSDeprecation QQmlJSAnnotation::deprecation() const { + Q_ASSERT(isDeprecation()); + QQQmlJSDeprecation deprecation; + if (bindings.contains(QStringLiteral("reason"))) { + + auto reason = bindings[QStringLiteral("reason")]; + + if (reason.typeId() == QMetaType::QString) { + deprecation.reason = reason.toString(); + } + } + + return deprecation; +} diff --git a/src/qmlcompiler/qqmljsannotation_p.h b/src/qmlcompiler/qqmljsannotation_p.h new file mode 100644 index 0000000000..bd19580745 --- /dev/null +++ b/src/qmlcompiler/qqmljsannotation_p.h @@ -0,0 +1,66 @@ +/**************************************************************************** +** +** Copyright (C) 2021 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 QQMLJSANNOTATION_P_H +#define QQMLJSANNOTATION_P_H + +// +// 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. + +#include +#include +#include + +QT_BEGIN_NAMESPACE + +using namespace QQmlJS::AST; + +struct QQQmlJSDeprecation +{ + QString reason; +}; + +struct QQmlJSAnnotation +{ + QString name; + QHash bindings; + + bool isDeprecation() const; + QQQmlJSDeprecation deprecation() const; +}; + +QT_END_NAMESPACE + +#endif // QQMLJSANNOTATION_P_H diff --git a/src/qmlcompiler/qqmljsimportvisitor.cpp b/src/qmlcompiler/qqmljsimportvisitor.cpp index 599f2bb00a..2359f5af9a 100644 --- a/src/qmlcompiler/qqmljsimportvisitor.cpp +++ b/src/qmlcompiler/qqmljsimportvisitor.cpp @@ -135,6 +135,69 @@ void QQmlJSImportVisitor::endVisit(UiProgram *) resolveAliases(); } +static QVariant bindingToVariant(QQmlJS::AST::Statement *statement) +{ + ExpressionStatement *expr = cast(statement); + + if (!statement || !expr->expression) + return QVariant(); + + switch (expr->expression->kind) { + case Node::Kind_StringLiteral: + return cast(expr->expression)->value.toString(); + case Node::Kind_NumericLiteral: + return cast(expr->expression)->value; + default: + return QVariant(); + } +} + +QVector QQmlJSImportVisitor::parseAnnotations(QQmlJS::AST::UiAnnotationList *list) +{ + + QVector annotationList; + + for (UiAnnotationList *item = list; item != nullptr; item = item->next) { + UiAnnotation *annotation = item->annotation; + + QString name; + for (auto id = annotation->qualifiedTypeNameId; id; id = id->next) + name += id->name.toString() + QLatin1Char('.'); + + name.chop(1); + + + + QQmlJSAnnotation qqmljsAnnotation; + + qqmljsAnnotation.name = name; + + for (UiObjectMemberList *memberItem = annotation->initializer->members; memberItem != nullptr; memberItem = memberItem->next) { + switch (memberItem->member->kind) { + case Node::Kind_UiScriptBinding: { + auto *scriptBinding = QQmlJS::AST::cast(memberItem->member); + QString bindingName; + for (auto id = scriptBinding->qualifiedId; id; id = id->next) + bindingName += id->name.toString() + QLatin1Char('.'); + + bindingName.chop(1); + + qqmljsAnnotation.bindings[bindingName] = bindingToVariant(scriptBinding->statement); + break; + } + default: + // We ignore all the other information contained in the annotation + break; + } + } + + annotationList.append(qqmljsAnnotation); + } + + return annotationList; +} + + bool QQmlJSImportVisitor::visit(UiObjectDefinition *definition) { QString superType; @@ -147,6 +210,8 @@ bool QQmlJSImportVisitor::visit(UiObjectDefinition *definition) if (!m_exportedRootScope) m_exportedRootScope = m_currentScope; + m_currentScope->setAnnotations(parseAnnotations(definition->annotations)); + QQmlJSScope::resolveTypes(m_currentScope, m_rootScopeImports); return true; } @@ -189,6 +254,8 @@ bool QQmlJSImportVisitor::visit(UiPublicMember *publicMember) prop.setIsWritable(!publicMember->isReadonlyMember); prop.setIsAlias(isAlias); prop.setType(m_rootScopeImports.value(prop.typeName())); + prop.setAnnotations(parseAnnotations(publicMember->annotations)); + m_currentScope->insertPropertyIdentifier(prop); if (publicMember->isRequired) m_currentScope->setPropertyLocallyRequired(prop.propertyName(), true); diff --git a/src/qmlcompiler/qqmljsimportvisitor_p.h b/src/qmlcompiler/qqmljsimportvisitor_p.h index 87183f5066..e93080c28d 100644 --- a/src/qmlcompiler/qqmljsimportvisitor_p.h +++ b/src/qmlcompiler/qqmljsimportvisitor_p.h @@ -40,6 +40,7 @@ // We mean it. #include "qqmljsscope_p.h" +#include "qqmljsannotation_p.h" #include #include @@ -126,6 +127,8 @@ protected: const QQmlJS::SourceLocation &location); void leaveEnvironment(); + QVector parseAnnotations(QQmlJS::AST::UiAnnotationList *list); + private: void importBaseModules(); void resolveAliases(); diff --git a/src/qmlcompiler/qqmljsmetatypes_p.h b/src/qmlcompiler/qqmljsmetatypes_p.h index 57328af836..8dc9748e29 100644 --- a/src/qmlcompiler/qqmljsmetatypes_p.h +++ b/src/qmlcompiler/qqmljsmetatypes_p.h @@ -43,6 +43,8 @@ #include #include +#include "qqmljsannotation_p.h" + // MetaMethod and MetaProperty have both type names and actual QQmlJSScope types. // When parsing the information from the relevant QML or qmltypes files, we only // see the names and don't have a complete picture of the types, yet. In a second @@ -238,6 +240,7 @@ class QQmlJSMetaProperty QString m_typeName; QString m_bindable; QWeakPointer m_type; + QVector m_annotations; bool m_isList = false; bool m_isWritable = false; bool m_isPointer = false; @@ -259,6 +262,9 @@ public: void setType(const QSharedPointer &type) { m_type = type; } QSharedPointer type() const { return m_type.toStrongRef(); } + void setAnnotations(const QList &annotation) { m_annotations = std::move(annotation); } + const QList &annotations() const { return m_annotations; } + void setIsList(bool isList) { m_isList = isList; } bool isList() const { return m_isList; } diff --git a/src/qmlcompiler/qqmljsscope_p.h b/src/qmlcompiler/qqmljsscope_p.h index 6db87b13ca..484f8ce1d3 100644 --- a/src/qmlcompiler/qqmljsscope_p.h +++ b/src/qmlcompiler/qqmljsscope_p.h @@ -41,6 +41,7 @@ #include "qqmljsmetatypes_p.h" #include "qdeferredpointer_p.h" +#include "qqmljsannotation_p.h" #include @@ -186,6 +187,9 @@ public: bool hasEnumerationKey(const QString &name) const; QQmlJSMetaEnum enumeration(const QString &name) const; + void setAnnotations(const QList &annotation) { m_annotations = std::move(annotation); } + const QList &annotations() const { return m_annotations; } + QString fileName() const { return m_fileName; } void setFileName(const QString &file) { m_fileName = file; } @@ -296,6 +300,7 @@ private: QHash m_properties; QHash m_enumerations; + QVector m_annotations; QVector m_childScopes; QQmlJSScope::WeakPtr m_parentScope; diff --git a/tests/auto/qml/qmllint/data/TypeDeprecated.qml b/tests/auto/qml/qmllint/data/TypeDeprecated.qml new file mode 100644 index 0000000000..53ae988052 --- /dev/null +++ b/tests/auto/qml/qmllint/data/TypeDeprecated.qml @@ -0,0 +1,4 @@ +import QtQml + +@Deprecated {} +QtObject {} diff --git a/tests/auto/qml/qmllint/data/TypeDeprecatedReason.qml b/tests/auto/qml/qmllint/data/TypeDeprecatedReason.qml new file mode 100644 index 0000000000..1d43c118f7 --- /dev/null +++ b/tests/auto/qml/qmllint/data/TypeDeprecatedReason.qml @@ -0,0 +1,4 @@ +import QtQml + +@Deprecated { reason: "Test" } +QtObject {} diff --git a/tests/auto/qml/qmllint/data/deprecatedProperty.qml b/tests/auto/qml/qmllint/data/deprecatedProperty.qml new file mode 100644 index 0000000000..539e7abb38 --- /dev/null +++ b/tests/auto/qml/qmllint/data/deprecatedProperty.qml @@ -0,0 +1,10 @@ +import QtQml + +QtObject { + @Deprecated {} + property int deprecated: 10 + + Component.onCompleted: { + console.log(deprecated); + } +} diff --git a/tests/auto/qml/qmllint/data/deprecatedPropertyReason.qml b/tests/auto/qml/qmllint/data/deprecatedPropertyReason.qml new file mode 100644 index 0000000000..0e1edad6d0 --- /dev/null +++ b/tests/auto/qml/qmllint/data/deprecatedPropertyReason.qml @@ -0,0 +1,12 @@ +import QtQml + +QtObject { + @Deprecated { + reason: "Test" + } + property int deprecated: 10 + + Component.onCompleted: { + console.log(deprecated); + } +} diff --git a/tests/auto/qml/qmllint/data/deprecatedType.qml b/tests/auto/qml/qmllint/data/deprecatedType.qml new file mode 100644 index 0000000000..997e5d1ff2 --- /dev/null +++ b/tests/auto/qml/qmllint/data/deprecatedType.qml @@ -0,0 +1,3 @@ +import QtQml + +TypeDeprecated {} diff --git a/tests/auto/qml/qmllint/data/deprecatedTypeReason.qml b/tests/auto/qml/qmllint/data/deprecatedTypeReason.qml new file mode 100644 index 0000000000..faa84a4127 --- /dev/null +++ b/tests/auto/qml/qmllint/data/deprecatedTypeReason.qml @@ -0,0 +1,3 @@ +import QtQml + +TypeDeprecatedReason {} diff --git a/tests/auto/qml/qmllint/tst_qmllint.cpp b/tests/auto/qml/qmllint/tst_qmllint.cpp index 25776c5448..012a918634 100644 --- a/tests/auto/qml/qmllint/tst_qmllint.cpp +++ b/tests/auto/qml/qmllint/tst_qmllint.cpp @@ -329,6 +329,22 @@ void TestQmllint::dirtyQmlCode_data() << QStringLiteral("No type found for property \"bad\". This may be due to a missing " "import statement or incomplete qmltypes files.") << QString(); + QTest::newRow("Deprecation (Property, with reason)") + << QStringLiteral("deprecatedPropertyReason.qml") + << QStringLiteral("Property \"deprecated\" is deprecated (Reason: Test)") + << QString(); + QTest::newRow("Deprecation (Property, no reason)") + << QStringLiteral("deprecatedProperty.qml") + << QStringLiteral("Property \"deprecated\" is deprecated") + << QString(); + QTest::newRow("Deprecation (Type, with reason)") + << QStringLiteral("deprecatedTypeReason.qml") + << QStringLiteral("Type \"TypeDeprecatedReason\" is deprecated (Reason: Test)") + << QString(); + QTest::newRow("Deprecation (Type, no reason)") + << QStringLiteral("deprecatedType.qml") + << QStringLiteral("Type \"TypeDeprecated\" is deprecated") + << QString(); } void TestQmllint::dirtyQmlCode() diff --git a/tools/qmllint/checkidentifiers.cpp b/tools/qmllint/checkidentifiers.cpp index b0c56bcec6..6516202a91 100644 --- a/tools/qmllint/checkidentifiers.cpp +++ b/tools/qmllint/checkidentifiers.cpp @@ -347,6 +347,27 @@ bool CheckIdentifiers::operator()( const auto property = qmlScope->property(memberAccessBase.m_name); if (!property.propertyName().isEmpty()) { + for (const QQmlJSAnnotation &annotation : property.annotations()) { + if (annotation.isDeprecation()) { + QQQmlJSDeprecation deprecation = annotation.deprecation(); + + QString message = QStringLiteral("Property \"%1\" is deprecated") + .arg(memberAccessBase.m_name); + + if (!deprecation.reason.isEmpty()) + message.append(QStringLiteral(" (Reason: %1)").arg(deprecation.reason)); + + message.append(QStringLiteral(" at %2:%3:%4") + .arg(m_fileName) + .arg(memberAccessBase.m_location.startLine) + .arg(memberAccessBase.m_location.startColumn) + ); + + m_colorOut->writePrefixedMessage(message, Warning); + identifiersClean = false; + } + } + if (memberAccessChain.isEmpty() || unknownBuiltins.contains(property.typeName())) continue; diff --git a/tools/qmllint/findwarnings.cpp b/tools/qmllint/findwarnings.cpp index e0e1c3831e..437b32ede3 100644 --- a/tools/qmllint/findwarnings.cpp +++ b/tools/qmllint/findwarnings.cpp @@ -45,8 +45,30 @@ void FindWarningVisitor::checkInheritanceCycle(QQmlJSScope::ConstPtr scope) { + QQmlJSScope::ConstPtr originalScope = scope; QList scopes; while (!scope.isNull()) { + + for (const QQmlJSAnnotation &annotation : scope->annotations()) { + if (annotation.isDeprecation()) { + QQQmlJSDeprecation deprecation = annotation.deprecation(); + + QString message = QStringLiteral("Type \"%1\" is deprecated") + .arg(scope->internalName()); + + if (!deprecation.reason.isEmpty()) + message.append(QStringLiteral(" (Reason: %1)").arg(deprecation.reason)); + + m_errors.append({ + message, + QtWarningMsg, + originalScope->sourceLocation() + }); + + m_visitFailed = true; + } + } + if (scopes.contains(scope)) { QString inheritenceCycle; for (const auto &seen: qAsConst(scopes)) {