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 <ulf.hermann@qt.io>
Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
This commit is contained in:
Maximilian Goldstein 2021-02-24 16:42:51 +01:00
parent 7ea690c61d
commit bbba1e5614
16 changed files with 289 additions and 0 deletions

View File

@ -20,6 +20,7 @@ qt_internal_add_module(QmlCompiler
qqmljstypedescriptionreader.cpp qqmljstypedescriptionreader_p.h qqmljstypedescriptionreader.cpp qqmljstypedescriptionreader_p.h
qqmljstypereader.cpp qqmljstypereader_p.h qqmljstypereader.cpp qqmljstypereader_p.h
qresourcerelocater.cpp qresourcerelocater_p.h qresourcerelocater.cpp qresourcerelocater_p.h
qqmljsannotation_p.h qqmljsannotation.cpp
PUBLIC_LIBRARIES PUBLIC_LIBRARIES
Qt::CorePrivate Qt::CorePrivate
Qt::QmlDevToolsPrivate Qt::QmlDevToolsPrivate

View File

@ -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;
}

View File

@ -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 <private/qqmljsast_p.h>
#include <qvariant.h>
#include <qglobal.h>
QT_BEGIN_NAMESPACE
using namespace QQmlJS::AST;
struct QQQmlJSDeprecation
{
QString reason;
};
struct QQmlJSAnnotation
{
QString name;
QHash<QString, QVariant> bindings;
bool isDeprecation() const;
QQQmlJSDeprecation deprecation() const;
};
QT_END_NAMESPACE
#endif // QQMLJSANNOTATION_P_H

View File

@ -135,6 +135,69 @@ void QQmlJSImportVisitor::endVisit(UiProgram *)
resolveAliases(); resolveAliases();
} }
static QVariant bindingToVariant(QQmlJS::AST::Statement *statement)
{
ExpressionStatement *expr = cast<ExpressionStatement *>(statement);
if (!statement || !expr->expression)
return QVariant();
switch (expr->expression->kind) {
case Node::Kind_StringLiteral:
return cast<StringLiteral *>(expr->expression)->value.toString();
case Node::Kind_NumericLiteral:
return cast<NumericLiteral *>(expr->expression)->value;
default:
return QVariant();
}
}
QVector<QQmlJSAnnotation> QQmlJSImportVisitor::parseAnnotations(QQmlJS::AST::UiAnnotationList *list)
{
QVector<QQmlJSAnnotation> 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<UiScriptBinding*>(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) bool QQmlJSImportVisitor::visit(UiObjectDefinition *definition)
{ {
QString superType; QString superType;
@ -147,6 +210,8 @@ bool QQmlJSImportVisitor::visit(UiObjectDefinition *definition)
if (!m_exportedRootScope) if (!m_exportedRootScope)
m_exportedRootScope = m_currentScope; m_exportedRootScope = m_currentScope;
m_currentScope->setAnnotations(parseAnnotations(definition->annotations));
QQmlJSScope::resolveTypes(m_currentScope, m_rootScopeImports); QQmlJSScope::resolveTypes(m_currentScope, m_rootScopeImports);
return true; return true;
} }
@ -189,6 +254,8 @@ bool QQmlJSImportVisitor::visit(UiPublicMember *publicMember)
prop.setIsWritable(!publicMember->isReadonlyMember); prop.setIsWritable(!publicMember->isReadonlyMember);
prop.setIsAlias(isAlias); prop.setIsAlias(isAlias);
prop.setType(m_rootScopeImports.value(prop.typeName())); prop.setType(m_rootScopeImports.value(prop.typeName()));
prop.setAnnotations(parseAnnotations(publicMember->annotations));
m_currentScope->insertPropertyIdentifier(prop); m_currentScope->insertPropertyIdentifier(prop);
if (publicMember->isRequired) if (publicMember->isRequired)
m_currentScope->setPropertyLocallyRequired(prop.propertyName(), true); m_currentScope->setPropertyLocallyRequired(prop.propertyName(), true);

View File

@ -40,6 +40,7 @@
// We mean it. // We mean it.
#include "qqmljsscope_p.h" #include "qqmljsscope_p.h"
#include "qqmljsannotation_p.h"
#include <private/qqmljsast_p.h> #include <private/qqmljsast_p.h>
#include <private/qqmljsdiagnosticmessage_p.h> #include <private/qqmljsdiagnosticmessage_p.h>
@ -126,6 +127,8 @@ protected:
const QQmlJS::SourceLocation &location); const QQmlJS::SourceLocation &location);
void leaveEnvironment(); void leaveEnvironment();
QVector<QQmlJSAnnotation> parseAnnotations(QQmlJS::AST::UiAnnotationList *list);
private: private:
void importBaseModules(); void importBaseModules();
void resolveAliases(); void resolveAliases();

View File

