Replace signal name manipulations with QQmlSignalNames

Remove custom implementations found in qqmljs* and use the
static helper methods from qqmlsignalnames_p.h instead. This sometimes
requires to move some code around to avoid bugs with property that do
not have letters in their name.
Add a warning in the JS implementation of the SignalSpy.qml that the
used heuristic might fail on certain signal names.
Add tests in in tst_qqmllanguage to see if the property change handlers
work correctly for weird names.

Change-Id: I4dc73c34df7f77f529511fa04ab5fcc5385b59fc
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
This commit is contained in:
Sami Shalayel 2023-08-10 09:45:37 +02:00
parent 8d6f9e716d
commit a1ce0596e5
34 changed files with 293 additions and 246 deletions

View File

@ -15,6 +15,7 @@
#include <private/qqmlvaluetype_p.h>
#include <private/qqmlvmemetaobject_p.h>
#include <private/qqmlexpression_p.h>
#include <private/qqmlsignalnames_p.h>
#include <QtCore/qdebug.h>
#include <QtCore/qmetaobject.h>
@ -119,22 +120,14 @@ QDataStream &operator>>(QDataStream &ds,
return ds;
}
static inline bool isSignalPropertyName(const QString &signalName)
{
// see QmlCompiler::isSignalPropertyName
return signalName.size() >= 3 && signalName.startsWith(QLatin1String("on")) &&
signalName.at(2).isLetter() && signalName.at(2).isUpper();
}
static bool hasValidSignal(QObject *object, const QString &propertyName)
{
if (!isSignalPropertyName(propertyName))
auto signalName = QQmlSignalNames::handlerNameToSignalName(propertyName);
if (!signalName)
return false;
QString signalName = propertyName.mid(2);
signalName[0] = signalName.at(0).toLower();
int sigIdx = QQmlPropertyPrivate::findSignalByName(object->metaObject(), signalName.toLatin1()).methodIndex();
int sigIdx = QQmlPropertyPrivate::findSignalByName(object->metaObject(), signalName->toLatin1())
.methodIndex();
if (sigIdx == -1)
return false;
@ -305,10 +298,8 @@ void QQmlEngineDebugServiceImpl::buildObjectDump(QDataStream &message,
if (scope) {
const QByteArray methodName = QMetaObjectPrivate::signal(scope->metaObject(),
signalHandler->signalIndex()).name();
const QLatin1String methodNameStr(methodName);
if (methodNameStr.size() != 0) {
prop.name = QLatin1String("on") + QChar(methodNameStr.at(0)).toUpper()
+ methodNameStr.mid(1);
if (!methodName.isEmpty()) {
prop.name = QQmlSignalNames::signalNameToHandlerName(methodName);
}
}
}

View File

@ -14,10 +14,10 @@ using namespace Qt::Literals;
static std::optional<qsizetype> firstLetterIdx(QStringView name, qsizetype removePrefix = 0,
qsizetype removeSuffix = 0)
{
auto result = std::find_if(std::next(name.cbegin(), removePrefix),
std::prev(name.cend(), removeSuffix),
auto end = std::prev(name.cend(), removeSuffix);
auto result = std::find_if(std::next(name.cbegin(), removePrefix), end,
[](const QChar &c) { return c.isLetter(); });
if (result != name.cend())
if (result != end)
return std::distance(name.begin(), result);
return {};
@ -157,23 +157,41 @@ QString QQmlSignalNames::signalNameToHandlerName(QAnyStringView signal)
return handlerName;
}
/*!
\internal
Returns a signal name from \a handlerName string.
*/
std::optional<QString> QQmlSignalNames::handlerNameToSignalName(QStringView handler)
enum HandlerType { ChangedHandler, Handler };
static std::optional<QString> handlerNameToSignalNameHelper(QStringView handler, HandlerType type)
{
if (!isHandlerName(handler))
if (!QQmlSignalNames::isHandlerName(handler))
return {};
QString signalName = handler.sliced(strlen("on")).toString();
if (signalName.isEmpty())
return {};
changeCaseOfFirstLetter(signalName, ToLower);
changeCaseOfFirstLetter(signalName, ToLower, 0, type == ChangedHandler ? strlen("Changed") : 0);
return signalName;
}
/*!
\internal
Returns a signal name from \a handlerName string. Do not use it on changed handlers, see
changedHandlerNameToSignalName for that!
*/
std::optional<QString> QQmlSignalNames::handlerNameToSignalName(QStringView handler)
{
return handlerNameToSignalNameHelper(handler, Handler);
}
/*!
\internal
Returns a signal name from \a changedHandlerName string. Makes sure not to lowercase the 'C' from
Changed.
*/
std::optional<QString> QQmlSignalNames::changedHandlerNameToSignalName(QStringView handler)
{
return handlerNameToSignalNameHelper(handler, ChangedHandler);
}
bool QQmlSignalNames::isChangedSignalName(QStringView signalName)
{
const qsizetype smallestAllowedSize = strlen("XChanged");

View File

@ -42,6 +42,7 @@ public:
static std::optional<QByteArray> changedHandlerNameToPropertyName(QUtf8StringView handler);
static std::optional<QString> handlerNameToSignalName(QStringView handler);
static std::optional<QString> changedHandlerNameToSignalName(QStringView changedHandler);
static bool isChangedHandlerName(QStringView signalName);
static bool isChangedSignalName(QStringView signalName);

View File

@ -442,38 +442,6 @@ bool IRBuilder::generateFromQml(const QString &code, const QString &url, Documen
return errors.isEmpty();
}
bool IRBuilder::isSignalPropertyName(const QString &name)
{
if (name.size() < 3) return false;
if (!name.startsWith(QLatin1String("on"))) return false;
int ns = name.size();
for (int i = 2; i < ns; ++i) {
const QChar curr = name.at(i);
if (curr.unicode() == '_') continue;
if (curr.isUpper()) return true;
return false;
}
return false; // consists solely of underscores - invalid.
}
QString IRBuilder::signalNameFromSignalPropertyName(const QString &signalPropertyName)
{
Q_ASSERT(signalPropertyName.startsWith(QLatin1String("on")));
QString signalNameCandidate = signalPropertyName;
signalNameCandidate.remove(0, 2);
// Note that the property name could start with any alpha or '_' or '$' character,
// so we need to do the lower-casing of the first alpha character.
for (int firstAlphaIndex = 0; firstAlphaIndex < signalNameCandidate.size(); ++firstAlphaIndex) {
if (signalNameCandidate.at(firstAlphaIndex).isUpper()) {
signalNameCandidate[firstAlphaIndex] = signalNameCandidate.at(firstAlphaIndex).toLower();
return signalNameCandidate;
}
}
Q_UNREACHABLE_RETURN(QString());
}
bool IRBuilder::visit(QQmlJS::AST::UiArrayMemberList *ast)
{
return QQmlJS::AST::Visitor::visit(ast);

View File

@ -506,9 +506,6 @@ public:
IRBuilder(const QSet<QString> &illegalNames);
bool generateFromQml(const QString &code, const QString &url, Document *output);
static bool isSignalPropertyName(const QString &name);
static QString signalNameFromSignalPropertyName(const QString &signalPropertyName);
using QQmlJS::AST::Visitor::visit;
using QQmlJS::AST::Visitor::endVisit;

View File

@ -20,6 +20,7 @@
#include <private/qqmlbuiltinfunctions_p.h>
#include <private/qqmlirbuilder_p.h>
#include <QtQml/private/qqmllist_p.h>
#include <QtQml/private/qqmlsignalnames_p.h>
#include <QStringList>
#include <QVector>
@ -363,11 +364,10 @@ void QQmlPropertyPrivate::initProperty(QObject *obj, const QString &name,
};
QQmlData *ddata = QQmlData::get(currentObject, false);
auto findChangeSignal = [&](QStringView signalName) {
const QString changed = QStringLiteral("Changed");
if (signalName.endsWith(changed)) {
const QStringView propName = signalName.first(signalName.size() - changed.size());
const QQmlPropertyData *d = ddata->propertyCache->property(propName, currentObject, context);
auto findChangeSignal = [&](QStringView changedHandlerName) {
if (auto propName = QQmlSignalNames::changedHandlerNameToPropertyName(changedHandlerName)) {
const QQmlPropertyData *d =
ddata->propertyCache->property(*propName, currentObject, context);
while (d && d->isFunction())
d = ddata->propertyCache->overrideData(d);
@ -381,19 +381,11 @@ void QQmlPropertyPrivate::initProperty(QObject *obj, const QString &name,
};
const QString terminalString = terminal.toString();
if (QmlIR::IRBuilder::isSignalPropertyName(terminalString)) {
QString signalName = terminalString.mid(2);
int firstNon_;
int length = signalName.size();
for (firstNon_ = 0; firstNon_ < length; ++firstNon_)
if (signalName.at(firstNon_) != u'_')
break;
signalName[firstNon_] = signalName.at(firstNon_).toLower();
if (auto signalName = QQmlSignalNames::handlerNameToSignalName(terminalString)) {
if (ddata && ddata->propertyCache) {
// Try method
const QQmlPropertyData *d = ddata->propertyCache->property(
signalName, currentObject, context);
const QQmlPropertyData *d =
ddata->propertyCache->property(*signalName, currentObject, context);
// ### Qt7: This code treats methods as signals. It should use d->isSignal().
// That would be a change in behavior, though. Right now you can construct a
@ -407,9 +399,9 @@ void QQmlPropertyPrivate::initProperty(QObject *obj, const QString &name,
return;
}
if (findChangeSignal(signalName))
if (findChangeSignal(terminalString))
return;
} else if (findSignalInMetaObject(signalName.toUtf8())) {
} else if (findSignalInMetaObject(signalName->toUtf8())) {
return;
}
}
@ -744,15 +736,7 @@ QString QQmlProperty::name() const
d->nameCache = d->core.name(d->object) + QLatin1Char('.') + QString::fromUtf8(vtName);
} else if (type() & SignalProperty) {
// ### Qt7: Return the original signal name here. Do not prepend "on"
QString name = QStringLiteral("on") + d->core.name(d->object);
for (int i = 2, end = name.size(); i != end; ++i) {
const QChar c = name.at(i);
if (c != u'_') {
name[i] = c.toUpper();
break;
}
}
d->nameCache = name;
d->nameCache = QQmlSignalNames::signalNameToHandlerName(d->core.name(d->object));
} else {
d->nameCache = d->core.name(d->object);
}
@ -1956,9 +1940,8 @@ QMetaMethod QQmlPropertyPrivate::findSignalByName(const QMetaObject *mo, const Q
// If no signal is found, but the signal is of the form "onBlahChanged",
// return the notify signal for the property "Blah"
if (name.endsWith("Changed")) {
QByteArray propName = name.mid(0, name.size() - 7);
int propIdx = mo->indexOfProperty(propName.constData());
if (auto propName = QQmlSignalNames::changedSignalNameToPropertyName(name)) {
int propIdx = mo->indexOfProperty(propName->constData());
if (propIdx >= 0) {
QMetaProperty prop = mo->property(propIdx);
if (prop.hasNotifySignal())

View File

@ -10,6 +10,7 @@
#include <private/qmetaobject_p.h>
#include <private/qmetaobjectbuilder_p.h>
#include <private/qqmlpropertycachemethodarguments_p.h>
#include <private/qqmlsignalnames_p.h>
#include <private/qv4value_p.h>
@ -249,8 +250,7 @@ void QQmlPropertyCache::appendSignal(const QString &name, QQmlPropertyData::Flag
int signalHandlerIndex = signalHandlerIndexCache.size();
signalHandlerIndexCache.append(handler);
QString handlerName = QLatin1String("on") + name;
handlerName[2] = handlerName.at(2).toUpper();
const QString handlerName = QQmlSignalNames::signalNameToHandlerName(name);
setNamedProperty(name, methodIndex + methodOffset(), methodIndexCache.data() + methodIndex);
setNamedProperty(handlerName, signalHandlerIndex + signalOffset(),
@ -449,7 +449,7 @@ void QQmlPropertyCache::append(const QMetaObject *metaObject,
setNamedProperty(methodName, ii, data);
if (data->isSignal()) {
QHashedString on(QLatin1String("on") % methodName.at(0).toUpper() % QStringView{methodName}.mid(1));
QHashedString on(QQmlSignalNames::signalNameToHandlerName(methodName));
setNamedProperty(on, ii, sigdata);
++signalHandlerIndex;
}
@ -462,17 +462,8 @@ void QQmlPropertyCache::append(const QMetaObject *metaObject,
setNamedProperty(methodName, ii, data);
if (data->isSignal()) {
int length = methodName.length();
QVarLengthArray<char, 128> str(length+3);
str[0] = 'o';
str[1] = 'n';
str[2] = QtMiscUtils::toAsciiUpper(rawName[0]);
if (length > 1)
memcpy(&str[3], &rawName[1], length - 1);
str[length + 2] = '\0';
QHashedString on(QString::fromLatin1(str.data()));
QHashedString on(QQmlSignalNames::signalNameToHandlerName(
QLatin1StringView{ methodName.constData(), methodName.length() }));
setNamedProperty(on, ii, data);
++signalHandlerIndex;
}

View File

@ -21,6 +21,7 @@
#include <private/qqmltypedata_p.h>
#include <private/inlinecomponentutils_p.h>
#include <private/qqmlsourcecoordinate_p.h>
#include <private/qqmlsignalnames_p.h>
#include <QScopedValueRollback>
#include <vector>
@ -486,7 +487,8 @@ inline QQmlError QQmlPropertyCacheCreator<ObjectContainer>::createMetaObject(
for ( ; p != pend; ++p) {
auto flags = QQmlPropertyData::defaultSignalFlags();
QString changedSigName = stringAt(p->nameIndex) + QLatin1String("Changed");
const QString changedSigName =
QQmlSignalNames::propertyNameToChangedSignalName(stringAt(p->nameIndex));
seenSignals.insert(changedSigName);
cache->appendSignal(changedSigName, flags, effectiveMethodIndex++);
@ -497,7 +499,8 @@ inline QQmlError QQmlPropertyCacheCreator<ObjectContainer>::createMetaObject(
for ( ; a != aend; ++a) {
auto flags = QQmlPropertyData::defaultSignalFlags();
QString changedSigName = stringAt(a->nameIndex()) + QLatin1String("Changed");
const QString changedSigName =
QQmlSignalNames::propertyNameToChangedSignalName(stringAt(a->nameIndex()));
seenSignals.insert(changedSigName);
cache->appendSignal(changedSigName, flags, effectiveMethodIndex++);

View File

@ -3,6 +3,7 @@
#include "qqmlpropertyresolver_p.h"
#include <private/qqmlcontextdata_p.h>
#include <private/qqmlsignalnames_p.h>
QT_BEGIN_NAMESPACE
@ -43,10 +44,8 @@ const QQmlPropertyData *QQmlPropertyResolver::signal(const QString &name, bool *
return d;
}
if (name.endsWith(QLatin1String("Changed"))) {
QString propName = name.mid(0, name.size() - static_cast<int>(strlen("Changed")));
d = property(propName, notInRevision);
if (auto propName = QQmlSignalNames::changedSignalNameToPropertyName(name)) {
d = property(*propName, notInRevision);
if (d)
return cache->signal(d->notifyIndex());
}

View File

@ -9,6 +9,7 @@
#include <private/qqmlpropertycachecreator_p.h>
#include <private/qqmlpropertyresolver_p.h>
#include <private/qqmlstringconverters_p.h>
#include <private/qqmlsignalnames_p.h>
#include <QtCore/qdatetime.h>
@ -140,7 +141,7 @@ QVector<QQmlError> QQmlPropertyValidator::validateObject(
customBindings << binding;
continue;
}
} else if (QmlIR::IRBuilder::isSignalPropertyName(name)
} else if (QQmlSignalNames::isHandlerName(name)
&& !(customParser->flags() & QQmlCustomParser::AcceptsSignalHandlers)) {
customBindings << binding;
continue;

View File

@ -9,6 +9,7 @@
#include <private/qqmlcomponent_p.h>
#include <private/qqmlpropertyresolver_p.h>
#include <private/qqmlcomponentandaliasresolver_p.h>
#include <private/qqmlsignalnames_p.h>
#define COMPILE_EXCEPTION(token, desc) \
{ \
@ -325,18 +326,21 @@ bool SignalHandlerResolver::resolveSignalHandlerExpressions(
continue;
}
if (!QmlIR::IRBuilder::isSignalPropertyName(bindingPropertyName))
QString qPropertyName;
QString signalName;
if (auto propertyName =
QQmlSignalNames::changedHandlerNameToPropertyName(bindingPropertyName)) {
qPropertyName = *propertyName;
signalName = *QQmlSignalNames::changedHandlerNameToSignalName(bindingPropertyName);
} else {
signalName = QQmlSignalNames::handlerNameToSignalName(bindingPropertyName)
.value_or(QString());
}
if (signalName.isEmpty())
continue;
QQmlPropertyResolver resolver(propertyCache);
const QString signalName = QmlIR::IRBuilder::signalNameFromSignalPropertyName(
bindingPropertyName);
QString qPropertyName;
if (signalName.endsWith(QLatin1String("Changed")))
qPropertyName = signalName.mid(0, signalName.size() - static_cast<int>(strlen("Changed")));
bool notInRevision = false;
const QQmlPropertyData * const signal = resolver.signal(signalName, &notInRevision);
const QQmlPropertyData * const signalPropertyData = resolver.property(signalName, /*notInRevision ptr*/nullptr);
@ -1045,7 +1049,7 @@ bool QQmlDeferredAndCustomParserBindingScanner::scanObject(
obj->flags |= Object::HasCustomParserBindings;
continue;
}
} else if (QmlIR::IRBuilder::isSignalPropertyName(name)
} else if (QQmlSignalNames::isHandlerName(name)
&& !(customParser->flags() & QQmlCustomParser::AcceptsSignalHandlers)) {
obj->flags |= Object::HasCustomParserBindings;
binding->setFlag(Binding::IsCustomParserBinding);

View File

@ -15,6 +15,8 @@
#include <QtCore/qdebug.h>
#include <QtCore/qstringlist.h>
#include <QtQml/private/qqmlsignalnames_p.h>
#include <private/qobject_p.h>
QT_BEGIN_NAMESPACE
@ -205,9 +207,7 @@ void QQmlConnectionsParser::verifyBindings(const QQmlRefPointer<QV4::ExecutableC
const QV4::CompiledData::Binding *binding = props.at(ii);
const QString &propName = compilationUnit->stringAt(binding->propertyNameIndex);
const bool thirdCharacterIsValid = (propName.size() >= 2)
&& (propName.at(2).isUpper() || propName.at(2) == u'_');
if (!propName.startsWith(QLatin1String("on")) || !thirdCharacterIsValid) {
if (!QQmlSignalNames::isHandlerName(propName)) {
error(props.at(ii), QQmlConnections::tr("Cannot assign to non-existent property \"%1\"").arg(propName));
return;
}

View File

@ -19,6 +19,8 @@
#include <QtCore/qfileinfo.h>
#include <QtCore/qloggingcategory.h>
#include <QtQml/private/qqmlsignalnames_p.h>
#include <limits>
QT_BEGIN_NAMESPACE
@ -123,9 +125,7 @@ static bool checkArgumentsObjectUseInSignalHandlers(const QmlIR::Document &doc,
if (binding->type() != QV4::CompiledData::Binding::Type_Script)
continue;
const QString propName = doc.stringAt(binding->propertyNameIndex);
if (!propName.startsWith(QLatin1String("on"))
|| propName.size() < 3
|| !propName.at(2).isUpper())
if (!QQmlSignalNames::isHandlerName(propName))
continue;
auto compiledFunction = doc.jsModule.functions.value(object->runtimeFunctionIndices.at(binding->value.compiledScriptIndex));
if (!compiledFunction)

View File

@ -8,6 +8,8 @@
#include <QtCore/qloggingcategory.h>
#include <QtCore/qfileinfo.h>
#include <QtQml/private/qqmlsignalnames_p.h>
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
@ -166,44 +168,45 @@ QQmlJSCompilePass::Function QQmlJSFunctionInitializer::run(
}
function.isProperty = m_objectType->hasProperty(propertyName);
if (!function.isProperty && QmlIR::IRBuilder::isSignalPropertyName(propertyName)) {
const QString signalName = QmlIR::IRBuilder::signalNameFromSignalPropertyName(propertyName);
if (signalName.endsWith(u"Changed"_s)
&& m_objectType->hasProperty(signalName.chopped(strlen("Changed")))) {
function.isSignalHandler = true;
} else {
const auto methods = m_objectType->methods(signalName);
for (const auto &method : methods) {
if (method.isCloned())
continue;
if (method.methodType() == QQmlJSMetaMethodType::Signal) {
function.isSignalHandler = true;
const auto arguments = method.parameters();
for (qsizetype i = 0, end = arguments.size(); i < end; ++i) {
const auto &type = arguments[i].type();
if (type.isNull()) {
diagnose(u"Cannot resolve the argument type %1."_s.arg(
arguments[i].typeName()),
QtDebugMsg, bindingLocation, error);
function.argumentTypes.append(
if (!function.isProperty) {
if (QQmlSignalNames::isHandlerName(propertyName)) {
if (auto actualPropertyName =
QQmlSignalNames::changedHandlerNameToPropertyName(propertyName);
actualPropertyName && m_objectType->hasProperty(*actualPropertyName)) {
function.isSignalHandler = true;
} else {
auto signalName = QQmlSignalNames::handlerNameToSignalName(propertyName);
const auto methods = m_objectType->methods(*signalName);
for (const auto &method : methods) {
if (method.isCloned())
continue;
if (method.methodType() == QQmlJSMetaMethodType::Signal) {
function.isSignalHandler = true;
const auto arguments = method.parameters();
for (qsizetype i = 0, end = arguments.size(); i < end; ++i) {
const auto &type = arguments[i].type();
if (type.isNull()) {
diagnose(u"Cannot resolve the argument type %1."_s.arg(
arguments[i].typeName()),
QtDebugMsg, bindingLocation, error);
function.argumentTypes.append(
m_typeResolver->tracked(
m_typeResolver->globalType(m_typeResolver->varType())));
} else {
function.argumentTypes.append(m_typeResolver->tracked(
m_typeResolver->globalType(type)));
m_typeResolver->globalType(m_typeResolver->varType())));
} else {
function.argumentTypes.append(m_typeResolver->tracked(
m_typeResolver->globalType(type)));
}
}
break;
}
break;
}
if (!function.isSignalHandler) {
diagnose(u"Could not compile signal handler for %1: The signal does not exist"_s.arg(
*signalName),
QtWarningMsg, bindingLocation, error);
}
}
}
if (!function.isSignalHandler) {
diagnose(u"Could not compile signal handler for %1: The signal does not exist"_s.arg(
signalName),
QtWarningMsg, bindingLocation, error);
}
}
if (!function.isSignalHandler) {

View File

@ -13,6 +13,7 @@
#include <QtCore/qrect.h>
#include <QtCore/qsize.h>
#include <QtQml/private/qqmlsignalnames_p.h>
#include <QtQml/private/qv4codegen_p.h>
#include <QtQml/private/qqmlstringconverters_p.h>
#include <QtQml/private/qqmlirbuilder_p.h>
@ -963,7 +964,7 @@ void QQmlJSImportVisitor::checkSignal(
const QQmlJSScope::ConstPtr &signalScope, const QQmlJS::SourceLocation &location,
const QString &handlerName, const QStringList &handlerParameters)
{
const auto signal = QQmlJSUtils::signalName(handlerName);
const auto signal = QQmlSignalNames::handlerNameToSignalName(handlerName);
std::optional<QQmlJSMetaMethod> signalMethod;
const auto setSignalMethod = [&](const QQmlJSScope::ConstPtr &scope, const QString &name) {
@ -975,8 +976,7 @@ void QQmlJSImportVisitor::checkSignal(
if (signal.has_value()) {
if (signalScope->hasMethod(*signal)) {
setSignalMethod(signalScope, *signal);
} else if (auto p = QQmlJSUtils::changeHandlerProperty(signalScope, *signal);
p.has_value()) {
} else if (auto p = QQmlJSUtils::propertyFromChangedHandler(signalScope, handlerName)) {
// we have a change handler of the form "onXChanged" where 'X'
// is a property name
@ -2025,11 +2025,11 @@ bool QQmlJSImportVisitor::visit(UiScriptBinding *scriptBinding)
prefix.clear();
}
auto name = group->name.toString();
const auto name = group->name.toString();
// This is a preliminary check.
// Even if the name starts with "on", it might later turn out not to be a signal.
const auto signal = QQmlJSUtils::signalName(name);
const auto signal = QQmlSignalNames::handlerNameToSignalName(name);
if (!signal.has_value() || m_currentScope->hasProperty(name)) {
m_propertyBindings[m_currentScope].append(
@ -2079,7 +2079,7 @@ bool QQmlJSImportVisitor::visit(UiScriptBinding *scriptBinding)
if (!methods.isEmpty()) {
kind = QQmlSA::ScriptBindingKind::Script_SignalHandler;
checkSignal(scope, groupLocation, name, signalParameters);
} else if (QQmlJSUtils::changeHandlerProperty(scope, signalName).has_value()) {
} else if (QQmlJSUtils::propertyFromChangedHandler(scope, name).has_value()) {
kind = QQmlSA::ScriptBindingKind::Script_ChangeHandler;
checkSignal(scope, groupLocation, name, signalParameters);
} else if (scope->hasProperty(name)) {

View File

@ -81,7 +81,8 @@ void QQmlJSScope::insertJSIdentifier(const QString &name, const JavaScriptIdenti
void QQmlJSScope::insertPropertyIdentifier(const QQmlJSMetaProperty &property)
{
addOwnProperty(property);
QQmlJSMetaMethod method(property.propertyName() + u"Changed"_s, u"void"_s);
QQmlJSMetaMethod method(
QQmlSignalNames::propertyNameToChangedSignalName(property.propertyName()), u"void"_s);
method.setMethodType(QQmlJSMetaMethodType::Signal);
method.setIsImplicitQmlPropertyChangeSignal(true);
addOwnMethod(method);

View File

@ -25,6 +25,7 @@
#include <QtCore/qstring.h>
#include <QtCore/qstringview.h>
#include <QtCore/qstringbuilder.h>
#include <QtQml/private/qqmlsignalnames_p.h>
#include <private/qduplicatetracker_p.h>
#include <optional>
@ -99,26 +100,6 @@ struct Q_QMLCOMPILER_PRIVATE_EXPORT QQmlJSUtils
return type;
}
/*! \internal
Returns a signal name from \a handlerName string.
*/
static std::optional<QString> signalName(QStringView handlerName)
{
if (handlerName.startsWith(u"on") && handlerName.size() > 2) {
QString signal = handlerName.mid(2).toString();
for (int i = 0; i < signal.size(); ++i) {
QChar &ch = signal[i];
if (ch.isLower())
return {};
if (ch.isUpper()) {
ch = ch.toLower();
return signal;
}
}
}
return {};
}
static std::optional<QQmlJSMetaProperty>
changeHandlerProperty(const QQmlJSScope::ConstPtr &scope, QStringView signalName)
{
@ -134,6 +115,21 @@ struct Q_QMLCOMPILER_PRIVATE_EXPORT QQmlJSUtils
return {};
}
static std::optional<QQmlJSMetaProperty>
propertyFromChangedHandler(const QQmlJSScope::ConstPtr &scope, QStringView changedHandler)
{
auto signalName = QQmlSignalNames::changedHandlerNameToPropertyName(changedHandler);
if (!signalName)
return {};
auto p = scope->property(*signalName);
const bool isBindable = !p.bindable().isEmpty();
const bool canNotify = !p.notify().isEmpty();
if (p.isValid() && (isBindable || canNotify))
return p;
return {};
}
static bool hasCompositeBase(const QQmlJSScope::ConstPtr &scope)
{
if (!scope)

View File

@ -22,6 +22,7 @@
#include <QtQml/private/qqmljsast_p.h>
#include <QtQml/private/qqmljsengine_p.h>
#include <QtQml/private/qqmlsignalnames_p.h>
#include <QtCore/QCborValue>
#include <QtCore/QCborMap>
@ -554,9 +555,7 @@ public:
bool isSignalHandler() const
{
QString baseName = m_name.split(QLatin1Char('.')).last();
if (baseName.startsWith(u"on") && baseName.size() > 2 && baseName.at(2).isUpper())
return true;
return false;
return QQmlSignalNames::isHandlerName(baseName);
}
static QString preCodeForName(QStringView n)
{

View File

@ -10,6 +10,7 @@
#include <QtCore/QRegularExpression>
#include <QtQmlDom/private/qqmldomexternalitems_p.h>
#include <QtQmlDom/private/qqmldomtop_p.h>
#include <QtQml/private/qqmlsignalnames_p.h>
QT_BEGIN_NAMESPACE
using namespace QLspSpecification;
@ -287,8 +288,7 @@ static QList<CompletionItem> bindingsCompletions(DomItem &containingObject)
if (it.value().methodType == MethodInfo::MethodType::Signal) {
CompletionItem comp;
QString signal = it.key();
comp.label =
(u"on"_s + signal.at(0).toUpper() + signal.mid(1)).toUtf8();
comp.label = QQmlSignalNames::signalNameToHandlerName(signal).toUtf8();
res.append(comp);
}
++it;

View File

@ -20,6 +20,7 @@
#include <private/qqmlirbuilder_p.h>
#include <QtCore/qdebug.h>
#include <QtQml/private/qqmlsignalnames_p.h>
#include <private/qobject_p.h>
@ -267,7 +268,7 @@ void QQuickPropertyChangesPrivate::decodeBinding(const QString &propertyPrefix,
break;
}
if (binding->isSignalHandler() || QmlIR::IRBuilder::isSignalPropertyName(propertyName)) {
if (binding->isSignalHandler() || QQmlSignalNames::isHandlerName(propertyName)) {
QQmlProperty prop = property(propertyName);
if (prop.isSignalProperty()) {
QQuickReplaceSignalHandler *handler = new QQuickReplaceSignalHandler;

View File

@ -37,6 +37,8 @@ void tst_qml_common::tst_propertyNameToChangedHandlerName_data()
QTest::addRow("___123aProperty") << u"___123a"_s << u"on___123AChanged"_s;
QTest::addRow("___123Property") << u"___123"_s << u"on___123Changed"_s;
QTest::addRow("AProperty") << u"A"_s << u"onAChanged"_s;
QTest::addRow("_Property") << u"_"_s << u"on_Changed"_s;
QTest::addRow("$Property") << u"$"_s << u"on$Changed"_s;
}
void tst_qml_common::tst_propertyNameToChangedHandlerName()
{
@ -67,6 +69,8 @@ void tst_qml_common::tst_signalNameToHandlerName_data()
QTest::addRow("___123aProperty") << u"___123a"_s << u"on___123A"_s;
QTest::addRow("___123Property") << u"___123"_s << u"on___123"_s;
QTest::addRow("AProperty") << u"A"_s << u"onA"_s;
QTest::addRow("_Property") << u"_"_s << u"on_"_s;
QTest::addRow("$Property") << u"$"_s << u"on$"_s;
}
void tst_qml_common::tst_signalNameToHandlerName()
@ -150,6 +154,8 @@ void tst_qml_common::tst_isChangedHandlerName_data()
QTest::addRow("empty") << u""_s << false;
QTest::addRow("empty2") << u"onChanged"_s << false;
QTest::addRow("on") << u"on"_s << false;
QTest::addRow("on_Changed") << u"on_Changed"_s << true;
QTest::addRow("on$Changed") << u"on$Changed"_s << true;
}
void tst_qml_common::tst_isChangedHandlerName()
{

View File

@ -22,6 +22,7 @@
#include <QtQml/qqmlproperty.h>
#include <QtQml/qqmlincubator.h>
#include <QtQml/qqmlapplicationengine.h>
#include <QtQml/private/qqmlsignalnames_p.h>
#include <QtQuick/qquickitem.h>
#include <QtNetwork/qhostaddress.h>
@ -244,7 +245,7 @@ void tst_QQmlEngineDebugService::recursiveObjectTest(
QCOMPARE(p.objectDebugId, QQmlDebugService::idForObject(o));
// signal properties are fake - they are generated from QQmlAbstractBoundSignal children
if (p.name.startsWith("on") && p.name.size() > 2 && p.name[2].isUpper()) {
if (QQmlSignalNames::isHandlerName(p.name)) {
QString signal = p.value.toString();
QQmlBoundSignalExpression *expr = QQmlPropertyPrivate::signalExpression(QQmlProperty(o, p.name));
QVERIFY(expr && expr->expression() == signal);

View File

@ -94,6 +94,8 @@ Item {
}
function qtest_signalHandlerName(sn) {
// Warning: to not test for signal handlers like this in actual code.
// Use the helper methods in QQmlSignalNames instead.
if (sn.substr(0, 2) === "on" && sn[2] === sn[2].toUpperCase())
return sn
return "on" + sn.substr(0, 1).toUpperCase() + sn.substr(1)

View File

@ -35,6 +35,8 @@ BirthdayParty {
}
function stuff(sn) {
// Warning: to not test for signal handlers like this in actual code.
// Use the helper methods in QQmlSignalNames instead.
if (sn.substr(0, 2) === "on" && sn[2] === sn[2].toUpperCase())
return sn
return "on" + sn.substr(0, 1).toUpperCase() + sn.substr(1)

View File

@ -1,12 +0,0 @@
import QtQuick 2.0
Item {
property int changeCount: 0
// invalid property name - we don't allow $
property bool $nameWithDollarsign: false
on$NameWithDollarsignChanged: {
changeCount = changeCount + 4;
}
}

View File

@ -1,12 +0,0 @@
import QtQuick 2.0
Item {
property int changeCount: 0
property bool _6nameWithUnderscoreNumber: false
// invalid property name - the first character after an underscore must be a letter
on_6NameWithUnderscoreNumberChanged: {
changeCount = changeCount + 3;
}
}

View File

@ -6,6 +6,8 @@ Item {
property bool normalName: false
property bool _nameWithUnderscore: false
property bool ____nameWithUnderscores: false
property bool _6nameWithUnderscoreNumber: false
property bool $nameWithDollarsign: false
onNormalNameChanged: {
changeCount = changeCount + 1;
@ -19,9 +21,20 @@ Item {
changeCount = changeCount + 3;
}
on$NameWithDollarsignChanged: {
changeCount = changeCount + 4;
}
on_6NameWithUnderscoreNumberChanged: {
changeCount = changeCount + 5;
}
Component.onCompleted: {
normalName = true;
_nameWithUnderscore = true;
____nameWithUnderscores = true;
$nameWithDollarsign = true;
_6nameWithUnderscoreNumber = true;
}
}

View File

@ -5300,6 +5300,7 @@ void tst_qqmlecmascript::propertyChangeSlots()
QQmlComponent component(&engine, testFileUrl("changeslots/propertyChangeSlots.qml"));
QScopedPointer<QObject> object(component.create());
QVERIFY2(object, qPrintable(component.errorString()));
QCOMPARE(object->property("changeCount"), 15);
// ensure that invalid property names fail properly.
QTest::ignoreMessage(QtWarningMsg, "QQmlComponent: Component is not ready");
@ -5315,20 +5316,6 @@ void tst_qqmlecmascript::propertyChangeSlots()
QCOMPARE(e2.errors().at(0).toString(), expectedErrorString);
object.reset(e2.create());
QVERIFY(!object);
QTest::ignoreMessage(QtWarningMsg, "QQmlComponent: Component is not ready");
QQmlComponent e3(&engine, testFileUrl("changeslots/propertyChangeSlotErrors.3.qml"));
expectedErrorString = e3.url().toString() + QLatin1String(":9:5: Cannot assign to non-existent property \"on$NameWithDollarsignChanged\"");
QCOMPARE(e3.errors().at(0).toString(), expectedErrorString);
object.reset(e3.create());
QVERIFY(!object);
QTest::ignoreMessage(QtWarningMsg, "QQmlComponent: Component is not ready");
QQmlComponent e4(&engine, testFileUrl("changeslots/propertyChangeSlotErrors.4.qml"));
expectedErrorString = e4.url().toString() + QLatin1String(":9:5: Cannot assign to non-existent property \"on_6NameWithUnderscoreNumberChanged\"");
QCOMPARE(e4.errors().at(0).toString(), expectedErrorString);
object.reset(e4.create());
QVERIFY(!object);
}
void tst_qqmlecmascript::propertyVar_data()

View File

@ -422,6 +422,10 @@ private slots:
void attachedInCtor();
void byteArrayConversion();
void propertySignalNames_data();
void propertySignalNames();
void signalNames_data();
void signalNames();
private:
QQmlEngine engine;
@ -8140,6 +8144,99 @@ void tst_qqmllanguage::byteArrayConversion()
QCOMPARE(receiver->byteArrays[0], QByteArray("\1\2\3"));
QCOMPARE(receiver->byteArrays[1], QByteArray("\4\5\6"));
}
void tst_qqmllanguage::propertySignalNames_data()
{
QTest::addColumn<QString>("propertyName");
QTest::addColumn<QString>("propertyChangedSignal");
QTest::addColumn<QString>("propertyChangedHandler");
QTest::addRow("helloWorld") << u"helloWorld"_s << u"helloWorldChanged"_s
<< u"onHelloWorldChanged"_s;
QTest::addRow("$helloWorld") << u"$helloWorld"_s << u"$helloWorldChanged"_s
<< u"on$HelloWorldChanged"_s;
QTest::addRow("_helloWorld") << u"_helloWorld"_s << u"_helloWorldChanged"_s
<< u"on_HelloWorldChanged"_s;
QTest::addRow("_") << u"_"_s << u"_Changed"_s << u"on_Changed"_s;
QTest::addRow("$") << u"$"_s << u"$Changed"_s << u"on$Changed"_s;
QTest::addRow("ä") << u"ä"_s << u"äChanged"_s << u"onÄChanged"_s;
QTest::addRow("___123a") << u"___123a"_s << u"___123aChanged"_s << u"on___123AChanged"_s;
}
void tst_qqmllanguage::propertySignalNames()
{
QFETCH(QString, propertyName);
QFETCH(QString, propertyChangedSignal);
QFETCH(QString, propertyChangedHandler);
QQmlEngine e;
QQmlComponent c(&e);
c.setData(uR"(
import QtQuick
Item {
property int %1: 456
property bool success: false
function f() { %1 = 123; }
function g() { %2(); }
%3: success = true
})"_s.arg(propertyName, propertyChangedSignal, propertyChangedHandler)
.toUtf8(),
QUrl());
QVERIFY2(c.isReady(), qPrintable(c.errorString()));
QScopedPointer<QObject> o(c.create());
QVERIFY(o != nullptr);
const QMetaObject *metaObject = o->metaObject();
auto signalIndex =
metaObject->indexOfSignal(propertyChangedSignal.append("()").toStdString().c_str());
QVERIFY(signalIndex > -1);
auto signal = metaObject->method(signalIndex);
QVERIFY(signal.isValid());
QSignalSpy changeSignal(o.data(), signal);
QMetaObject::invokeMethod(o.data(), "f");
QCOMPARE(o->property(propertyName.toStdString().c_str()), 123);
QVERIFY(changeSignal.size() == 1);
QCOMPARE(o->property("success"), true);
QMetaObject::invokeMethod(o.data(), "g");
QVERIFY(changeSignal.size() == 2);
}
void tst_qqmllanguage::signalNames_data()
{
QTest::addColumn<QString>("signalName");
QTest::addColumn<QString>("handlerName");
QTest::addRow("helloWorld") << u"helloWorld"_s << u"onHelloWorld"_s;
QTest::addRow("$helloWorld") << u"$helloWorld"_s << u"on$HelloWorld"_s;
QTest::addRow("_helloWorld") << u"_helloWorld"_s << u"on_HelloWorld"_s;
QTest::addRow("_") << u"_"_s << u"on_"_s;
QTest::addRow("aUmlaut") << u"ä"_s << u"onÄ"_s;
QTest::addRow("___123a") << u"___123a"_s << u"on___123A"_s;
}
void tst_qqmllanguage::signalNames()
{
QFETCH(QString, signalName);
QFETCH(QString, handlerName);
QQmlEngine e;
QQmlComponent c(&e);
c.setData(uR"(
import QtQuick
Item {
signal %1()
property bool success: false
function f() { %1(); }
%2: success = true
})"_s.arg(signalName, handlerName)
.toUtf8(),
QUrl());
QVERIFY2(c.isReady(), qPrintable(c.errorString()));
QScopedPointer<QObject> o(c.create());
QVERIFY(o != nullptr);
const QMetaObject *metaObject = o->metaObject();
auto signalIndex = metaObject->indexOfSignal(signalName.append("()").toStdString().c_str());
QVERIFY(signalIndex > -1);
auto signal = metaObject->method(signalIndex);
QVERIFY(signal.isValid());
QSignalSpy changeSignal(o.data(), signal);
signal.invoke(o.data());
QVERIFY(changeSignal.size() == 1);
QCOMPARE(o->property("success"), true);
QMetaObject::invokeMethod(o.data(), "f");
QVERIFY(changeSignal.size() == 2);
}
QTEST_MAIN(tst_qqmllanguage)

View File

@ -8,6 +8,7 @@
#include <QtQml/private/qqmlengine_p.h>
#include <QtQmlModels/private/qqmllistmodel_p.h>
#include <QtQml/private/qqmlexpression_p.h>
#include <QtQml/private/qqmlsignalnames_p.h>
#include <QQmlComponent>
#include <QtCore/qtimer.h>
@ -1098,12 +1099,12 @@ void tst_qqmllistmodel::property_changes()
expr.evaluate();
QVERIFY2(!expr.hasError(), qPrintable(expr.error().toString()));
QString signalHandler = "on" + QString(roleName[0].toUpper()) + roleName.mid(1, roleName.size()) + "Changed:";
QString signalHandler = QQmlSignalNames::propertyNameToChangedHandlerName(roleName);
QString qml = "import QtQuick 2.0\n"
"Connections {\n"
"property bool gotSignal: false\n"
"target: model.get(" + QString::number(listIndex) + ")\n"
+ signalHandler + " gotSignal = true\n"
+ signalHandler + ": gotSignal = true\n"
"}\n";
QQmlComponent component(&engine);

View File

@ -30,6 +30,7 @@
#include <QtCore/private/qobject_p.h>
#include <QtCore/private/qmetaobject_p.h>
#include <QtQmlTypeRegistrar/private/qqmljsstreamwriter_p.h>
#include <QtQml/private/qqmlsignalnames_p.h>
#include <QRegularExpression>
#include <iostream>
@ -690,10 +691,13 @@ private:
for (int index = meta->propertyOffset(); index < meta->propertyCount(); ++index) {
const QMetaProperty &property = meta->property(index);
dump(property, metaRevision, knownAttributes);
const QByteArray changedSignalName =
QQmlSignalNames::propertyNameToChangedSignalName(property.name());
if (knownAttributes)
knownAttributes->knownMethod(QByteArray(property.name()).append("Changed"),
0, QTypeRevision::fromEncodedVersion(property.revision()));
implicitSignals.insert(QString("%1Changed").arg(QString::fromUtf8(property.name())));
knownAttributes->knownMethod(
changedSignalName, 0,
QTypeRevision::fromEncodedVersion(property.revision()));
implicitSignals.insert(changedSignalName);
}
return implicitSignals;
}

View File

@ -8,6 +8,7 @@
#include "qmltccompilerpieces.h"
#include <QtCore/qloggingcategory.h>
#include <QtQml/private/qqmlsignalnames_p.h>
#include <private/qqmljsutils_p.h>
#include <algorithm>
@ -1734,7 +1735,7 @@ void QmltcCompiler::compileScriptBinding(QmltcType &current,
break;
}
case QQmlSA::ScriptBindingKind::Script_SignalHandler: {
const auto name = QQmlJSUtils::signalName(propertyName);
const auto name = QQmlSignalNames::handlerNameToSignalName(propertyName);
Q_ASSERT(name.has_value());
compileScriptSignal(*name);
break;
@ -1743,9 +1744,10 @@ void QmltcCompiler::compileScriptBinding(QmltcType &current,
const QString objectClassName = objectType->internalName();
const QString bindingFunctorName = newSymbol(bindingSymbolName + u"Functor");
const auto signalName = QQmlJSUtils::signalName(propertyName);
const auto signalName = QQmlSignalNames::handlerNameToSignalName(propertyName);
Q_ASSERT(signalName.has_value()); // an error somewhere else
const auto actualProperty = QQmlJSUtils::changeHandlerProperty(objectType, *signalName);
const auto actualProperty =
QQmlJSUtils::propertyFromChangedHandler(objectType, propertyName);
Q_ASSERT(actualProperty.has_value()); // an error somewhere else
const auto actualPropertyType = actualProperty->type();
if (!actualPropertyType) {

View File

@ -6,6 +6,7 @@
#include <private/qqmljsmetatypes_p.h>
#include <private/qqmljsscope_p.h>
#include <QtQml/private/qqmlsignalnames_p.h>
QT_BEGIN_NAMESPACE
@ -35,13 +36,11 @@ struct QmltcPropertyData
QmltcPropertyData(const QString &propertyName)
{
const QString nameWithUppercase = propertyName[0].toUpper() + propertyName.sliced(1);
read = propertyName;
write = u"set" + nameWithUppercase;
bindable = u"bindable" + nameWithUppercase;
notify = propertyName + u"Changed";
reset = u"reset" + nameWithUppercase;
write = QQmlSignalNames::addPrefixToPropertyName(u"set", propertyName);
bindable = QQmlSignalNames::addPrefixToPropertyName(u"bindable", propertyName);
notify = QQmlSignalNames::propertyNameToChangedSignalName(propertyName);
reset = QQmlSignalNames::addPrefixToPropertyName(u"reset", propertyName);
}
QString read;

View File

@ -8,6 +8,7 @@
#include <QtCore/qstack.h>
#include <QtCore/qdir.h>
#include <QtCore/qloggingcategory.h>
#include <QtQml/private/qqmlsignalnames_p.h>
#include <private/qqmljsutils_p.h>
@ -269,7 +270,7 @@ bool QmltcVisitor::visit(QQmlJS::AST::UiPublicMember *publicMember)
owner->addOwnProperty(property);
}
const QString notifyName = name + u"Changed"_s;
const QString notifyName = QQmlSignalNames::propertyNameToChangedSignalName(name);
// also check that notify is already a method of the scope
{
auto owningScope = m_savedBindingOuterScope ? m_savedBindingOuterScope : m_currentScope;