Source code
Revision control
Copy as Markdown
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
// Qt-Security score:significant reason:default
#include "qdslintplugin.h"
#include <QtCore/qlist.h>
#include <QtCore/qvarlengtharray.h>
#include <QtCore/qhash.h>
#include <QtCore/qset.h>
#include <QtCore/qspan.h>
#include <QtQmlCompiler/private/qqmljsscope_p.h>
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
using namespace QQmlSA;
// note: is a warning, but is prefixed Err to share the name with its QtC codemodel counterpart.
constexpr LoggerWarningId ErrFunctionsNotSupportedInQmlUi{
"QtDesignStudio.FunctionsNotSupportedInQmlUi"
};
constexpr LoggerWarningId WarnReferenceToParentItemNotSupportedByVisualDesigner{
"QtDesignStudio.ReferenceToParentItemNotSupportedByVisualDesigner"
};
constexpr LoggerWarningId WarnImperativeCodeNotEditableInVisualDesigner{
"QtDesignStudio.ImperativeCodeNotEditableInVisualDesigner"
};
constexpr LoggerWarningId ErrUnsupportedTypeInQmlUi{ "QtDesignStudio.UnsupportedTypeInQmlUi" };
constexpr LoggerWarningId ErrInvalidIdeInVisualDesigner{
"QtDesignStudio.InvalidIdeInVisualDesigner"
};
constexpr LoggerWarningId ErrUnsupportedRootTypeInQmlUi{
"QtDesignStudio.UnsupportedRootTypeInQmlUi"
};
constexpr LoggerWarningId qmlTranslationFunctionMismatch{
"QtDesignStudio.TranslationFunctionMismatch"
};
class FunctionCallValidator : public PropertyPass
{
public:
FunctionCallValidator(PassManager *manager)
: PropertyPass(manager)
, m_connectionsType(resolveType("QtQuick", "Connections")) {}
void onCall(const Element &element, const QString &propertyName, const Element &readScope,
SourceLocation location) override;
private:
Element m_connectionsType;
};
class QdsBindingValidator : public PropertyPass
{
public:
QdsBindingValidator(PassManager *manager, const Element &) : PropertyPass(manager) { }
void onRead(const QQmlSA::Element &element, const QString &propertyName,
const QQmlSA::Element &readScope, QQmlSA::SourceLocation location) override;
void onWrite(const QQmlSA::Element &element, const QString &propertyName,
const QQmlSA::Element &value, const QQmlSA::Element &writeScope,
QQmlSA::SourceLocation location) override;
};
class QdsElementValidator : public ElementPass
{
public:
QdsElementValidator(PassManager *passManager);
void run(const Element &element) override;
private:
void complainAboutFunctions(const Element &element);
static constexpr std::array s_unsupportedElementNames = {
std::make_pair("QtQuick.Controls"_L1, "ApplicationWindow"_L1),
std::make_pair("QtQuick.Controls"_L1, "Drawer"_L1),
std::make_pair("QtQml.Models"_L1, "Package"_L1),
std::make_pair("QtQuick"_L1, "ShaderEffect"_L1),
};
using UnsupportedName = decltype(s_unsupportedElementNames)::value_type;
std::array<Element, s_unsupportedElementNames.size()> m_unsupportedElements;
static constexpr std::array s_unsupportedRootNames = {
std::make_pair("QtQml.Models"_L1, "ListModel"_L1),
std::make_pair("QtQml.Models"_L1, "Package"_L1),
std::make_pair("QtQml"_L1, "Timer"_L1),
};
std::array<Element, s_unsupportedRootNames.size()> m_unsupportedRootElements;
std::array<Element, 2> m_supportFunctions;
Element m_qtObject;
};
class QQmlJSTranslationFunctionMismatchCheck : public QQmlSA::PropertyPass
{
public:
using QQmlSA::PropertyPass::PropertyPass;
void onCall(const QQmlSA::Element &element, const QString &propertyName,
const QQmlSA::Element &readScope, QQmlSA::SourceLocation location) override;
private:
enum TranslationType : quint8 { None, Normal, IdBased };
TranslationType m_lastTranslationFunction = None;
};
void QdsBindingValidator::onRead(const QQmlSA::Element &element, const QString &propertyName,
const QQmlSA::Element &readScope, QQmlSA::SourceLocation location)
{
Q_UNUSED(readScope);
if (element.isFileRootComponent() && propertyName == u"parent") {
emitWarning("Referencing the parent of the root item is not supported in a UI file (.ui.qml)",
WarnReferenceToParentItemNotSupportedByVisualDesigner, location);
}
}
void QdsBindingValidator::onWrite(const QQmlSA::Element &, const QString &propertyName,
const QQmlSA::Element &, const QQmlSA::Element &,
QQmlSA::SourceLocation location)
{
static constexpr std::array forbiddenAssignments = { "baseline"_L1,
"baselineOffset"_L1,
"bottomMargin"_L1,
"centerIn"_L1,
"color"_L1,
"fill"_L1,
"height"_L1,
"horizontalCenter"_L1,
"horizontalCenterOffset"_L1,
"left"_L1,
"leftMargin"_L1,
"margins"_L1,
"mirrored"_L1,
"opacity"_L1,
"right"_L1,
"rightMargin"_L1,
"rotation"_L1,
"scale"_L1,
"topMargin"_L1,
"verticalCenter"_L1,
"verticalCenterOffset"_L1,
"width"_L1,
"x"_L1,
"y"_L1,
"z"_L1 };
Q_ASSERT(std::is_sorted(forbiddenAssignments.cbegin(), forbiddenAssignments.cend()));
if (std::find(forbiddenAssignments.cbegin(), forbiddenAssignments.cend(), propertyName)
!= forbiddenAssignments.cend()) {
emitWarning("Imperative JavaScript assignments can break the visual tooling in Qt Design "
"Studio.",
WarnImperativeCodeNotEditableInVisualDesigner, location);
}
}
void QQmlJSTranslationFunctionMismatchCheck::onCall(const QQmlSA::Element &element,
const QString &propertyName,
const QQmlSA::Element &readScope,
QQmlSA::SourceLocation location)
{
Q_UNUSED(readScope);
const QQmlSA::Element globalJSObject = resolveBuiltinType(u"GlobalObject");
if (element != globalJSObject)
return;
constexpr std::array translationFunctions = {
"qsTranslate"_L1,
"QT_TRANSLATE_NOOP"_L1,
"qsTr"_L1,
"QT_TR_NOOP"_L1,
};
constexpr std::array idTranslationFunctions = {
"qsTrId"_L1,
"QT_TRID_NOOP"_L1,
};
const bool isTranslation =
std::find(translationFunctions.cbegin(), translationFunctions.cend(), propertyName)
!= translationFunctions.cend();
const bool isIdTranslation =
std::find(idTranslationFunctions.cbegin(), idTranslationFunctions.cend(), propertyName)
!= idTranslationFunctions.cend();
if (!isTranslation && !isIdTranslation)
return;
const TranslationType current = isTranslation ? Normal : IdBased;
if (m_lastTranslationFunction == None) {
m_lastTranslationFunction = current;
return;
}
if (m_lastTranslationFunction != current) {
emitWarning("Do not mix translation functions", qmlTranslationFunctionMismatch, location);
}
}
void QmlLintQdsPlugin::registerPasses(PassManager *manager, const Element &rootElement)
{
if (!rootElement.filePath().endsWith(u".ui.qml"))
return;
manager->registerPropertyPass(std::make_shared<FunctionCallValidator>(manager),
QAnyStringView(), QAnyStringView());
manager->registerPropertyPass(std::make_shared<QdsBindingValidator>(manager, rootElement),
QAnyStringView(), QAnyStringView());
manager->registerPropertyPass(std::make_unique<QQmlJSTranslationFunctionMismatchCheck>(manager),
QString(), QString(), QString());
manager->registerElementPass(std::make_unique<QdsElementValidator>(manager));
}
void FunctionCallValidator::onCall(const Element &element, const QString &propertyName,
const Element &readScope, SourceLocation location)
{
auto currentQmlScope = QQmlJSScope::findCurrentQMLScope(QQmlJSScope::scope(readScope));
// TODO: we would benefit from some public additional public QQmlSA API here.
// This should be considered in the context of QTBUG-138360
if (currentQmlScope && currentQmlScope->inherits(QQmlJSScope::scope(m_connectionsType)))
return;
// all math functions are allowed
const Element globalJSObject = resolveBuiltinType(u"GlobalObject");
const Element mathObjectType = globalJSObject.property(u"Math"_s).type();
if (element.inherits(mathObjectType))
return;
const Element qjsValue = resolveBuiltinType(u"QJSValue");
if (element.inherits(qjsValue)) {
// Workaround because the Date method has methods and those are only represented in
// QQmlJSTypePropagator as QJSValue.
// This is an overapproximation and might flag unrelated methods with the same name as ok
// even if they are not, but this is better than bogus warnings about the valid Date methods.
const std::array<QStringView, 4> dateMethodmethods{ u"now", u"parse", u"prototype",
u"UTC" };
if (auto it = std::find(dateMethodmethods.cbegin(), dateMethodmethods.cend(), propertyName);
it != dateMethodmethods.cend())
return;
}
static const std::vector<std::pair<Element, std::unordered_set<QString>>>
whiteListedFunctions = {
{ Element(),
{
// used on JS objects and many other types
u"valueOf"_s,
u"toString"_s,
u"toLocaleString"_s,
} },
{ globalJSObject,
{
u"isNaN"_s, u"isFinite"_s,
u"qsTr"_s, u"qsTrId"_s, u"qsTranslate"_s,
u"QT_TRANSLATE_NOOP"_s, u"QT_TRID_NOOP"_s, u"QT_TR_NOOP"_s,
}
},
{ resolveBuiltinType(u"ArrayPrototype"_s), { u"indexOf"_s, u"lastIndexOf"_s } },
{ resolveBuiltinType(u"NumberPrototype"_s),
{
u"isNaN"_s,
u"isFinite"_s,
u"toFixed"_s,
u"toExponential"_s,
u"toPrecision"_s,
u"isInteger"_s,
} },
{ resolveBuiltinType(u"StringPrototype"_s),
{
u"arg"_s,
u"toLowerCase"_s,
u"toLocaleLowerCase"_s,
u"toUpperCase"_s,
u"toLocaleUpperCase"_s,
u"substring"_s,
u"charAt"_s,
u"charCodeAt"_s,
u"concat"_s,
u"includes"_s,
u"endsWith"_s,
u"indexOf"_s,
u"lastIndexOf"_s,
} },
{ resolveType(u"QtQml"_s, u"Qt"_s),
{ u"lighter"_s, u"darker"_s, u"rgba"_s, u"tint"_s, u"hsla"_s, u"hsva"_s,
u"point"_s, u"rect"_s, u"size"_s, u"vector2d"_s, u"vector3d"_s, u"vector4d"_s,
u"quaternion"_s, u"matrix4x4"_s, u"formatDate"_s, u"formatDateTime"_s,
u"formatTime"_s, u"resolvedUrl"_s } },
};
for (const auto &[currentElement, methods] : whiteListedFunctions) {
if ((!currentElement || element.inherits(currentElement)) && methods.count(propertyName)) {
return;
}
}
// all other functions are forbidden
emitWarning(u"Arbitrary functions and function calls outside of a Connections object are not "
u"supported in a UI file (.ui.qml)",
ErrFunctionsNotSupportedInQmlUi, location);
}
QdsElementValidator::QdsElementValidator(PassManager *manager) : ElementPass(manager)
{
auto loadTypes = [&manager, this](QSpan<const UnsupportedName> names, QSpan<Element> output) {
for (qsizetype i = 0; i < qsizetype(names.size()); ++i) {
if (!manager->hasImportedModule(names[i].first))
continue;
output[i] = resolveType(names[i].first, names[i].second);
}
};
loadTypes(s_unsupportedElementNames, m_unsupportedElements);
loadTypes(s_unsupportedRootNames, m_unsupportedRootElements);
m_qtObject = resolveType("QtQml"_L1, "QtObject"_L1);
m_supportFunctions = { resolveType("QtQml"_L1, "Connections"_L1),
resolveType("QtQuick"_L1, "ScriptAction"_L1) };
}
void QdsElementValidator::complainAboutFunctions(const Element &element)
{
for (const auto &method : element.ownMethods()) {
if (method.methodType() != QQmlSA::MethodType::Method)
continue;
emitWarning(
u"Arbitrary functions and function calls outside of a Connections object are not "
u"supported in a UI file (.ui.qml)",
ErrFunctionsNotSupportedInQmlUi, method.sourceLocation());
}
for (const auto &binding : element.ownPropertyBindings()) {
if (!binding.hasFunctionScriptValue())
continue;
emitWarning(u"Arbitrary functions and function calls outside of a Connections object "
u"are not "
u"supported in a UI file (.ui.qml)",
ErrFunctionsNotSupportedInQmlUi, binding.sourceLocation());
}
}
void QdsElementValidator::run(const Element &element)
{
enum WarningType { ForElements, ForRootElements };
auto warnIfElementIsUnsupported = [this, &element](WarningType warningType) {
QSpan<const Element> unsupportedComponents = warningType == ForElements
? QSpan<const Element>(m_unsupportedElements)
: QSpan<const Element>(m_unsupportedRootElements);
const QStringView message = warningType == ForElements
? u"This type (%1) is not supported in a UI file (.ui.qml)."_sv
: u"This type (%1) is not supported as a root element of a UI file (.ui.qml)."_sv;
const LoggerWarningId &id = warningType == ForElements ? ErrUnsupportedTypeInQmlUi
: ErrUnsupportedRootTypeInQmlUi;
for (const auto &unsupportedElement : unsupportedComponents) {
if (!element.inherits(unsupportedElement))
continue;
emitWarning(message.arg(element.baseTypeName()), id, element.sourceLocation());
break;
}
// special case: we don't want to warn on types indirectly inheriting from QtObject, for
// example Item.
if (warningType == ForRootElements && element.baseType() == m_qtObject)
emitWarning(message.arg(element.baseTypeName()), id, element.sourceLocation());
};
if (element.isFileRootComponent())
warnIfElementIsUnsupported(ForRootElements);
warnIfElementIsUnsupported(ForElements);
if (QString id = resolveElementToId(element, element); !id.isEmpty()) {
static constexpr std::array unsupportedNames = {
"action"_L1, "alias"_L1, "anchors"_L1, "as"_L1, "baseState"_L1,
"bool"_L1, "border"_L1, "bottom"_L1, "break"_L1, "case"_L1,
"catch"_L1, "clip"_L1, "color"_L1, "continue"_L1, "data"_L1,
"date"_L1, "debugger"_L1, "default"_L1, "delete"_L1, "do"_L1,
"double"_L1, "else"_L1, "enabled"_L1, "enumeration"_L1, "finally"_L1,
"flow"_L1, "focus"_L1, "font"_L1, "for"_L1, "function"_L1,
"height"_L1, "id"_L1, "if"_L1, "import"_L1, "in"_L1,
"instanceof"_L1, "int"_L1, "item"_L1, "layer"_L1, "left"_L1,
"list"_L1, "margin"_L1, "matrix4x4"_L1, "new"_L1, "opacity"_L1,
"padding"_L1, "parent"_L1, "point"_L1, "print"_L1, "quaternion"_L1,
"real"_L1, "rect"_L1, "return"_L1, "right"_L1, "scale"_L1,
"shaderInfo"_L1, "size"_L1, "source"_L1, "sprite"_L1, "spriteSequence"_L1,
"state"_L1, "string"_L1, "switch"_L1, "text"_L1, "texture"_L1,
"this"_L1, "throw"_L1, "time"_L1, "top"_L1, "try"_L1,
"typeof"_L1, "url"_L1, "var"_L1, "variant"_L1, "vector"_L1,
"vector2d"_L1, "vector3d"_L1, "vector4d"_L1, "visible"_L1, "void"_L1,
"while"_L1, "width"_L1, "with"_L1, "x"_L1, "y"_L1,
"z"_L1,
};
Q_ASSERT(std::is_sorted(unsupportedNames.begin(), unsupportedNames.end()));
if (std::binary_search(unsupportedNames.cbegin(), unsupportedNames.cend(), id)) {
emitWarning(
u"This id (%1) might be ambiguous and is not supported in a UI file (.ui.qml)."_s
.arg(id),
ErrInvalidIdeInVisualDesigner, element.idSourceLocation());
}
}
if (std::none_of(m_supportFunctions.cbegin(), m_supportFunctions.cend(),
[&element](const Element &base) { return element.inherits(base); })) {
complainAboutFunctions(element);
}
}
QT_END_NAMESPACE
#include "moc_qdslintplugin.cpp"