@ -43,6 +43,8 @@
#include <QtCore/qstringlist.h> #include <QtCore/qstringlist.h>
#include <QtCore/qsharedpointer.h> #include <QtCore/qsharedpointer.h>
#include "qqmljsannotation_p.h"
// MetaMethod and MetaProperty have both type names and actual QQmlJSScope types. // MetaMethod and MetaProperty have both type names and actual QQmlJSScope types.
// When parsing the information from the relevant QML or qmltypes files, we only // 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 // 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_typeName;
QString m_bindable; QString m_bindable;
QWeakPointer<const QQmlJSScope> m_type; QWeakPointer<const QQmlJSScope> m_type;
QVector<QQmlJSAnnotation> m_annotations;
bool m_isList = false; bool m_isList = false;
bool m_isWritable = false; bool m_isWritable = false;
bool m_isPointer = false; bool m_isPointer = false;
@ -259,6 +262,9 @@ public:
void setType(const QSharedPointer<const QQmlJSScope> &type) { m_type = type; } void setType(const QSharedPointer<const QQmlJSScope> &type) { m_type = type; }
QSharedPointer<const QQmlJSScope> type() const { return m_type.toStrongRef(); } QSharedPointer<const QQmlJSScope> type() const { return m_type.toStrongRef(); }
void setAnnotations(const QList<QQmlJSAnnotation> &annotation) { m_annotations = std::move(annotation); }
const QList<QQmlJSAnnotation> &annotations() const { return m_annotations; }
void setIsList(bool isList) { m_isList = isList; } void setIsList(bool isList) { m_isList = isList; }
bool isList() const { return m_isList; } bool isList() const { return m_isList; }

View File

@ -41,6 +41,7 @@
#include "qqmljsmetatypes_p.h" #include "qqmljsmetatypes_p.h"
#include "qdeferredpointer_p.h" #include "qdeferredpointer_p.h"
#include "qqmljsannotation_p.h"
#include <QtQml/private/qqmljssourcelocation_p.h> #include <QtQml/private/qqmljssourcelocation_p.h>
@ -186,6 +187,9 @@ public:
bool hasEnumerationKey(const QString &name) const; bool hasEnumerationKey(const QString &name) const;
QQmlJSMetaEnum enumeration(const QString &name) const; QQmlJSMetaEnum enumeration(const QString &name) const;
void setAnnotations(const QList<QQmlJSAnnotation> &annotation) { m_annotations = std::move(annotation); }
const QList<QQmlJSAnnotation> &annotations() const { return m_annotations; }
QString fileName() const { return m_fileName; } QString fileName() const { return m_fileName; }
void setFileName(const QString &file) { m_fileName = file; } void setFileName(const QString &file) { m_fileName = file; }
@ -296,6 +300,7 @@ private:
QHash<QString, QQmlJSMetaProperty> m_properties; QHash<QString, QQmlJSMetaProperty> m_properties;
QHash<QString, QQmlJSMetaEnum> m_enumerations; QHash<QString, QQmlJSMetaEnum> m_enumerations;
QVector<QQmlJSAnnotation> m_annotations;
QVector<QQmlJSScope::Ptr> m_childScopes; QVector<QQmlJSScope::Ptr> m_childScopes;
QQmlJSScope::WeakPtr m_parentScope; QQmlJSScope::WeakPtr m_parentScope;

View File

@ -0,0 +1,4 @@
import QtQml
@Deprecated {}
QtObject {}

View File

@ -0,0 +1,4 @@
import QtQml
@Deprecated { reason: "Test" }
QtObject {}

View File

@ -0,0 +1,10 @@
import QtQml
QtObject {
@Deprecated {}
property int deprecated: 10
Component.onCompleted: {
console.log(deprecated);
}
}

View File

@ -0,0 +1,12 @@
import QtQml
QtObject {
@Deprecated {
reason: "Test"
}
property int deprecated: 10
Component.onCompleted: {
console.log(deprecated);
}
}

View File

@ -0,0 +1,3 @@
import QtQml
TypeDeprecated {}

View File

@ -0,0 +1,3 @@
import QtQml
TypeDeprecatedReason {}

View File

@ -329,6 +329,22 @@ void TestQmllint::dirtyQmlCode_data()
<< QStringLiteral("No type found for property \"bad\". This may be due to a missing " << QStringLiteral("No type found for property \"bad\". This may be due to a missing "
"import statement or incomplete qmltypes files.") "import statement or incomplete qmltypes files.")
<< QString(); << 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() void TestQmllint::dirtyQmlCode()

View File

@ -347,6 +347,27 @@ bool CheckIdentifiers::operator()(
const auto property = qmlScope->property(memberAccessBase.m_name); const auto property = qmlScope->property(memberAccessBase.m_name);
if (!property.propertyName().isEmpty()) { 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())) if (memberAccessChain.isEmpty() || unknownBuiltins.contains(property.typeName()))
continue; continue;

View File

@ -45,8 +45,30 @@
void FindWarningVisitor::checkInheritanceCycle(QQmlJSScope::ConstPtr scope) void FindWarningVisitor::checkInheritanceCycle(QQmlJSScope::ConstPtr scope)
{ {
QQmlJSScope::ConstPtr originalScope = scope;
QList<QQmlJSScope::ConstPtr> scopes; QList<QQmlJSScope::ConstPtr> scopes;
while (!scope.isNull()) { 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)) { if (scopes.contains(scope)) {
QString inheritenceCycle; QString inheritenceCycle;
for (const auto &seen: qAsConst(scopes)) { for (const auto &seen: qAsConst(scopes)) {