// Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #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 allowedTypes, bool allowInDelegate, QAnyStringView warning) { QVarLengthArray 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) { // If the attachment does not have such a property or method then // it's either a more general error or an enum. Enums are fine. if (element->hasProperty(propertyName) || element->hasMethod(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 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 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 std::array 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(manager)); auto forbiddenChildProperty = std::make_unique(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(manager); auto addAttachedWarning = [&](AttachedPropertyTypeValidatorPass::TypeDescription attachedType, QList 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(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(manager)); } QT_END_NAMESPACE #include "moc_quicklintplugin.cpp"