diff options
author | Sami Shalayel <sami.shalayel@qt.io> | 2023-07-04 17:12:32 +0200 |
---|---|---|
committer | Sami Shalayel <sami.shalayel@qt.io> | 2023-08-10 19:55:40 +0200 |
commit | a130481af21af38f931b98a33b84eaa0637e69c7 (patch) | |
tree | a548fe660ea9fa5b632b6639eaaff9be9fa209a3 /src | |
parent | 62014e9cecc633a046754e5f76cae66f052ae17b (diff) |
qmlls: check user-supplied names on renaming
Implement the checking of user-supplied names for renaming operations
in QmlLSUtils and qqmlrenamesymbolsupport.cpp
Add a helper method QQmlLSUtils::isValidEcmaScriptIdentifier that runs
the lexer on an identifier. Reject identifiers that do not parse as
T_IDENTIFIER, like keywords and invalid unicode escapes, for example.
Extend QQmlLSUtilsExpressionType to contain the name of the current
object.
Drive-by change: fix a off-by-one bug in the lexer, where files (or
identifiers, in this case) could not be lexed when they were ending with
an unicode-sequence.
Also, do not crash on JSIdentifiers without semantic scope in
resolveIdentifierExpressionType.
Add some tests, and fix a warning about positionAfterOneIndent
not being used in tst_qmlls_modules.cpp.
Add QQmlLSUtils::isChangedSignalName next to
QQmlLSUtils::isChangedHandlerName, and QQmlLSUtils::isHandlerName
and add tests for all three.
Fixes: QTBUG-114951
Task-number: QTBUG-114788
Change-Id: I0f1a544b70dfb69bca4aef355a8a8658f1d23081
Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
Diffstat (limited to 'src')
-rw-r--r-- | src/qml/common/qqmlsignalnames.cpp | 12 | ||||
-rw-r--r-- | src/qml/common/qqmlsignalnames_p.h | 1 | ||||
-rw-r--r-- | src/qml/parser/qqmljslexer.cpp | 3 | ||||
-rw-r--r-- | src/qmlls/qqmllsutils.cpp | 230 | ||||
-rw-r--r-- | src/qmlls/qqmllsutils_p.h | 12 | ||||
-rw-r--r-- | src/qmlls/qqmlrenamesymbolsupport.cpp | 21 |
6 files changed, 241 insertions, 38 deletions
diff --git a/src/qml/common/qqmlsignalnames.cpp b/src/qml/common/qqmlsignalnames.cpp index c08277e7f1..5d46531dfb 100644 --- a/src/qml/common/qqmlsignalnames.cpp +++ b/src/qml/common/qqmlsignalnames.cpp @@ -174,6 +174,18 @@ std::optional<QString> QQmlSignalNames::handlerNameToSignalName(QStringView hand return signalName; } +bool QQmlSignalNames::isChangedSignalName(QStringView signalName) +{ + const qsizetype smallestAllowedSize = strlen("XChanged"); + if (signalName.size() < smallestAllowedSize || !signalName.endsWith(u"Changed"_s)) + return false; + + if (auto letter = firstLetter(signalName, 0, strlen("Changed"))) + return letter->isLower(); + + return true; +} + bool QQmlSignalNames::isChangedHandlerName(QStringView signalName) { const qsizetype smallestAllowedSize = strlen("onXChanged"); diff --git a/src/qml/common/qqmlsignalnames_p.h b/src/qml/common/qqmlsignalnames_p.h index 7c46a71b3a..f777e89812 100644 --- a/src/qml/common/qqmlsignalnames_p.h +++ b/src/qml/common/qqmlsignalnames_p.h @@ -44,6 +44,7 @@ public: static std::optional<QString> handlerNameToSignalName(QStringView handler); static bool isChangedHandlerName(QStringView signalName); + static bool isChangedSignalName(QStringView signalName); static bool isHandlerName(QStringView signalName); static QString addPrefixToPropertyName(QStringView prefix, QStringView propertyName); diff --git a/src/qml/parser/qqmljslexer.cpp b/src/qml/parser/qqmljslexer.cpp index 28f4684649..704c7eb00d 100644 --- a/src/qml/parser/qqmljslexer.cpp +++ b/src/qml/parser/qqmljslexer.cpp @@ -368,7 +368,8 @@ uint Lexer::decodeUnicodeEscapeCharacter(bool *ok) { Q_ASSERT(_state.currentChar == u'u'); scanChar(); // skip u - if (_codePtr + 4 <= _endPtr && isHexDigit(_state.currentChar)) { + constexpr int distanceFromFirstHexToLastHex = 3; + if (_codePtr + distanceFromFirstHexToLastHex <= _endPtr && isHexDigit(_state.currentChar)) { uint codePoint = 0; for (int i = 0; i < 4; ++i) { int digit = hexDigit(_state.currentChar); diff --git a/src/qmlls/qqmllsutils.cpp b/src/qmlls/qqmllsutils.cpp index 9daf89978b..78dbfa0c27 100644 --- a/src/qmlls/qqmllsutils.cpp +++ b/src/qmlls/qqmllsutils.cpp @@ -12,8 +12,12 @@ #include <QtQmlDom/private/qqmldomscriptelements_p.h> #include <QtQmlDom/private/qqmldom_utils_p.h> #include <QtQml/private/qqmlsignalnames_p.h> +#include <QtQml/private/qqmljslexer_p.h> + +#include <iterator> #include <memory> #include <optional> +#include <set> #include <stack> #include <type_traits> #include <utility> @@ -708,11 +712,11 @@ methodFromReferrerScope(const QQmlJSScope::ConstPtr &referrerScope, const QStrin { for (QQmlJSScope::ConstPtr current = referrerScope; current; current = current->parentScope()) { if (auto methodType = hasMethodOrSignal(current, name)) - return QQmlLSUtilsExpressionType{ current, *methodType }; + return QQmlLSUtilsExpressionType{ name, current, *methodType }; if (const auto signalName = QQmlSignalNames::handlerNameToSignalName(name)) { if (auto methodType = hasMethodOrSignal(current, *signalName)) { - return QQmlLSUtilsExpressionType{ current, *methodType }; + return QQmlLSUtilsExpressionType{ name, current, *methodType }; } } } @@ -727,10 +731,10 @@ propertyFromReferrerScope(const QQmlJSScope::ConstPtr &referrerScope, const QStr if (auto property = current->property(propertyName); property.isValid()) { switch (options) { case ResolveOwnerType: - return QQmlLSUtilsExpressionType{ current, + return QQmlLSUtilsExpressionType{ propertyName, current, QQmlLSUtilsIdentifierType::PropertyIdentifier }; case ResolveActualTypeForFieldMemberExpression: - return QQmlLSUtilsExpressionType{ property.type(), + return QQmlLSUtilsExpressionType{ propertyName, property.type(), QQmlLSUtilsIdentifierType::PropertyIdentifier }; } } @@ -762,7 +766,7 @@ resolveIdentifierExpressionType(DomItem item, QQmlLSUtilsResolveOptions options) QQmlLSUtilsIdentifierType type = isSignal ? QQmlLSUtilsIdentifierType::SignalIdentifier : QQmlLSUtilsIdentifierType::MethodIdentifier; - return QQmlLSUtilsExpressionType{ owner->semanticScope, type }; + return QQmlLSUtilsExpressionType{ name, owner->semanticScope, type }; } case ResolveActualTypeForFieldMemberExpression: // not implemented, but JS functions have methods and properties @@ -776,10 +780,10 @@ resolveIdentifierExpressionType(DomItem item, QQmlLSUtilsResolveOptions options) if (auto property = owner->semanticScope->property(name); property.isValid()) { switch (options) { case ResolveOwnerType: - return QQmlLSUtilsExpressionType{ owner->semanticScope, + return QQmlLSUtilsExpressionType{ name, owner->semanticScope, QQmlLSUtilsIdentifierType::PropertyIdentifier }; case ResolveActualTypeForFieldMemberExpression: - return QQmlLSUtilsExpressionType{ property.type(), + return QQmlLSUtilsExpressionType{ name, property.type(), QQmlLSUtilsIdentifierType::PropertyIdentifier }; } } @@ -787,19 +791,19 @@ resolveIdentifierExpressionType(DomItem item, QQmlLSUtilsResolveOptions options) DomItem definitionOfItem = findJSIdentifierDefinition(item, name); if (definitionOfItem) { Q_ASSERT_X(definitionOfItem.semanticScope().has_value() - && definitionOfItem.semanticScope() - .value() - ->JSIdentifier(name) - .has_value(), + && definitionOfItem.semanticScope().value()->JSIdentifier(name), "QQmlLSUtils::findDefinitionOf", "JS definition does not actually define the JS identifer. " "It should be empty."); - auto scope = definitionOfItem.semanticScope() - .value() - ->JSIdentifier(name) - ->scope.toStrongRef(); - return QQmlLSUtilsExpressionType{ scope, - QQmlLSUtilsIdentifierType::JavaScriptIdentifier }; + auto scope = definitionOfItem.semanticScope().value(); + auto jsIdentifier = scope->JSIdentifier(name); + if (jsIdentifier->scope) { + return QQmlLSUtilsExpressionType{ name, jsIdentifier->scope.toStrongRef(), + QQmlLSUtilsIdentifierType::JavaScriptIdentifier }; + } else { + return QQmlLSUtilsExpressionType{ name, scope, + QQmlLSUtilsIdentifierType::JavaScriptIdentifier }; + } } // check if its a method @@ -819,7 +823,7 @@ resolveIdentifierExpressionType(DomItem item, QQmlLSUtilsResolveOptions options) return {}; QQmlJSScope::ConstPtr fromId = resolver.value()->scopeForId(name, referrerScope.value()); if (fromId) - return QQmlLSUtilsExpressionType{ fromId, QmlObjectIdIdentifier }; + return QQmlLSUtilsExpressionType{ name, fromId, QmlObjectIdIdentifier }; return {}; } @@ -839,7 +843,7 @@ QQmlLSUtils::resolveExpressionType(QQmlJS::Dom::DomItem item, QQmlLSUtilsResolve auto propertyDefinition = item.as<PropertyDefinition>(); if (propertyDefinition && propertyDefinition->scope) { auto &scope = propertyDefinition->scope.value(); - return QQmlLSUtilsExpressionType{ scope, PropertyIdentifier }; + return QQmlLSUtilsExpressionType{ propertyDefinition->name, scope, PropertyIdentifier }; } return {}; } @@ -852,11 +856,11 @@ QQmlLSUtils::resolveExpressionType(QQmlJS::Dom::DomItem item, QQmlLSUtilsResolve const QString name = binding->name(); if (name == u"id") - return QQmlLSUtilsExpressionType{ owner.value(), QmlObjectIdIdentifier }; + return QQmlLSUtilsExpressionType{ name, owner.value(), QmlObjectIdIdentifier }; auto signalOrProperty = resolveNameInQmlScope(name, owner.value()); if (signalOrProperty) - return QQmlLSUtilsExpressionType{ owner.value(), signalOrProperty->type }; + return QQmlLSUtilsExpressionType{ name, owner.value(), signalOrProperty->type }; qDebug(QQmlLSUtilsLog) << "QQmlLSUtils::resolveExpressionType() could not resolve the" "type of a Binding."; @@ -867,7 +871,7 @@ QQmlLSUtils::resolveExpressionType(QQmlJS::Dom::DomItem item, QQmlLSUtilsResolve case DomType::QmlObject: { auto object = item.as<QmlObject>(); if (object && object->semanticScope()) - return QQmlLSUtilsExpressionType{ object->semanticScope().value(), + return QQmlLSUtilsExpressionType{ std::nullopt, object->semanticScope().value(), QmlObjectIdentifier }; return {}; @@ -885,7 +889,7 @@ QQmlLSUtils::resolveExpressionType(QQmlJS::Dom::DomItem item, QQmlLSUtilsResolve scope = scope.value()->parentScope(); if (auto type = hasMethodOrSignal(scope.value(), object->name)) - return QQmlLSUtilsExpressionType{ scope.value(), type.value() }; + return QQmlLSUtilsExpressionType{ object->name, scope.value(), type.value() }; qDebug(QQmlLSUtilsLog) << "QQmlLSUtils::resolveExpressionType() could not resolve the" "type of a MethodInfo."; @@ -1082,12 +1086,161 @@ std::optional<QQmlLSUtilsLocation> QQmlLSUtils::findDefinitionOf(DomItem item) Q_UNREACHABLE_RETURN(std::nullopt); } -std::optional<QQmlLSUtilsErrorMessage> QQmlLSUtils::checkNameForRename(DomItem item, - const QString &newName) +static std::optional<QQmlJSScope::ConstPtr> propertyOwnerFrom(const QQmlJSScope::ConstPtr &type, + const std::optional<QString> &name) +{ + if (!name) { + qCDebug(QQmlLSUtilsLog) << "QQmlLSUtils::checkNameForRename cannot find property name," + " ignoring."; + return {}; + } + + QQmlJSScope::ConstPtr typeWithDefinition = type; + while (!typeWithDefinition->hasOwnProperty(*name)) { + typeWithDefinition = type->baseType(); + if (!typeWithDefinition) { + qCDebug(QQmlLSUtilsLog) + << "QQmlLSUtils::checkNameForRename cannot find property definition," + " ignoring."; + return {}; + } + } + return typeWithDefinition; +} + +static std::optional<QQmlJSScope::ConstPtr> methodOwnerFrom(const QQmlJSScope::ConstPtr &type, + const std::optional<QString> &name) +{ + if (!name) { + qCDebug(QQmlLSUtilsLog) << "QQmlLSUtils::checkNameForRename cannot find method name," + " ignoring."; + return {}; + } + + QQmlJSScope::ConstPtr typeWithDefinition = type; + while (!typeWithDefinition->hasOwnMethod(*name)) { + typeWithDefinition = type->baseType(); + if (!typeWithDefinition) { + qCDebug(QQmlLSUtilsLog) + << "QQmlLSUtils::checkNameForRename cannot find method definition," + " ignoring."; + return {}; + } + } + return typeWithDefinition; +} + +static std::optional<QQmlJSScope::ConstPtr> +expressionTypeWithDefinition(const QQmlLSUtilsExpressionType &ownerType) { - // TODO - Q_UNUSED(item); - Q_UNUSED(newName); + switch (ownerType.type) { + case PropertyIdentifier: + return propertyOwnerFrom(ownerType.semanticScope, ownerType.name); + case PropertyChangedHandlerIdentifier: { + const auto propertyName = + QQmlSignalNames::changedHandlerNameToPropertyName(*ownerType.name); + return propertyOwnerFrom(ownerType.semanticScope, propertyName); + break; + } + case PropertyChangedSignalIdentifier: { + const auto propertyName = QQmlSignalNames::changedSignalNameToPropertyName(*ownerType.name); + return propertyOwnerFrom(ownerType.semanticScope, propertyName); + } + case MethodIdentifier: + case SignalIdentifier: + return methodOwnerFrom(ownerType.semanticScope, ownerType.name); + case SignalHandlerIdentifier: { + const auto signalName = QQmlSignalNames::handlerNameToSignalName(*ownerType.name); + return methodOwnerFrom(ownerType.semanticScope, signalName); + } + case JavaScriptIdentifier: + case QmlObjectIdIdentifier: + case QmlObjectIdentifier: + return ownerType.semanticScope; + } + return {}; +} + +std::optional<QQmlLSUtilsErrorMessage> +QQmlLSUtils::checkNameForRename(DomItem item, const QString &dirtyNewName, + std::optional<QQmlLSUtilsExpressionType> ownerType) +{ + // general checks for ECMAscript identifiers + if (!isValidEcmaScriptIdentifier(dirtyNewName)) + return QQmlLSUtilsErrorMessage{ 0, u"Invalid EcmaScript identifier!"_s }; + + if (!ownerType) + ownerType = QQmlLSUtils::resolveExpressionType(item, ResolveOwnerType); + + const auto userSemanticScope = item.nearestSemanticScope(); + + if (!ownerType || !userSemanticScope) { + return QQmlLSUtilsErrorMessage{ 0, u"Requested item cannot be renamed"_s }; + } + + // type specific checks + switch (ownerType->type) { + case PropertyChangedSignalIdentifier: { + if (!QQmlSignalNames::isChangedSignalName(dirtyNewName)) { + return QQmlLSUtilsErrorMessage{ 0, u"Invalid name for a property changed signal."_s }; + } + break; + } + case PropertyChangedHandlerIdentifier: { + if (!QQmlSignalNames::isChangedHandlerName(dirtyNewName)) { + return QQmlLSUtilsErrorMessage{ + 0, u"Invalid name for a property changed handler identifier."_s + }; + } + break; + } + case SignalHandlerIdentifier: { + if (!QQmlSignalNames::isHandlerName(dirtyNewName)) { + return QQmlLSUtilsErrorMessage{ 0, u"Invalid name for a signal handler identifier."_s }; + } + } + // TODO: any other specificities? + case QmlObjectIdIdentifier: + if (dirtyNewName.front().isLetter() && !dirtyNewName.front().isLower()) { + return QQmlLSUtilsErrorMessage{ + 0, u"Object id names cannot start with an upper case letter."_s + }; + } + break; + case JavaScriptIdentifier: + case PropertyIdentifier: + case SignalIdentifier: + case MethodIdentifier: + case QmlObjectIdentifier: + default: + break; + }; + + auto typeWithDefinition = expressionTypeWithDefinition(*ownerType); + + if (!typeWithDefinition) { + return QQmlLSUtilsErrorMessage{ + 0, + u"Renaming has not been implemented for the requested item."_s, + }; + } + + // is it not defined in QML? + if (!typeWithDefinition.value()->isComposite()) { + return QQmlLSUtilsErrorMessage{ 0, u"Cannot rename items defined in non-QML files."_s }; + } + + // is it defined in the current module? + const QString moduleOfDefinition = ownerType->semanticScope->moduleName(); + const QString moduleOfCurrentItem = userSemanticScope.value()->moduleName(); + if (moduleOfDefinition != moduleOfCurrentItem) { + return QQmlLSUtilsErrorMessage{ + 0, + u"Cannot rename items defined in the %1 module fromits usage in the %2 module."_s + .arg(moduleOfDefinition, moduleOfCurrentItem), + }; + } + return {}; } @@ -1155,7 +1308,9 @@ Special cases: All of the chopping operations are done using the static helpers from QQmlSignalNames. \endlist */ -QList<QQmlLSUtilsEdit> QQmlLSUtils::renameUsagesOf(DomItem item, const QString &dirtyNewName) +QList<QQmlLSUtilsEdit> +QQmlLSUtils::renameUsagesOf(DomItem item, const QString &dirtyNewName, + std::optional<QQmlLSUtilsExpressionType> targetType) { QList<QQmlLSUtilsEdit> results; const QList<QQmlLSUtilsLocation> locations = findUsagesOf(item); @@ -1166,8 +1321,9 @@ QList<QQmlLSUtilsEdit> QQmlLSUtils::renameUsagesOf(DomItem item, const QString & if (!oldName) return results; - auto targetType = - QQmlLSUtils::resolveExpressionType(item, QQmlLSUtilsResolveOptions::ResolveOwnerType); + if (!targetType) + targetType = QQmlLSUtils::resolveExpressionType( + item, QQmlLSUtilsResolveOptions::ResolveOwnerType); if (!targetType) return results; @@ -1246,4 +1402,16 @@ QQmlLSUtilsEdit QQmlLSUtilsEdit::from(const QString &fileName, const QString &co return rename; } +bool QQmlLSUtils::isValidEcmaScriptIdentifier(QStringView identifier) +{ + QQmlJS::Lexer lexer(nullptr); + lexer.setCode(identifier.toString(), 0); + const int token = lexer.lex(); + if (token != static_cast<int>(QQmlJS::Lexer::T_IDENTIFIER)) + return false; + // make sure there is nothing following the lexed identifier + const int eofToken = lexer.lex(); + return eofToken == static_cast<int>(QQmlJS::Lexer::EOF_SYMBOL); +} + QT_END_NAMESPACE diff --git a/src/qmlls/qqmllsutils_p.h b/src/qmlls/qqmllsutils_p.h index 8fca32d5ff..e74021c23d 100644 --- a/src/qmlls/qqmllsutils_p.h +++ b/src/qmlls/qqmllsutils_p.h @@ -19,6 +19,7 @@ #include <QtQmlDom/private/qqmldomexternalitems_p.h> #include <QtQmlDom/private/qqmldomtop_p.h> #include <algorithm> +#include <optional> #include <tuple> #include <variant> @@ -56,6 +57,7 @@ struct QQmlLSUtilsErrorMessage struct QQmlLSUtilsExpressionType { + std::optional<QString> name; QQmlJSScope::ConstPtr semanticScope; QQmlLSUtilsIdentifierType type; }; @@ -128,12 +130,16 @@ public: static std::optional<QQmlLSUtilsLocation> findDefinitionOf(QQmlJS::Dom::DomItem item); static QList<QQmlLSUtilsLocation> findUsagesOf(QQmlJS::Dom::DomItem item); - static std::optional<QQmlLSUtilsErrorMessage> checkNameForRename(QQmlJS::Dom::DomItem item, - const QString &newName); - static QList<QQmlLSUtilsEdit> renameUsagesOf(QQmlJS::Dom::DomItem item, const QString &newName); + static std::optional<QQmlLSUtilsErrorMessage> + checkNameForRename(QQmlJS::Dom::DomItem item, const QString &newName, + std::optional<QQmlLSUtilsExpressionType> targetType = std::nullopt); + static QList<QQmlLSUtilsEdit> + renameUsagesOf(QQmlJS::Dom::DomItem item, const QString &newName, + std::optional<QQmlLSUtilsExpressionType> targetType = std::nullopt); static std::optional<QQmlLSUtilsExpressionType> resolveExpressionType(QQmlJS::Dom::DomItem item, QQmlLSUtilsResolveOptions); + static bool isValidEcmaScriptIdentifier(QStringView view); }; QT_END_NAMESPACE diff --git a/src/qmlls/qqmlrenamesymbolsupport.cpp b/src/qmlls/qqmlrenamesymbolsupport.cpp index 2db9d16f1a..89c6617fb6 100644 --- a/src/qmlls/qqmlrenamesymbolsupport.cpp +++ b/src/qmlls/qqmlrenamesymbolsupport.cpp @@ -1,6 +1,7 @@ // Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#include "qqmllsutils_p.h" #include "qqmlrenamesymbolsupport_p.h" #include <utility> @@ -47,14 +48,28 @@ void QQmlRenameSymbolSupport::process(QQmlRenameSymbolSupport::RequestPointerArg return; } - // TODO: sanity checks: check for errors + const QString newName = QString::fromUtf8(request->m_parameters.newName); + auto expressionType = + QQmlLSUtils::resolveExpressionType(itemsFound->front().domItem, ResolveOwnerType); + + if (!expressionType) { + error = QQmlLSUtilsErrorMessage{ 0, u"Cannot rename the requested object"_s }; + return; + } + + if (auto renameImpossible = QQmlLSUtils::checkNameForRename(itemsFound->front().domItem, + newName, expressionType)) { + error = renameImpossible; + return; + } QList<QLspSpecification::TextDocumentEdit> editsByFileForResult; // The QLspSpecification::WorkspaceEdit requires the changes to be grouped by files, so // collect them into editsByFileUris. QMap<QUrl, QList<QLspSpecification::TextEdit>> editsByFileUris; - auto renames = QQmlLSUtils::renameUsagesOf(itemsFound->front().domItem, - QString::fromUtf8(request->m_parameters.newName)); + + auto renames = + QQmlLSUtils::renameUsagesOf(itemsFound->front().domItem, newName, expressionType); QQmlJS::Dom::DomItem files = itemsFound->front().domItem.top().field(QQmlJS::Dom::Fields::qmlFileWithPath); |