diff options
Diffstat (limited to 'src/plugins/qmllint/quick/quicklintplugin.cpp')
-rw-r--r-- | src/plugins/qmllint/quick/quicklintplugin.cpp | 392 |
1 files changed, 275 insertions, 117 deletions
diff --git a/src/plugins/qmllint/quick/quicklintplugin.cpp b/src/plugins/qmllint/quick/quicklintplugin.cpp index 3b89635748..52d4d759e3 100644 --- a/src/plugins/qmllint/quick/quicklintplugin.cpp +++ b/src/plugins/qmllint/quick/quicklintplugin.cpp @@ -3,17 +3,18 @@ #include "quicklintplugin.h" -#include <QtQmlCompiler/private/qqmljslogger_p.h> - QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; -static constexpr LoggerWarningId quickLayoutPositioning { "Quick.layout-positioning" }; -static constexpr LoggerWarningId quickAttachedPropertyType { "Quick.attached-property-type" }; -static constexpr LoggerWarningId quickControlsNativeCustomize { "Quick.controls-native-customize" }; -static constexpr LoggerWarningId quickAnchorCombinations { "Quick.anchor-combinations" }; -static constexpr LoggerWarningId quickUnexpectedVarType { "Quick.unexpected-var-type" }; +static constexpr QQmlSA::LoggerWarningId quickLayoutPositioning { "Quick.layout-positioning" }; +static constexpr QQmlSA::LoggerWarningId quickAttachedPropertyType { "Quick.attached-property-type" }; +static constexpr QQmlSA::LoggerWarningId quickControlsNativeCustomize { "Quick.controls-native-customize" }; +static constexpr QQmlSA::LoggerWarningId quickAnchorCombinations { "Quick.anchor-combinations" }; +static constexpr QQmlSA::LoggerWarningId quickUnexpectedVarType { "Quick.unexpected-var-type" }; +static constexpr QQmlSA::LoggerWarningId quickPropertyChangesParsed { "Quick.property-changes-parsed" }; +static constexpr QQmlSA::LoggerWarningId quickControlsAttachedPropertyReuse { "Quick.controls-attached-property-reuse" }; +static constexpr QQmlSA::LoggerWarningId quickAttachedPropertyReuse { "Quick.attached-property-reuse" }; ForbiddenChildrenPropertyValidatorPass::ForbiddenChildrenPropertyValidatorPass( QQmlSA::PassManager *manager) @@ -33,11 +34,11 @@ void ForbiddenChildrenPropertyValidatorPass::addWarning(QAnyStringView moduleNam bool ForbiddenChildrenPropertyValidatorPass::shouldRun(const QQmlSA::Element &element) { - if (!element->parentScope()) + if (!element.parentScope()) return false; - for (const auto pair : m_types.asKeyValueRange()) { - if (element->parentScope()->inherits(pair.first)) + for (const auto &pair : std::as_const(m_types).asKeyValueRange()) { + if (element.parentScope().inherits(pair.first)) return true; } @@ -46,18 +47,18 @@ bool ForbiddenChildrenPropertyValidatorPass::shouldRun(const QQmlSA::Element &el void ForbiddenChildrenPropertyValidatorPass::run(const QQmlSA::Element &element) { - for (const auto elementPair : m_types.asKeyValueRange()) { + for (const auto &elementPair : std::as_const(m_types).asKeyValueRange()) { const QQmlSA::Element &type = elementPair.first; - if (!element->parentScope()->inherits(type)) + if (!element.parentScope().inherits(type)) continue; for (const auto &warning : elementPair.second) { - if (!element->hasOwnPropertyBindings(warning.propertyName)) + if (!element.hasOwnPropertyBindings(warning.propertyName)) continue; - auto bindings = element->ownPropertyBindings(warning.propertyName); - - emitWarning(warning.message, quickLayoutPositioning, bindings.first->sourceLocation()); + const auto bindings = element.ownPropertyBindings(warning.propertyName); + const auto firstBinding = bindings.constBegin().value(); + emitWarning(warning.message, quickLayoutPositioning, firstBinding.sourceLocation()); } break; } @@ -75,8 +76,7 @@ QString AttachedPropertyTypeValidatorPass::addWarning(TypeDescription attachType QVarLengthArray<QQmlSA::Element, 4> elements; const QQmlSA::Element baseType = resolveType(attachType.module, attachType.name); - - QString typeName = baseType->attachedTypeName(); + const QQmlSA::Element attachedType = resolveAttached(attachType.module, attachType.name); for (const TypeDescription &desc : allowedTypes) { const QQmlSA::Element type = resolveType(desc.module, desc.name); @@ -85,36 +85,39 @@ QString AttachedPropertyTypeValidatorPass::addWarning(TypeDescription attachType elements.push_back(type); } - m_attachedTypes.insert({ std::make_pair<>( - typeName, Warning { elements, allowInDelegate, warning.toString() }) }); + m_attachedTypes.insert( + { std::make_pair<>(attachedType.internalId(), + Warning{ elements, allowInDelegate, warning.toString() }) }); - return typeName; + return attachedType.internalId(); } void AttachedPropertyTypeValidatorPass::checkWarnings(const QQmlSA::Element &element, const QQmlSA::Element &scopeUsedIn, - const QQmlJS::SourceLocation &location) + const QQmlSA::SourceLocation &location) { - auto warning = m_attachedTypes.constFind(element->internalName()); + auto warning = m_attachedTypes.constFind(element.internalId()); if (warning == m_attachedTypes.cend()) return; for (const QQmlSA::Element &type : warning->allowedTypes) { - if (scopeUsedIn->inherits(type)) + if (scopeUsedIn.inherits(type)) return; } if (warning->allowInDelegate) { - if (scopeUsedIn->isPropertyRequired(u"index"_s) - || scopeUsedIn->isPropertyRequired(u"model"_s)) + if (scopeUsedIn.isPropertyRequired(u"index"_s) + || scopeUsedIn.isPropertyRequired(u"model"_s)) return; // If the scope is at the root level, we cannot know whether it will be used // as a delegate or not. - if (!scopeUsedIn->parentScope() || scopeUsedIn->parentScope()->internalName() == u"global"_s) - return; + // ### TODO: add a method to check whether a scope is the global scope + // so that we do not need to use internalId + if (!scopeUsedIn.parentScope() || scopeUsedIn.parentScope().internalId() == u"global"_s) + return; - for (const QQmlJSMetaPropertyBinding &binding : - scopeUsedIn->parentScope()->propertyBindings(u"delegate"_s)) { + for (const QQmlSA::Binding &binding : + scopeUsedIn.parentScope().propertyBindings(u"delegate"_s)) { if (!binding.hasObject()) continue; if (binding.objectType() == scopeUsedIn) @@ -127,26 +130,31 @@ void AttachedPropertyTypeValidatorPass::checkWarnings(const QQmlSA::Element &ele void AttachedPropertyTypeValidatorPass::onBinding(const QQmlSA::Element &element, const QString &propertyName, - const QQmlJSMetaPropertyBinding &binding, + const QQmlSA::Binding &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()); + // We can only analyze simple attached bindings since we don't see + // the grouped and attached properties that lead up to this here. + // + // TODO: This is very crude. + // We should add API for grouped and attached properties. + if (propertyName.count(QLatin1Char('.')) > 1) + return; + + checkWarnings(bindingScope.baseType(), element, binding.sourceLocation()); } void AttachedPropertyTypeValidatorPass::onRead(const QQmlSA::Element &element, const QString &propertyName, const QQmlSA::Element &readScope, - QQmlJS::SourceLocation location) + QQmlSA::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)) + if (element.hasProperty(propertyName) || element.hasMethod(propertyName)) checkWarnings(element, readScope, location); } @@ -154,7 +162,7 @@ void AttachedPropertyTypeValidatorPass::onWrite(const QQmlSA::Element &element, const QString &propertyName, const QQmlSA::Element &value, const QQmlSA::Element &writeScope, - QQmlJS::SourceLocation location) + QQmlSA::SourceLocation location) { Q_UNUSED(propertyName) Q_UNUSED(value) @@ -177,7 +185,6 @@ ControlsNativeValidatorPass::ControlsNativeValidatorPass(QQmlSA::PassManager *ma 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" } }, @@ -205,7 +212,7 @@ ControlsNativeValidatorPass::ControlsNativeValidatorPass(QQmlSA::PassManager *ma if (type.isNull()) continue; - element.inheritsControl = !element.isControl && type->inherits(control); + element.inheritsControl = !element.isControl && type.inherits(control); element.element = type; } @@ -221,7 +228,7 @@ bool ControlsNativeValidatorPass::shouldRun(const QQmlSA::Element &element) // If our element inherits control, we don't have to individually check for them here. if (controlElement.inheritsControl) continue; - if (element->inherits(controlElement.element)) + if (element.inherits(controlElement.element)) return true; } return false; @@ -230,16 +237,16 @@ bool ControlsNativeValidatorPass::shouldRun(const QQmlSA::Element &element) void ControlsNativeValidatorPass::run(const QQmlSA::Element &element) { for (const ControlElement &controlElement : m_elements) { - if (element->inherits(controlElement.element)) { + if (element.inherits(controlElement.element)) { for (const QString &propertyName : controlElement.restrictedProperties) { - if (element->hasOwnPropertyBindings(propertyName)) { + 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/" "qtquickcontrols-customize.html#customization-" "reference for more information.") .arg(propertyName), - quickControlsNativeCustomize, element->sourceLocation()); + quickControlsNativeCustomize, element.sourceLocation()); } } // Since all the different types we have rules for don't inherit from each other (except @@ -253,14 +260,14 @@ void ControlsNativeValidatorPass::run(const QQmlSA::Element &element) AnchorsValidatorPass::AnchorsValidatorPass(QQmlSA::PassManager *manager) : QQmlSA::ElementPass(manager) + , m_item(resolveType("QtQuick", "Item")) { - 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); + return !m_item.isNull() && element.inherits(m_item) + && element.hasOwnPropertyBindings(u"anchors"_s); } void AnchorsValidatorPass::run(const QQmlSA::Element &element) @@ -272,21 +279,22 @@ void AnchorsValidatorPass::run(const QQmlSA::Element &element) u"top"_s, u"bottom"_s, u"verticalCenter"_s, u"baseline"_s }; - QList<QQmlJSMetaPropertyBinding> anchorBindings = element->propertyBindings(u"anchors"_s); + QList<QQmlSA::Binding> anchorBindings = element.propertyBindings(u"anchors"_s); for (qsizetype i = anchorBindings.size() - 1; i >= 0; i--) { auto groupType = anchorBindings[i].groupType(); - if (groupType == nullptr) + if (groupType.isNull()) continue; for (const QString &name : properties) { - auto pair = groupType->ownPropertyBindings(name); - if (pair.first == pair.second) + + const auto &propertyBindings = groupType.ownPropertyBindings(name); + if (propertyBindings.begin() == propertyBindings.end()) continue; + bool isUndefined = false; - for (auto it = pair.first; it != pair.second; it++) { - if (it->bindingType() == QQmlJSMetaPropertyBinding::Script - && it->scriptValueType() == QQmlJSMetaPropertyBinding::ScriptValue_Undefined) { + for (const auto &propertyBinding : propertyBindings) { + if (propertyBinding.hasUndefinedScriptValue()) { isUndefined = true; break; } @@ -299,14 +307,15 @@ void AnchorsValidatorPass::run(const QQmlSA::Element &element) } } - auto ownSourceLocation = [&](QStringList properties) { - QQmlJS::SourceLocation warnLoc; + auto ownSourceLocation = [&](QStringList properties) -> QQmlSA::SourceLocation { + QQmlSA::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(); + QQmlSA::Element groupType = QQmlSA::Element{ anchorBindings[0].groupType() }; + auto bindings = groupType.ownPropertyBindings(name); + Q_ASSERT(bindings.begin() != bindings.end()); + warnLoc = bindings.begin().value().sourceLocation(); break; } } @@ -314,7 +323,7 @@ void AnchorsValidatorPass::run(const QQmlSA::Element &element) }; if ((bindings[u"left"_s] & bindings[u"right"_s] & bindings[u"horizontalCenter"_s]) & Exists) { - QQmlJS::SourceLocation warnLoc = + QQmlSA::SourceLocation warnLoc = ownSourceLocation({ u"left"_s, u"right"_s, u"horizontalCenter"_s }); if (warnLoc.isValid()) { @@ -325,7 +334,7 @@ void AnchorsValidatorPass::run(const QQmlSA::Element &element) } if ((bindings[u"top"_s] & bindings[u"bottom"_s] & bindings[u"verticalCenter"_s]) & Exists) { - QQmlJS::SourceLocation warnLoc = + QQmlSA::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.", @@ -335,7 +344,7 @@ void AnchorsValidatorPass::run(const QQmlSA::Element &element) if ((bindings[u"baseline"_s] & (bindings[u"bottom"_s] | bindings[u"verticalCenter"_s])) & Exists) { - QQmlJS::SourceLocation warnLoc = + QQmlSA::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 " @@ -347,37 +356,36 @@ void AnchorsValidatorPass::run(const QQmlSA::Element &element) ControlsSwipeDelegateValidatorPass::ControlsSwipeDelegateValidatorPass(QQmlSA::PassManager *manager) : QQmlSA::ElementPass(manager) + , m_swipeDelegate(resolveType("QtQuick.Controls", "SwipeDelegate")) { - m_swipeDelegate = resolveType("QtQuick.Controls", "SwipeDelegate"); } bool ControlsSwipeDelegateValidatorPass::shouldRun(const QQmlSA::Element &element) { - return !m_swipeDelegate.isNull() && element->inherits(m_swipeDelegate); + 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()) + for (const auto &binding : element.ownPropertyBindings(property)) { + if (!binding.hasObject()) continue; - const QQmlSA::Element element = it->objectType(); - const auto bindings = element->propertyBindings(u"anchors"_s); + const QQmlSA::Element element = QQmlSA::Element{ binding.objectType() }; + const auto &bindings = element.propertyBindings(u"anchors"_s); if (bindings.isEmpty()) continue; - if (bindings.first().bindingType() != QQmlJSMetaPropertyBinding::GroupProperty) + if (bindings.first().bindingType() != QQmlSA::BindingType::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(); + if (anchors.hasPropertyBindings(disallowed)) { + QQmlSA::SourceLocation location; + const auto &ownBindings = anchors.ownPropertyBindings(disallowed); + if (ownBindings.begin() != ownBindings.end()) { + location = ownBindings.begin().value().sourceLocation(); } emitWarning( @@ -391,30 +399,31 @@ void ControlsSwipeDelegateValidatorPass::run(const QQmlSA::Element &element) } } - auto swipe = element->ownPropertyBindings(u"swipe"_s); - if (swipe.first == swipe.second) + const auto &swipe = element.ownPropertyBindings(u"swipe"_s); + if (swipe.begin() == swipe.end()) return; - if (swipe.first->bindingType() != QQmlJSMetaPropertyBinding::GroupProperty) + const auto firstSwipe = swipe.begin().value(); + if (firstSwipe.bindingType() != QQmlSA::BindingType::GroupProperty) return; - auto group = swipe.first->groupType(); + auto group = firstSwipe.groupType(); - const std::array ownDirBindings = { group->ownPropertyBindings(u"right"_s), - group->ownPropertyBindings(u"left"_s), - group->ownPropertyBindings(u"behind"_s) }; + 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; }); + [](const auto &bindings) { return bindings.begin() != bindings.end(); }); if (ownBindingIterator == ownDirBindings.end()) return; - if (group->hasPropertyBindings(u"behind"_s) - && (group->hasPropertyBindings(u"right"_s) || group->hasPropertyBindings(u"left"_s))) { + 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", - quickAnchorCombinations, ownBindingIterator->first->sourceLocation()); + quickAnchorCombinations, ownBindingIterator->begin().value().sourceLocation()); } } @@ -423,22 +432,14 @@ VarBindingTypeValidatorPass::VarBindingTypeValidatorPass( const QMultiHash<QString, TypeDescription> &expectedPropertyTypes) : QQmlSA::PropertyPass(manager) { - QMultiHash<QString, QQmlJSScope::ConstPtr> propertyTypes; - - for (const auto pair : expectedPropertyTypes.asKeyValueRange()) { - QQmlSA::Element propType; - - if (!pair.second.module.isEmpty()) { - propType = resolveType(pair.second.module, pair.second.name); - if (propType.isNull()) - continue; - } else { - auto scope = QQmlJSScope::create(); - scope->setInternalName(pair.second.name); - propType = scope; - } - - propertyTypes.insert(pair.first, propType); + QMultiHash<QString, QQmlSA::Element> propertyTypes; + + for (const auto &pair : expectedPropertyTypes.asKeyValueRange()) { + const QQmlSA::Element propType = pair.second.module.isEmpty() + ? resolveBuiltinType(pair.second.name) + : resolveType(pair.second.module, pair.second.name); + if (!propType.isNull()) + propertyTypes.insert(pair.first, propType); } m_expectedPropertyTypes = propertyTypes; @@ -446,7 +447,7 @@ VarBindingTypeValidatorPass::VarBindingTypeValidatorPass( void VarBindingTypeValidatorPass::onBinding(const QQmlSA::Element &element, const QString &propertyName, - const QQmlJSMetaPropertyBinding &binding, + const QQmlSA::Binding &binding, const QQmlSA::Element &bindingScope, const QQmlSA::Element &value) { @@ -463,14 +464,14 @@ void VarBindingTypeValidatorPass::onBinding(const QQmlSA::Element &element, if (!value.isNull()) { bindingType = value; } else { - if (QQmlJSMetaPropertyBinding::isLiteralBinding(binding.bindingType())) { + if (QQmlSA::Binding::isLiteralBinding(binding.bindingType())) { bindingType = resolveLiteralType(binding); } else { switch (binding.bindingType()) { - case QQmlJSMetaPropertyBinding::Object: - bindingType = binding.objectType(); + case QQmlSA::BindingType::Object: + bindingType = QQmlSA::Element{ binding.objectType() }; break; - case QQmlJSMetaPropertyBinding::Script: + case QQmlSA::BindingType::Script: break; default: return; @@ -479,11 +480,11 @@ void VarBindingTypeValidatorPass::onBinding(const QQmlSA::Element &element, } if (std::find_if(range.first, range.second, - [&](const QQmlSA::Element &scope) { return bindingType->inherits(scope); }) + [&](const QQmlSA::Element &scope) { return bindingType.inherits(scope); }) == range.second) { - const bool bindingTypeIsComposite = bindingType->isComposite(); - if (bindingTypeIsComposite && !bindingType->baseType()) { + const bool bindingTypeIsComposite = bindingType.isComposite(); + if (bindingTypeIsComposite && !bindingType.baseType()) { /* broken module or missing import, there is nothing we can really check here, as something is amiss. We simply skip this binding, and assume that whatever @@ -492,14 +493,13 @@ void VarBindingTypeValidatorPass::onBinding(const QQmlSA::Element &element, */ return; } - const QString bindingTypeName = QQmlJSScope::prettyName( - bindingTypeIsComposite - ? bindingType->baseType()->internalName() - : bindingType->internalName()); + const QString bindingTypeName = + bindingTypeIsComposite ? bindingType.baseType().name() + : bindingType.name(); QStringList expectedTypeNames; for (auto it = range.first; it != range.second; it++) - expectedTypeNames << QQmlJSScope::prettyName(it.value()->internalName()); + expectedTypeNames << it.value().name(); emitWarning(u"Unexpected type for property \"%1\" expected %2 got %3"_s.arg( propertyName, expectedTypeNames.join(u", "_s), bindingTypeName), @@ -507,9 +507,98 @@ void VarBindingTypeValidatorPass::onBinding(const QQmlSA::Element &element, } } +void AttachedPropertyReuse::onRead(const QQmlSA::Element &element, const QString &propertyName, + const QQmlSA::Element &readScope, + QQmlSA::SourceLocation location) +{ + const auto range = usedAttachedTypes.equal_range(readScope); + const auto attachedTypeAndLocation = std::find_if( + range.first, range.second, [&](const ElementAndLocation &elementAndLocation) { + return elementAndLocation.element == element; + }); + if (attachedTypeAndLocation != range.second) { + const QQmlSA::SourceLocation attachedLocation = attachedTypeAndLocation->location; + + // Ignore enum accesses, as these will not cause the attached object to be created. + // Also ignore anything we cannot determine. + if (!element.hasProperty(propertyName) && !element.hasMethod(propertyName)) + return; + + for (QQmlSA::Element scope = readScope.parentScope(); !scope.isNull(); + scope = scope.parentScope()) { + const auto range = usedAttachedTypes.equal_range(scope); + bool found = false; + for (auto it = range.first; it != range.second; ++it) { + if (it->element == element) { + found = true; + break; + } + } + if (!found) + continue; + + const QString id = resolveElementToId(scope, readScope); + const QQmlSA::SourceLocation idInsertLocation{ attachedLocation.offset(), 0, + attachedLocation.startLine(), + attachedLocation.startColumn() }; + QQmlSA::FixSuggestion suggestion{ "Reference it by id instead:"_L1, idInsertLocation, + id.isEmpty() ? u"<id>."_s : (id + '.'_L1) }; + + if (id.isEmpty()) + suggestion.setHint("You first have to give the element an id"_L1); + else + suggestion.setAutoApplicable(); + + emitWarning("Using attached type %1 already initialized in a parent scope."_L1.arg( + element.name()), + category, attachedLocation, suggestion); + } + + return; + } + + if (element.hasProperty(propertyName)) + return; // an actual property + + QQmlSA::Element type = resolveTypeInFileScope(propertyName); + QQmlSA::Element attached = resolveAttachedInFileScope(propertyName); + if (!type || !attached) + return; + + if (category == quickControlsAttachedPropertyReuse) { + for (QQmlSA::Element parent = attached; parent; parent = parent.baseType()) { + // ### TODO: Make it possible to resolve QQuickAttachedPropertyPropagator + // so that we don't have to compare the internal id + if (parent.internalId() == "QQuickAttachedPropertyPropagator"_L1) { + usedAttachedTypes.insert(readScope, {attached, location}); + break; + } + } + + } else { + usedAttachedTypes.insert(readScope, {attached, location}); + } +} + +void AttachedPropertyReuse::onWrite(const QQmlSA::Element &element, const QString &propertyName, + const QQmlSA::Element &value, const QQmlSA::Element &writeScope, + QQmlSA::SourceLocation location) +{ + Q_UNUSED(value); + onRead(element, propertyName, writeScope, location); +} + void QmlLintQuickPlugin::registerPasses(QQmlSA::PassManager *manager, const QQmlSA::Element &rootElement) { + const QQmlSA::LoggerWarningId attachedReuseCategory = [manager]() { + if (manager->isCategoryEnabled(quickAttachedPropertyReuse)) + return quickAttachedPropertyReuse; + if (manager->isCategoryEnabled(qmlAttachedPropertyReuse)) + return qmlAttachedPropertyReuse; + return quickControlsAttachedPropertyReuse; + }(); + const bool hasQuick = manager->hasImportedModule("QtQuick"); const bool hasQuickLayouts = manager->hasImportedModule("QtQuick.Layouts"); const bool hasQuickControls = manager->hasImportedModule("QtQuick.Templates") @@ -520,6 +609,7 @@ void QmlLintQuickPlugin::registerPasses(QQmlSA::PassManager *manager, if (hasQuick) { manager->registerElementPass(std::make_unique<AnchorsValidatorPass>(manager)); + manager->registerElementPass(std::make_unique<PropertyChangesValidatorPass>(manager)); auto forbiddenChildProperty = std::make_unique<ForbiddenChildrenPropertyValidatorPass>(manager); @@ -585,7 +675,7 @@ void QmlLintQuickPlugin::registerPasses(QQmlSA::PassManager *manager, { { "columnWidthProvider", { "", "function" } }, { "rowHeightProvider", { "", "function" } } }); addAttachedWarning({ "QtQuick", "Accessible" }, { { "QtQuick", "Item" } }, - "Accessible must be attached to an Item"); + "Accessible must be attached to an Item or an Action"); addAttachedWarning({ "QtQuick", "LayoutMirroring" }, { { "QtQuick", "Item" }, { "QtQuick", "Window" } }, "LayoutDirection attached property only works with Items and Windows"); @@ -598,8 +688,12 @@ void QmlLintQuickPlugin::registerPasses(QQmlSA::PassManager *manager, addAttachedWarning({ "QtQuick.Layouts", "StackLayout" }, { { "QtQuick", "Item" } }, "StackLayout must be attached to an Item"); } + + if (hasQuickControls) { manager->registerElementPass(std::make_unique<ControlsSwipeDelegateValidatorPass>(manager)); + manager->registerPropertyPass(std::make_unique<AttachedPropertyReuse>( + manager, attachedReuseCategory), "", ""); addAttachedWarning({ "QtQuick.Templates", "ScrollBar" }, { { "QtQuick", "Flickable" }, { "QtQuick.Templates", "ScrollView" } }, @@ -629,6 +723,9 @@ void QmlLintQuickPlugin::registerPasses(QQmlSA::PassManager *manager, addVarBindingWarning("QtQuick.Templates", "SpinBox", { { "textFromValue", { "", "function" } }, { "valueFromText", { "", "function" } } }); + } else if (attachedReuseCategory != quickControlsAttachedPropertyReuse) { + manager->registerPropertyPass(std::make_unique<AttachedPropertyReuse>( + manager, attachedReuseCategory), "", ""); } if (manager->hasImportedModule(u"QtQuick.Controls.macOS"_s) @@ -636,6 +733,67 @@ void QmlLintQuickPlugin::registerPasses(QQmlSA::PassManager *manager, manager->registerElementPass(std::make_unique<ControlsNativeValidatorPass>(manager)); } +PropertyChangesValidatorPass::PropertyChangesValidatorPass(QQmlSA::PassManager *manager) + : QQmlSA::ElementPass(manager) + , m_propertyChanges(resolveType("QtQuick", "PropertyChanges")) +{ +} + +bool PropertyChangesValidatorPass::shouldRun(const QQmlSA::Element &element) +{ + return !m_propertyChanges.isNull() && element.inherits(m_propertyChanges); +} + +void PropertyChangesValidatorPass::run(const QQmlSA::Element &element) +{ + const QQmlSA::Binding::Bindings bindings = element.ownPropertyBindings(); + + const auto target = + std::find_if(bindings.constBegin(), bindings.constEnd(), + [](const auto binding) { return binding.propertyName() == u"target"_s; }); + if (target == bindings.constEnd()) + return; + + QString targetId = u"<id>"_s; + const auto targetLocation = target.value().sourceLocation(); + const QString targetBinding = sourceCode(targetLocation); + const QQmlSA::Element targetElement = resolveIdToElement(targetBinding, element); + if (!targetElement.isNull()) + targetId = targetBinding; + + bool hadCustomParsedBindings = false; + for (auto it = bindings.constBegin(); it != bindings.constEnd(); ++it) { + const auto &propertyName = it.key(); + const auto &propertyBinding = it.value(); + if (element.hasProperty(propertyName)) + continue; + + const QQmlSA::SourceLocation bindingLocation = propertyBinding.sourceLocation(); + if (!targetElement.isNull() && !targetElement.hasProperty(propertyName)) { + emitWarning( + "Unknown property \"%1\" in PropertyChanges."_L1.arg(propertyName), + quickPropertyChangesParsed, bindingLocation); + continue; + } + + QString binding = sourceCode(bindingLocation); + if (binding.length() > 16) + binding = binding.left(13) + "..."_L1; + + hadCustomParsedBindings = true; + emitWarning("Property \"%1\" is custom-parsed in PropertyChanges. " + "You should phrase this binding as \"%2.%1: %3\""_L1.arg(propertyName, targetId, + binding), + quickPropertyChangesParsed, bindingLocation); + } + + if (hadCustomParsedBindings && !targetElement.isNull()) { + emitWarning("You should remove any bindings on the \"target\" property and avoid " + "custom-parsed bindings in PropertyChanges.", + quickPropertyChangesParsed, targetLocation); + } +} + QT_END_NAMESPACE #include "moc_quicklintplugin.cpp" |