806 lines
34 KiB
C++
806 lines
34 KiB
C++
// Copyright (C) 2022 The Qt Company Ltd.
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial
|
|
|
|
#include "quicklintplugin.h"
|
|
|
|
#include <QtQmlCompiler/private/qqmljslogger_p.h>
|
|
#include <QtQmlCompiler/private/qqmljsutils_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 LoggerWarningId quickPropertyChangesParsed { "Quick.property-changes-parsed" };
|
|
static constexpr LoggerWarningId quickControlsAttachedPropertyReuse { "Quick.controls-attached-property-reuse" };
|
|
static constexpr LoggerWarningId quickAttachedPropertyReuse { "Quick.attached-property-reuse" };
|
|
|
|
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, quickLayoutPositioning, 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 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;
|
|
|
|
for (const QQmlJSMetaPropertyBinding &binding :
|
|
scopeUsedIn->parentScope()->propertyBindings(u"delegate"_s)) {
|
|
if (!binding.hasObject())
|
|
continue;
|
|
if (binding.objectType() == scopeUsedIn)
|
|
return;
|
|
}
|
|
}
|
|
|
|
emitWarning(warning->message, quickAttachedPropertyType, 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 { "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/"
|
|
"qtquickcontrols-customize.html#customization-"
|
|
"reference for more information.")
|
|
.arg(propertyName),
|
|
quickControlsNativeCustomize, 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.",
|
|
quickAnchorCombinations, 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.",
|
|
quickAnchorCombinations, 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.",
|
|
quickAnchorCombinations, 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),
|
|
quickAnchorCombinations, 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",
|
|
quickAnchorCombinations, ownBindingIterator->first->sourceLocation());
|
|
}
|
|
}
|
|
|
|
VarBindingTypeValidatorPass::VarBindingTypeValidatorPass(
|
|
QQmlSA::PassManager *manager,
|
|
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);
|
|
}
|
|
|
|
m_expectedPropertyTypes = propertyTypes;
|
|
}
|
|
|
|
void VarBindingTypeValidatorPass::onBinding(const QQmlSA::Element &element,
|
|
const QString &propertyName,
|
|
const QQmlJSMetaPropertyBinding &binding,
|
|
const QQmlSA::Element &bindingScope,
|
|
const QQmlSA::Element &value)
|
|
{
|
|
Q_UNUSED(element);
|
|
Q_UNUSED(bindingScope);
|
|
|
|
const auto range = m_expectedPropertyTypes.equal_range(propertyName);
|
|
|
|
if (range.first == range.second)
|
|
return;
|
|
|
|
QQmlSA::Element bindingType;
|
|
|
|
if (!value.isNull()) {
|
|
bindingType = value;
|
|
} else {
|
|
if (QQmlJSMetaPropertyBinding::isLiteralBinding(binding.bindingType())) {
|
|
bindingType = resolveLiteralType(binding);
|
|
} else {
|
|
switch (binding.bindingType()) {
|
|
case QQmlJSMetaPropertyBinding::Object:
|
|
bindingType = binding.objectType();
|
|
break;
|
|
case QQmlJSMetaPropertyBinding::Script:
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (std::find_if(range.first, range.second,
|
|
[&](const QQmlSA::Element &scope) { return bindingType->inherits(scope); })
|
|
== range.second) {
|
|
|
|
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
|
|
caused the breakage here will already cause another
|
|
warning somewhere else.
|
|
*/
|
|
return;
|
|
}
|
|
const QString bindingTypeName = QQmlJSScope::prettyName(
|
|
bindingTypeIsComposite
|
|
? bindingType->baseType()->internalName()
|
|
: bindingType->internalName());
|
|
QStringList expectedTypeNames;
|
|
|
|
for (auto it = range.first; it != range.second; it++)
|
|
expectedTypeNames << QQmlJSScope::prettyName(it.value()->internalName());
|
|
|
|
emitWarning(u"Unexpected type for property \"%1\" expected %2 got %3"_s.arg(
|
|
propertyName, expectedTypeNames.join(u", "_s), bindingTypeName),
|
|
quickUnexpectedVarType, binding.sourceLocation());
|
|
}
|
|
}
|
|
|
|
void AttachedPropertyReuse::onRead(
|
|
const QQmlSA::Element &element, const QString &propertyName,
|
|
const QQmlSA::Element &readScope, QQmlJS::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 QQmlJS::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 (QQmlJSScope::ConstPtr 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 QQmlJS::SourceLocation idInsertLocation {
|
|
attachedLocation.offset, 0,
|
|
attachedLocation.startLine, attachedLocation.startColumn
|
|
};
|
|
QQmlJSFixSuggestion suggestion {
|
|
"Reference it by id instead:"_L1,
|
|
idInsertLocation,
|
|
id.isEmpty() ? "<id>."_L1 : (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->internalName()),
|
|
category, attachedLocation, suggestion);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
if (element->hasProperty(propertyName))
|
|
return; // an actual property
|
|
|
|
QQmlSA::Element type = resolveTypeInFileScope(propertyName);
|
|
if (!type || type->attachedTypeName().isEmpty())
|
|
return;
|
|
|
|
const QQmlSA::Element attached = type->attachedType();
|
|
if (category == quickControlsAttachedPropertyReuse) {
|
|
for (QQmlSA::Element parent = attached; parent; parent = parent->baseType()) {
|
|
if (parent->internalName() == "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,
|
|
QQmlJS::SourceLocation location)
|
|
{
|
|
Q_UNUSED(value);
|
|
onRead(element, propertyName, writeScope, location);
|
|
}
|
|
|
|
void QmlLintQuickPlugin::registerPasses(QQmlSA::PassManager *manager,
|
|
const QQmlSA::Element &rootElement)
|
|
{
|
|
const 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")
|
|
|| manager->hasImportedModule("QtQuick.Controls")
|
|
|| manager->hasImportedModule("QtQuick.Controls.Basic");
|
|
|
|
Q_UNUSED(rootElement);
|
|
|
|
if (hasQuick) {
|
|
manager->registerElementPass(std::make_unique<AnchorsValidatorPass>(manager));
|
|
manager->registerElementPass(std::make_unique<PropertyChangesValidatorPass>(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 = [&](TypeDescription attachedType, QList<TypeDescription> allowedTypes,
|
|
QAnyStringView warning, bool allowInDelegate = false) {
|
|
QString attachedTypeName = attachedPropertyType->addWarning(attachedType, allowedTypes,
|
|
allowInDelegate, warning);
|
|
manager->registerPropertyPass(attachedPropertyType, attachedType.module,
|
|
u"$internal$."_s + attachedTypeName, {}, false);
|
|
};
|
|
|
|
auto addVarBindingWarning =
|
|
[&](QAnyStringView moduleName, QAnyStringView typeName,
|
|
const QMultiHash<QString, TypeDescription> &expectedPropertyTypes) {
|
|
auto varBindingType = std::make_shared<VarBindingTypeValidatorPass>(
|
|
manager, expectedPropertyTypes);
|
|
for (const auto &propertyName : expectedPropertyTypes.uniqueKeys()) {
|
|
manager->registerPropertyPass(varBindingType, moduleName, typeName,
|
|
propertyName);
|
|
}
|
|
};
|
|
|
|
if (hasQuick) {
|
|
addVarBindingWarning("QtQuick", "TableView",
|
|
{ { "columnWidthProvider", { "", "function" } },
|
|
{ "rowHeightProvider", { "", "function" } } });
|
|
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));
|
|
manager->registerPropertyPass(std::make_unique<AttachedPropertyReuse>(
|
|
manager, attachedReuseCategory), "", "");
|
|
|
|
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);
|
|
addVarBindingWarning("QtQuick.Templates", "Tumbler",
|
|
{ { "contentItem", { "QtQuick", "PathView" } },
|
|
{ "contentItem", { "QtQuick", "ListView" } } });
|
|
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)
|
|
|| manager->hasImportedModule(u"QtQuick.Controls.Windows"_s))
|
|
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 QMultiHash<QString, QQmlJSMetaPropertyBinding> bindings = element->ownPropertyBindings();
|
|
|
|
const auto target = bindings.find(u"target"_s);
|
|
if (target == bindings.end())
|
|
return;
|
|
|
|
QString targetId = u"<id>"_s;
|
|
const auto targetLocation = target->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.begin(), end = bindings.end(); it != end; ++it) {
|
|
const QString name = it->propertyName();
|
|
if (element->hasProperty(name))
|
|
continue;
|
|
|
|
const QQmlJS::SourceLocation bindingLocation = it->sourceLocation();
|
|
if (!targetElement.isNull() && !targetElement->hasProperty(name)) {
|
|
emitWarning(
|
|
"Unknown property \"%1\" in PropertyChanges."_L1.arg(name),
|
|
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(name, 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"
|