qtdeclarative/src/plugins/qmllint/quick/quicklintplugin.cpp

547 lines
23 KiB
C++
Raw Normal View History

/****************************************************************************
**
** Copyright (C) 2022 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 "quicklintplugin.h"
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
ForbiddenChildrenPropertyValidatorPass::ForbiddenChildrenPropertyValidatorPass(
QQmlSA::PassManager *manager)
: QQmlSA::ElementPass(manager)
{
}
void ForbiddenChildrenPropertyValidatorPass::addWarning(QAnyStringView moduleName,
QAnyStringView typeName,
QAnyStringView propertyName,
QAnyStringView warning)
{
auto element = resolveType(moduleName, typeName);
if (!element.isNull())
m_types[element].append({ propertyName.toString(), warning.toString() });
}
bool ForbiddenChildrenPropertyValidatorPass::shouldRun(const QQmlSA::Element &element)
{
if (!element->parentScope())
return false;
for (const auto pair : m_types.asKeyValueRange()) {
if (element->parentScope()->inherits(pair.first))
return true;
}
return false;
}
void ForbiddenChildrenPropertyValidatorPass::run(const QQmlSA::Element &element)
{
for (const auto elementPair : m_types.asKeyValueRange()) {
const QQmlSA::Element &type = elementPair.first;
if (!element->parentScope()->inherits(type))
continue;
for (const auto &warning : elementPair.second) {
if (!element->hasOwnPropertyBindings(warning.propertyName))
continue;
auto bindings = element->ownPropertyBindings(warning.propertyName);
emitWarning(warning.message, bindings.first->sourceLocation());
}
break;
}
}
AttachedPropertyTypeValidatorPass::AttachedPropertyTypeValidatorPass(QQmlSA::PassManager *manager)
: QQmlSA::PropertyPass(manager)
{
}
QString AttachedPropertyTypeValidatorPass::addWarning(TypeDescription attachType,
QList<TypeDescription> allowedTypes,
bool allowInDelegate, QAnyStringView warning)
{
QVarLengthArray<QQmlSA::Element, 4> elements;
const QQmlSA::Element baseType = resolveType(attachType.module, attachType.name);
QString typeName = baseType->attachedTypeName();
for (const TypeDescription &desc : allowedTypes) {
const QQmlSA::Element type = resolveType(desc.module, desc.name);
if (type.isNull())
continue;
elements.push_back(type);
}
m_attachedTypes.insert({ std::make_pair<>(
typeName, Warning { elements, allowInDelegate, warning.toString() }) });
return typeName;
}
void AttachedPropertyTypeValidatorPass::checkWarnings(const QQmlSA::Element &element,
const QQmlSA::Element &scopeUsedIn,
const QQmlJS::SourceLocation &location)
{
auto warning = m_attachedTypes.constFind(element->internalName());
if (warning == m_attachedTypes.cend())
return;
for (const QQmlSA::Element &type : warning->allowedTypes) {
if (scopeUsedIn->inherits(type))
return;
}
if (warning->allowInDelegate) {
if (scopeUsedIn->isPropertyRequired(u"index"_s)
|| scopeUsedIn->isPropertyRequired(u"model"_s))
return;
if (scopeUsedIn->parentScope()) {
for (const QQmlJSMetaPropertyBinding &binding :
scopeUsedIn->parentScope()->propertyBindings(u"delegate"_s)) {
if (!binding.hasObject())
continue;
if (binding.objectType() == scopeUsedIn)
return;
}
}
}
emitWarning(warning->message, location);
}
void AttachedPropertyTypeValidatorPass::onBinding(const QQmlSA::Element &element,
const QString &propertyName,
const QQmlJSMetaPropertyBinding &binding,
const QQmlSA::Element &bindingScope,
const QQmlSA::Element &value)
{
Q_UNUSED(element)
Q_UNUSED(propertyName)
Q_UNUSED(bindingScope)
Q_UNUSED(value)
checkWarnings(bindingScope->baseType(), element, binding.sourceLocation());
}
void AttachedPropertyTypeValidatorPass::onRead(const QQmlSA::Element &element,
const QString &propertyName,
const QQmlSA::Element &readScope,
QQmlJS::SourceLocation location)
{
Q_UNUSED(readScope)
Q_UNUSED(propertyName)
checkWarnings(element, readScope, location);
}
void AttachedPropertyTypeValidatorPass::onWrite(const QQmlSA::Element &element,
const QString &propertyName,
const QQmlSA::Element &value,
const QQmlSA::Element &writeScope,
QQmlJS::SourceLocation location)
{
Q_UNUSED(propertyName)
Q_UNUSED(value)
checkWarnings(element, writeScope, location);
}
ControlsNativeValidatorPass::ControlsNativeValidatorPass(QQmlSA::PassManager *manager)
: QQmlSA::ElementPass(manager)
{
m_elements = {
ControlElement { "Control",
QStringList { "background", "contentItem", "leftPadding", "rightPadding",
"topPadding", "bottomPadding", "horizontalPadding",
"verticalPadding", "padding" },
false, true },
ControlElement { "Button", QStringList { "indicator" } },
ControlElement {
"ApplicationWindow",
QStringList { "background", "contentItem", "header", "footer", "menuBar" } },
ControlElement { "ComboBox", QStringList { "indicator" } },
ControlElement { "Dial", QStringList { "handle" } },
ControlElement { "Dialog", QStringList { "header", "footer" } },
ControlElement { "GroupBox", QStringList { "label" } },
ControlElement { "$internal$.QQuickIndicatorButton", QStringList { "indicator" }, false },
ControlElement { "Label", QStringList { "background" } },
ControlElement { "MenuItem", QStringList { "arrow" } },
ControlElement { "Page", QStringList { "header", "footer" } },
ControlElement { "Popup", QStringList { "background", "contentItem" } },
ControlElement { "RangeSlider", QStringList { "handle" } },
ControlElement { "Slider", QStringList { "handle" } },
ControlElement { "$internal$.QQuickSwipe",
QStringList { "leftItem", "behindItem", "rightItem" }, false },
ControlElement { "TextArea", QStringList { "background" } },
ControlElement { "TextField", QStringList { "background" } },
};
for (const QString &module : { u"QtQuick.Controls.macOS"_s, u"QtQuick.Controls.Windows"_s }) {
if (!manager->hasImportedModule(module))
continue;
QQmlSA::Element control = resolveType(module, "Control");
for (ControlElement &element : m_elements) {
auto type = resolveType(element.isInModuleControls ? module : "QtQuick.Templates",
element.name);
if (type.isNull())
continue;
element.inheritsControl = !element.isControl && type->inherits(control);
element.element = type;
}
m_elements.removeIf([](const ControlElement &element) { return element.element.isNull(); });
break;
}
}
bool ControlsNativeValidatorPass::shouldRun(const QQmlSA::Element &element)
{
for (const ControlElement &controlElement : m_elements) {
// If our element inherits control, we don't have to individually check for them here.
if (controlElement.inheritsControl)
continue;
if (element->inherits(controlElement.element))
return true;
}
return false;
}
void ControlsNativeValidatorPass::run(const QQmlSA::Element &element)
{
for (const ControlElement &controlElement : m_elements) {
if (element->inherits(controlElement.element)) {
for (const QString &propertyName : controlElement.restrictedProperties) {
if (element->hasOwnPropertyBindings(propertyName)) {
emitWarning(QStringLiteral("Not allowed to override \"%1\" because native "
"styles cannot be customized: See "
"https://doc-snapshots.qt.io/qt6-dev/"
"qtquickcontrols2-customize.html#customization-"
"reference for more information.")
.arg(propertyName),
element->sourceLocation());
}
}
// Since all the different types we have rules for don't inherit from each other (except
// for Control) we don't have to keep checking whether other types match once we've
// found one that has been inherited from.
if (!controlElement.isControl)
break;
}
}
}
AnchorsValidatorPass::AnchorsValidatorPass(QQmlSA::PassManager *manager)
: QQmlSA::ElementPass(manager)
{
m_item = resolveType("QtQuick", "Item");
}
bool AnchorsValidatorPass::shouldRun(const QQmlSA::Element &element)
{
return !m_item.isNull() && element->inherits(m_item)
&& element->hasOwnPropertyBindings(u"anchors"_s);
}
void AnchorsValidatorPass::run(const QQmlSA::Element &element)
{
enum BindingLocation { Exists = 1, Own = (1 << 1) };
QHash<QString, qint8> bindings;
const QStringList properties = { u"left"_s, u"right"_s, u"horizontalCenter"_s,
u"top"_s, u"bottom"_s, u"verticalCenter"_s,
u"baseline"_s };
QList<QQmlJSMetaPropertyBinding> anchorBindings = element->propertyBindings(u"anchors"_s);
for (qsizetype i = anchorBindings.size() - 1; i >= 0; i--) {
auto groupType = anchorBindings[i].groupType();
if (groupType == nullptr)
continue;
for (const QString &name : properties) {
auto pair = groupType->ownPropertyBindings(name);
if (pair.first == pair.second)
continue;
bool isUndefined = false;
for (auto it = pair.first; it != pair.second; it++) {
if (it->bindingType() == QQmlJSMetaPropertyBinding::Script
&& it->scriptValueType() == QQmlJSMetaPropertyBinding::ScriptValue_Undefined) {
isUndefined = true;
break;
}
}
if (isUndefined)
bindings[name] = 0;
else
bindings[name] |= Exists | ((i == 0) ? Own : 0);
}
}
auto ownSourceLocation = [&](QStringList properties) {
QQmlJS::SourceLocation warnLoc;
for (const QString &name : properties) {
if (bindings[name] & Own) {
QQmlSA::Element groupType = anchorBindings[0].groupType();
auto bindingRange = groupType->ownPropertyBindings(name);
Q_ASSERT(bindingRange.first != bindingRange.second);
warnLoc = bindingRange.first->sourceLocation();
break;
}
}
return warnLoc;
};
if ((bindings[u"left"_s] & bindings[u"right"_s] & bindings[u"horizontalCenter"_s]) & Exists) {
QQmlJS::SourceLocation warnLoc =
ownSourceLocation({ u"left"_s, u"right"_s, u"horizontalCenter"_s });
if (warnLoc.isValid()) {
emitWarning(
"Cannot specify left, right, and horizontalCenter anchors at the same time.",
warnLoc);
}
}
if ((bindings[u"top"_s] & bindings[u"bottom"_s] & bindings[u"verticalCenter"_s]) & Exists) {
QQmlJS::SourceLocation warnLoc =
ownSourceLocation({ u"top"_s, u"bottom"_s, u"verticalCenter"_s });
if (warnLoc.isValid()) {
emitWarning("Cannot specify top, bottom, and verticalCenter anchors at the same time.",
warnLoc);
}
}
if ((bindings[u"baseline"_s] & (bindings[u"bottom"_s] | bindings[u"verticalCenter"_s]))
& Exists) {
QQmlJS::SourceLocation warnLoc =
ownSourceLocation({ u"baseline"_s, u"bottom"_s, u"verticalCenter"_s });
if (warnLoc.isValid()) {
emitWarning("Baseline anchor cannot be used in conjunction with top, bottom, or "
"verticalCenter anchors.",
warnLoc);
}
}
}
ControlsSwipeDelegateValidatorPass::ControlsSwipeDelegateValidatorPass(QQmlSA::PassManager *manager)
: QQmlSA::ElementPass(manager)
{
m_swipeDelegate = resolveType("QtQuick.Controls", "SwipeDelegate");
}
bool ControlsSwipeDelegateValidatorPass::shouldRun(const QQmlSA::Element &element)
{
return !m_swipeDelegate.isNull() && element->inherits(m_swipeDelegate);
}
void ControlsSwipeDelegateValidatorPass::run(const QQmlSA::Element &element)
{
for (const auto &property : { u"background"_s, u"contentItem"_s }) {
auto bindings = element->ownPropertyBindings(property);
for (auto it = bindings.first; it != bindings.second; it++) {
if (!it->hasObject())
continue;
const QQmlSA::Element element = it->objectType();
const auto bindings = element->propertyBindings(u"anchors"_s);
if (bindings.isEmpty())
continue;
if (bindings.first().bindingType() != QQmlJSMetaPropertyBinding::GroupProperty)
continue;
auto anchors = bindings.first().groupType();
for (const auto &disallowed : { u"fill"_s, u"centerIn"_s, u"left"_s, u"right"_s }) {
if (anchors->hasPropertyBindings(disallowed)) {
QQmlJS::SourceLocation location;
auto ownBindings = anchors->ownPropertyBindings(disallowed);
if (ownBindings.first != ownBindings.second) {
location = ownBindings.first->sourceLocation();
}
emitWarning(
u"SwipeDelegate: Cannot use horizontal anchors with %1; unable to layout the item."_s
.arg(property),
location);
break;
}
}
break;
}
}
auto swipe = element->ownPropertyBindings(u"swipe"_s);
if (swipe.first == swipe.second)
return;
if (swipe.first->bindingType() != QQmlJSMetaPropertyBinding::GroupProperty)
return;
auto group = swipe.first->groupType();
const auto ownDirBindings = { group->ownPropertyBindings(u"right"_s),
group->ownPropertyBindings(u"left"_s),
group->ownPropertyBindings(u"behind"_s) };
auto ownBindingIterator =
std::find_if(ownDirBindings.begin(), ownDirBindings.end(),
[](const auto &pair) { return pair.first != pair.second; });
if (ownBindingIterator == ownDirBindings.end())
return;
if (group->hasPropertyBindings(u"behind"_s)
&& (group->hasPropertyBindings(u"right"_s) || group->hasPropertyBindings(u"left"_s))) {
emitWarning("SwipeDelegate: Cannot set both behind and left/right properties",
ownBindingIterator->first->sourceLocation());
}
}
void QmlLintQuickPlugin::registerPasses(QQmlSA::PassManager *manager,
const QQmlSA::Element &rootElement)
{
const bool hasQuick = manager->hasImportedModule("QtQuick");
const bool hasQuickLayouts = manager->hasImportedModule("QtQuick.Layouts");
const bool hasQuickControls = manager->hasImportedModule("QtQuick.Templates")
|| manager->hasImportedModule("QtQuick.Controls");
Q_UNUSED(rootElement);
if (hasQuick) {
manager->registerElementPass(std::make_unique<AnchorsValidatorPass>(manager));
auto forbiddenChildProperty =
std::make_unique<ForbiddenChildrenPropertyValidatorPass>(manager);
for (const QString &element : { u"Grid"_s, u"Flow"_s }) {
for (const QString &property : { u"anchors"_s, u"x"_s, u"y"_s }) {
forbiddenChildProperty->addWarning(
"QtQuick", element, property,
u"Cannot specify %1 for items inside %2. %2 will not function."_s.arg(
property, element));
}
}
if (hasQuickLayouts) {
forbiddenChildProperty->addWarning(
"QtQuick.Layouts", "Layout", "anchors",
"Detected anchors on an item that is managed by a layout. This is undefined "
u"behavior; use Layout.alignment instead.");
forbiddenChildProperty->addWarning(
"QtQuick.Layouts", "Layout", "x",
"Detected x on an item that is managed by a layout. This is undefined "
u"behavior; use Layout.leftMargin or Layout.rightMargin instead.");
forbiddenChildProperty->addWarning(
"QtQuick.Layouts", "Layout", "y",
"Detected y on an item that is managed by a layout. This is undefined "
u"behavior; use Layout.topMargin or Layout.bottomMargin instead.");
forbiddenChildProperty->addWarning(
"QtQuick.Layouts", "Layout", "width",
"Detected width on an item that is managed by a layout. This is undefined "
u"behavior; use implicitWidth or Layout.preferredWidth instead.");
forbiddenChildProperty->addWarning(
"QtQuick.Layouts", "Layout", "height",
"Detected height on an item that is managed by a layout. This is undefined "
u"behavior; use implictHeight or Layout.preferredHeight instead.");
}
manager->registerElementPass(std::move(forbiddenChildProperty));
}
auto attachedPropertyType = std::make_shared<AttachedPropertyTypeValidatorPass>(manager);
auto addAttachedWarning =
[&](AttachedPropertyTypeValidatorPass::TypeDescription attachedType,
QList<AttachedPropertyTypeValidatorPass::TypeDescription> allowedTypes,
QAnyStringView warning, bool allowInDelegate = false) {
QString attachedTypeName = attachedPropertyType->addWarning(
attachedType, allowedTypes, allowInDelegate, warning);
manager->registerPropertyPass(attachedPropertyType, attachedType.module,
u"$internal$."_s + attachedTypeName);
};
if (hasQuick) {
addAttachedWarning({ "QtQuick", "Accessible" }, { { "QtQuick", "Item" } },
"Accessible must be attached to an Item");
addAttachedWarning({ "QtQuick", "LayoutMirroring" },
{ { "QtQuick", "Item" }, { "QtQuick", "Window" } },
"LayoutDirection attached property only works with Items and Windows");
addAttachedWarning({ "QtQuick", "EnterKey" }, { { "QtQuick", "Item" } },
"EnterKey attached property only works with Items");
}
if (hasQuickLayouts) {
addAttachedWarning({ "QtQuick.Layouts", "Layout" }, { { "QtQuick", "Item" } },
"Layout must be attached to Item elements");
addAttachedWarning({ "QtQuick.Layouts", "StackLayout" }, { { "QtQuick", "Item" } },
"StackLayout must be attached to an Item");
}
if (hasQuickControls) {
manager->registerElementPass(std::make_unique<ControlsSwipeDelegateValidatorPass>(manager));
addAttachedWarning({ "QtQuick.Templates", "ScrollBar" },
{ { "QtQuick", "Flickable" }, { "QtQuick.Templates", "ScrollView" } },
"ScrollBar must be attached to a Flickable or ScrollView");
addAttachedWarning({ "QtQuick.Templates", "ScrollIndicator" },
{ { "QtQuick", "Flickable" } },
"ScrollIndicator must be attached to a Flickable");
addAttachedWarning({ "QtQuick.Templates", "TextArea" }, { { "QtQuick", "Flickable" } },
"TextArea must be attached to a Flickable");
addAttachedWarning({ "QtQuick.Templates", "SplitView" }, { { "QtQuick", "Item" } },
"SplitView attached property only works with Items");
addAttachedWarning({ "QtQuick.Templates", "StackView" }, { { "QtQuick", "Item" } },
"StackView attached property only works with Items");
addAttachedWarning({ "QtQuick.Templates", "ToolTip" }, { { "QtQuick", "Item" } },
"ToolTip must be attached to an Item");
addAttachedWarning({ "QtQuick.Templates", "SwipeDelegate" }, { { "QtQuick", "Item" } },
"Attached properties of SwipeDelegate must be accessed through an Item");
addAttachedWarning({ "QtQuick.Templates", "SwipeView" }, { { "QtQuick", "Item" } },
"SwipeView must be attached to an Item");
addAttachedWarning(
{ "QtQuick.Templates", "Tumbler" }, { { "QtQuick", "Tumbler" } },
"Tumbler: attached properties of Tumbler must be accessed through a delegate item",
true);
}
if (manager->hasImportedModule(u"QtQuick.Controls.macOS"_s)
|| manager->hasImportedModule(u"QtQuick.Controls.Windows"_s))
manager->registerElementPass(std::make_unique<ControlsNativeValidatorPass>(manager));
}
QT_END_NAMESPACE
#include "moc_quicklintplugin.cpp"