Revision control
++ // 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
++ // Qt-Security score:significant reason:default
++
++ #include "qqmllsutils_p.h"
++
++ #include <QtCore/qassert.h>
++ #include <QtLanguageServer/private/qlanguageserverspectypes_p.h>
++ #include <QtCore/qthreadpool.h>
++ #include <QtCore/private/qduplicatetracker_p.h>
++ #include <QtCore/QRegularExpression>
++ #include <QtQmlDom/private/qqmldomexternalitems_p.h>
++ #include <QtQmlDom/private/qqmldomtop_p.h>
++ #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 <QtQmlCompiler/private/qqmljsutils_p.h>
++
++ #include <algorithm>
++ #include <memory>
++ #include <optional>
++ #include <queue>
++ #include <utility>
++
++ QT_BEGIN_NAMESPACE
++
++ Q_LOGGING_CATEGORY(QQmlLSUtilsLog, "qt.languageserver.utils")
++
++ using namespace QQmlJS::Dom;
++ using namespace Qt::StringLiterals;
++
++ namespace QQmlLSUtils {
++ QString qualifiersFrom(const DomItem &el)
++ {
++ const bool isAccess = QQmlLSUtils::isFieldMemberAccess(el);
++ if (!isAccess && !QQmlLSUtils::isFieldMemberExpression(el))
++ return {};
++
++ const DomItem fieldMemberExpressionBeginning = el.filterUp(
++ [](DomType, const DomItem &item) { return !QQmlLSUtils::isFieldMemberAccess(item); },
++ FilterUpOptions::ReturnOuter);
++ QStringList qualifiers =
++ QQmlLSUtils::fieldMemberExpressionBits(fieldMemberExpressionBeginning, el);
++
++ QString result;
++ for (const QString &qualifier : qualifiers)
++ result.append(qualifier).append(QChar(u'.'));
++ return result;
++ }
++
++ /*!
++ \internal
++ Helper to check if item is a Field Member Expression \c {<someExpression>.propertyName}.
++ */
++ bool isFieldMemberExpression(const DomItem &item)
++ {
++ return item.internalKind() == DomType::ScriptBinaryExpression
++ && item.field(Fields::operation).value().toInteger()
++ == ScriptElements::BinaryExpression::FieldMemberAccess;
++ }
++
++ /*!
++ \internal
++ Helper to check if item is a Field Member Access \c memberAccess in
++ \c {<someExpression>.memberAccess}.
++ */
++ bool isFieldMemberAccess(const DomItem &item)
++ {
++ auto parent = item.directParent();
++ if (!isFieldMemberExpression(parent))
++ return false;
++
++ DomItem rightHandSide = parent.field(Fields::right);
++ return item == rightHandSide;
++ }
++
++ /*!
++ \internal
++ Helper to check if item is a Field Member Base \c base in
++ \c {base.memberAccess}.
++ */
++ bool isFieldMemberBase(const DomItem &item)
++ {
++ auto parent = item.directParent();
++ if (!isFieldMemberExpression(parent))
++ return false;
++
++ // First case, checking `a` for being a base in `a.b`: a is the left hand side of the binary
++ // expression B(a,b).
++ const DomItem leftHandSide = parent.field(Fields::left);
++ if (item == leftHandSide)
++ return true;
++
++ // Second case, checking `d` for being a base in `a.b.c.d.e.f.g`: the binary expressions are
++ // nested as following: B(B(B(B(B(B(a,b),c),d),e),f),g) so for `d`, check whether its
++ // grandparent B(B(B(B(a,b),c),d),e), which has `e` on its right hand side, is a binary
++ // expression.
++ const DomItem grandParent = parent.directParent();
++ return isFieldMemberExpression(grandParent) && grandParent.field(Fields::left) == parent;
++ }
++
++ /*!
++ \internal
++ Get the bits of a field member expression, like \c{a}, \c{b} and \c{c} for \c{a.b.c}.
++
++ stopAtChild can either be an FieldMemberExpression, a ScriptIdentifierExpression or a default
++ constructed DomItem: This exits early before processing Field::right of an
++ FieldMemberExpression stopAtChild, or before processing a ScriptIdentifierExpression stopAtChild.
++ No early exits if stopAtChild is default constructed.
++ */
++ QStringList fieldMemberExpressionBits(const DomItem &item, const DomItem &stopAtChild)
++ {
++ const bool isAccess = isFieldMemberAccess(item);
++ const bool isExpression = isFieldMemberExpression(item);
++
++ // assume it is a non-qualified name
++ if (!isAccess && !isExpression)
++ return { item.value().toString() };
++
++ const DomItem stopMarker =
++ isFieldMemberExpression(stopAtChild) ? stopAtChild : stopAtChild.directParent();
++
++ QStringList result;
++ DomItem current =
++ isAccess ? item.directParent() : (isFieldMemberExpression(item) ? item : DomItem{});
++
++ for (; isFieldMemberExpression(current); current = current.field(Fields::right)) {
++ result << current.field(Fields::left).value().toString();
++
++ if (current == stopMarker)
++ return result;
++ }
++ result << current.value().toString();
++
++ return result;
++ }
++
++ /*!
++ \internal
++ The language server protocol calls "URI" what QML calls "URL".
++ According to RFC 3986, a URL is a special case of URI that not only
++ identifies a resource but also shows how to access it.
++ In QML, however, URIs are distinct from URLs. URIs are the
++ identifiers of modules, for example "QtQuick.Controls".
++ In order to not confuse the terms we interpret language server URIs
++ as URLs in the QML code model.
++ This method marks a point of translation between the terms, but does
++ not have to change the actual URI/URL.
++
++ \sa QQmlLSUtils::qmlUriToLspUrl
++ */
++ QByteArray lspUriToQmlUrl(const QByteArray &uri)
++ {
++ return uri;
++ }
++
++ QByteArray qmlUrlToLspUri(const QByteArray &url)
++ {
++ return url;
++ }
++
++ /*!
++ \internal
++ \brief Converts a QQmlJS::SourceLocation to a LSP Range.
++
++ QQmlJS::SourceLocation starts counting lines and rows at 1, but the LSP Range starts at 0.
++ */
++ QLspSpecification::Range qmlLocationToLspLocation(Location qmlLocation)
++ {
++ QLspSpecification::Range range;
++
++ range.start.line = qmlLocation.sourceLocation().startLine - 1;
++ range.start.character = qmlLocation.sourceLocation().startColumn - 1;
++ range.end.line = qmlLocation.end().line;
++ range.end.character = qmlLocation.end().character;
++
++ return range;
++ }
++
++ /*!
++ \internal
++ \brief Convert a text position from (line, column) into an offset.
++
++ Row, Column and the offset are all 0-based.
++ For example, \c{s[textOffsetFrom(s, 5, 55)]} returns the character of s at line 5 and column 55.
++
++ \sa QQmlLSUtils::textRowAndColumnFrom
++ */
++ qsizetype textOffsetFrom(const QString &text, int row, int column)
++ {
++ return QQmlJS::SourceLocation::offsetFrom(text, row + 1, column + 1);
++ }
++
++ /*!
++ \internal
++ \brief Convert a text position from an offset into (line, column).
++
++ Row, Column and the offset are all 0-based.
++ For example, \c{textRowAndColumnFrom(s, 55)} returns the line and columns of the
++ character at \c {s[55]}.
++
++ \sa QQmlLSUtils::textOffsetFrom
++ */
++ TextPosition textRowAndColumnFrom(const QString &text, qsizetype offset)
++ {
++ auto [row, column] = QQmlJS::SourceLocation::rowAndColumnFrom(text, offset);
++
++ // special case: return last character when accessing after end of file
++ if (offset >= text.size())
++ --column;
++
++ return TextPosition{ int(row - 1), int(column - 1) };
++ }
++
++ static QList<ItemLocation>::const_iterator
++ handlePropertyDefinitionAndBindingOverlap(const QList<ItemLocation> &items, qsizetype offsetInFile)
++ {
++ auto smallest = std::min_element(
++ items.begin(), items.end(), [](const ItemLocation &a, const ItemLocation &b) {
++ return a.fileLocation->info().fullRegion.length
++ < b.fileLocation->info().fullRegion.length;
++ });
++
++ if (smallest->domItem.internalKind() == DomType::Binding) {
++ // weird edge case: the filelocations of property definitions and property bindings are
++ // actually overlapping, which means that qmlls cannot distinguish between bindings and
++ // bindings in property definitions. Those need to be treated differently for
++ // autocompletion, for example.
++ // Therefore: when inside a binding and a propertydefinition, choose the property definition
++ // if offsetInFile is before the colon, like for example:
++ // property var helloProperty: Rectangle { /*...*/ }
++ // |----return propertydef---|-- return Binding ---|
++
++ // get the smallest property definition to avoid getting the property definition that the
++ // current QmlObject is getting bound to!
++ auto smallestPropertyDefinition = std::min_element(
++ items.begin(), items.end(), [](const ItemLocation &a, const ItemLocation &b) {
++ // make property definition smaller to avoid getting smaller items that are not
++ // property definitions
++ const bool aIsPropertyDefinition =
++ a.domItem.internalKind() == DomType::PropertyDefinition;
++ const bool bIsPropertyDefinition =
++ b.domItem.internalKind() == DomType::PropertyDefinition;
++ return aIsPropertyDefinition > bIsPropertyDefinition
++ && a.fileLocation->info().fullRegion.length
++ < b.fileLocation->info().fullRegion.length;
++ });
++
++ if (smallestPropertyDefinition->domItem.internalKind() != DomType::PropertyDefinition)
++ return smallest;
++
++ const auto propertyDefinitionColon =
++ smallestPropertyDefinition->fileLocation->info().regions[ColonTokenRegion];
++ const auto smallestColon = smallest->fileLocation->info().regions[ColonTokenRegion];
++ // sanity check: is it the definition of the current binding? check if they both have their
++ // ':' at the same location
++ if (propertyDefinitionColon.isValid() && propertyDefinitionColon == smallestColon
++ && offsetInFile < smallestColon.begin()) {
++ return smallestPropertyDefinition;
++ }
++ }
++ return smallest;
++ }
++
++ static QList<ItemLocation> filterItemsFromTextLocation(const QList<ItemLocation> &items,
++ qsizetype offsetInFile)
++ {
++ if (items.size() < 2)
++ return items;
++
++ // if there are multiple items, take the smallest one + its neighbors
++ // this allows to prefer inline components over main components, when both contain the
++ // current textposition, and to disregard internal structures like property maps, which
++ // "contain" everything from their first-appearing to last-appearing property (e.g. also
++ // other stuff in between those two properties).
++
++ QList<ItemLocation> filteredItems;
++
++ auto smallest = handlePropertyDefinitionAndBindingOverlap(items, offsetInFile);
++
++ filteredItems.append(*smallest);
++
++ const QQmlJS::SourceLocation smallestLoc = smallest->fileLocation->info().fullRegion;
++ const qsizetype smallestBegin = smallestLoc.begin();
++ const qsizetype smallestEnd = smallestLoc.end();
++
++ for (auto it = items.begin(); it != items.end(); it++) {
++ if (it == smallest)
++ continue;
++
++ const QQmlJS::SourceLocation itLoc = it->fileLocation->info().fullRegion;
++ const qsizetype itBegin = itLoc.begin();
++ const qsizetype itEnd = itLoc.end();
++ if (itBegin == smallestEnd || smallestBegin == itEnd) {
++ filteredItems.append(*it);
++ }
++ }
++ return filteredItems;
++ }
++
++ /*!
++ \internal
++ \brief Find the DomItem representing the object situated in file at given line and
++ character/column.
++
++ If line and character point between two objects, two objects might be returned.
++ If line and character point to whitespace, it might return an inner node of the QmlDom-Tree.
++
++ We usually assume that sourcelocations have inclusive ends, for example
++ we assume that auto-completion on `\n` in `someName\n` wants suggestions
++ for `someName`, even if its technically one position "outside" the
++ sourcelocation of `someName`. This is not true for
++ ScriptBinaryExpressions, where auto-completion on `.` in `someName.` should
++ not return suggestions for `someName`.
++ The same also applies to all other binary expressions `+`, `-`, and so on.
++ */
++ QList<ItemLocation> itemsFromTextLocation(const DomItem &file, int line, int character)
++ {
++ QList<ItemLocation> itemsFound;
++ std::shared_ptr<QmlFile> filePtr = file.ownerAs<QmlFile>();
++ if (!filePtr)
++ return itemsFound;
++ FileLocations::Tree t = filePtr->fileLocationsTree();
++ Q_ASSERT(t);
++ QString code = filePtr->code(); // do something more advanced wrt to changes wrt to this->code?
++ QList<ItemLocation> toDo;
++ qsizetype targetPos = textOffsetFrom(code, line, character);
++ Q_ASSERT(targetPos >= 0);
++
++ enum ComparisonOption { Normal, ExcludePositionAfterLast };
++ auto containsTarget = [targetPos](QQmlJS::SourceLocation l, ComparisonOption c) {
++ return l.begin() <= targetPos && targetPos < l.end() + (c == Normal ? 1 : 0);
++ };
++ if (containsTarget(t->info().fullRegion, Normal)) {
++ ItemLocation loc;
++ loc.domItem = file;
++ loc.fileLocation = t;
++ toDo.append(loc);
++ }
++ while (!toDo.isEmpty()) {
++ ItemLocation iLoc = toDo.last();
++ toDo.removeLast();
++
++ bool inParentButOutsideChildren = true;
++
++ // Exclude the position behind the source location in ScriptBinaryExpressions to avoid
++ // returning `owner` in `owner.member` when completion is triggered on the \c{.}. This
++ // tells the code for the completion if the completion was triggered on `owner` or on `.`.
++ // Same is true for templateliterals, where ScriptTemplateExpressionParts and
++ // ScriptTemplateStringParts stop overlapping when using ExcludePositionAfterLast.
++ const ComparisonOption comparisonOption =
++ iLoc.domItem.internalKind() == DomType::ScriptBinaryExpression
++ || iLoc.domItem.directParent().internalKind()
++ == DomType::ScriptTemplateLiteral
++ ? ExcludePositionAfterLast
++ : Normal;
++
++ auto subEls = iLoc.fileLocation->subItems();
++ for (auto it = subEls.begin(); it != subEls.end(); ++it) {
++ auto subLoc = it.value();
++ Q_ASSERT(subLoc);
++
++ if (containsTarget(subLoc->info().fullRegion, comparisonOption)) {
++ ItemLocation subItem;
++ subItem.domItem = iLoc.domItem.path(it.key());
++ if (!subItem.domItem) {
++ qCDebug(QQmlLSUtilsLog)
++ << "A DomItem child is missing or the FileLocationsTree structure does "
++ "not follow the DomItem Structure.";
++ continue;
++ }
++ // the parser inserts empty Script Expressions for bindings that are not completely
++ // written out yet. Ignore them here.
++ if (subItem.domItem.internalKind() == DomType::ScriptExpression
++ && subLoc->info().fullRegion.length == 0) {
++ continue;
++ }
++ subItem.fileLocation = subLoc;
++ toDo.append(subItem);
++ inParentButOutsideChildren = false;
++ }
++ }
++ if (inParentButOutsideChildren) {
++ itemsFound.append(iLoc);
++ }
++ }
++
++ // filtering step:
++ auto filtered = filterItemsFromTextLocation(itemsFound, targetPos);
++ return filtered;
++ }
++
++ /*!
++ \internal
++ \brief Returns the location of the type definition pointed by object.
++
++ For a \c PropertyDefinition, return the location of the type of the property.
++ For a \c Binding, return the bound item's type location if an QmlObject is bound, and otherwise
++ the type of the property.
++ For a \c QmlObject, return the location of the QmlObject's base.
++ For an \c Id, return the location of the object to which the id resolves.
++ For a \c Methodparameter, return the location of the type of the parameter.
++ Otherwise, return std::nullopt.
++ */
++ std::optional<Location> findTypeDefinitionOf(const DomItem &object)
++ {
++ auto expressionType = resolveExpressionType(object, ResolveOptions::ResolveOwnerType);
++
++ if (!expressionType)
++ return { };
++
++ QQmlJSScope::ConstPtr scope = expressionType->semanticScope;
++ if (!scope)
++ return { };
++ switch (expressionType->type) {
++ case PropertyIdentifier: {
++ if (!expressionType->name)
++ return { };
++ QQmlJSScope::ConstPtr propertyType = scope->property(*expressionType->name).type();
++ if (!propertyType)
++ return { };
++ return Location::tryFrom(propertyType->filePath(), propertyType->sourceLocation(), object);
++ }
++ case JavaScriptIdentifier: {
++ if (!expressionType->name)
++ return { };
++ auto jsIdentifier = scope->jsIdentifier(*expressionType->name);
++ if (!jsIdentifier || !jsIdentifier->typeName)
++ return { };
++ auto qmlFile = object.fileObject().as<QmlFile>();
++ if (!qmlFile)
++ return { };
++ auto definition = qmlFile->typeResolver()->typeForName(*jsIdentifier->typeName);
++ if (!definition)
++ return { };
++ return Location::tryFrom(definition->filePath(), definition->sourceLocation(), object);
++ }
++ case PropertyChangedSignalIdentifier:
++ case PropertyChangedHandlerIdentifier:
++ case AttachedTypeIdentifier:
++ case AttachedTypeIdentifierInBindingTarget:
++ case GroupedPropertyIdentifier:
++ case QmlObjectIdIdentifier:
++ case NotAnIdentifier:
++ case SignalIdentifier:
++ case SignalHandlerIdentifier:
++ case MethodIdentifier:
++ case LambdaMethodIdentifier:
++ case SingletonIdentifier:
++ case QualifiedModuleIdentifier:
++ case EnumeratorIdentifier:
++ case EnumeratorValueIdentifier:
++ case QmlComponentIdentifier:
++ return Location::tryFrom(scope->filePath(), scope->sourceLocation(), object);
++ }
++ Q_UNREACHABLE_RETURN({ });
++ }
++
++ static bool findDefinitionFromItem(const DomItem &item, const QString &name)
++ {
++ if (const QQmlJSScope::ConstPtr &scope = item.semanticScope()) {
++ qCDebug(QQmlLSUtilsLog) << "Searching for definition in" << item.internalKindStr();
++ if (auto jsIdentifier = scope->ownJSIdentifier(name)) {
++ qCDebug(QQmlLSUtilsLog) << "Found scope" << scope->baseTypeName();
++ return true;
++ }
++ }
++ return false;
++ }
++
++ static DomItem findJSIdentifierDefinition(const DomItem &item, const QString &name)
++ {
++ DomItem definitionOfItem;
++ item.visitUp([&name, &definitionOfItem](const DomItem &i) {
++ if (findDefinitionFromItem(i, name)) {
++ definitionOfItem = i;
++ return false;
++ }
++ // early exit: no JS definitions/usages outside the ScriptExpression DOM element.
++ if (i.internalKind() == DomType::ScriptExpression)
++ return false;
++ return true;
++ });
++
++ if (definitionOfItem)
++ return definitionOfItem;
++
++ // special case: somebody asks for usages of a function parameter from its definition
++ // function parameters are defined in the method's scope
++ if (DomItem res = item.filterUp([](DomType k, const DomItem &) { return k == DomType::MethodInfo; },
++ FilterUpOptions::ReturnOuter)) {
++ DomItem candidate = res.field(Fields::body).field(Fields::scriptElement);
++ if (findDefinitionFromItem(candidate, name)) {
++ return candidate;
++ }
++ }
++
++ // lambda function parameters are defined in the FunctionExpression scope
++ if (DomItem res = item.filterUp(
++ [](DomType k, const DomItem &) { return k == DomType::ScriptFunctionExpression; },
++ FilterUpOptions::ReturnOuter)) {
++ if (findDefinitionFromItem(res, name)) {
++ return res;
++ }
++ }
++
++ return definitionOfItem;
++ }
++
++ /*!
++ \internal
++ Represents a signal, signal handler, property, property changed signal or a property changed
++ handler.
++ */
++ struct SignalOrProperty
++ {
++ /*!
++ \internal The name of the signal or property, independent of whether this is a changed signal
++ or handler.
++ */
++ QString name;
++ IdentifierType type;
++ };
++
++ /*!
++ \internal
++ \brief Find out if \c{name} is a signal, signal handler, property, property changed signal, or a
++ property changed handler in the given QQmlJSScope.
++
++ Heuristic to find if name is a property, property changed signal, .... because those can appear
++ under different names, for example \c{mySignal} and \c{onMySignal} for a signal.
++ This will give incorrect results as soon as properties/signals/methods are called \c{onMySignal},
++ \c{on<some already existing property>Changed}, ..., but the good news is that the engine also
++ will act weird in these cases (e.g. one cannot bind to a property called like an already existing
++ signal or a property changed handler).
++ For future reference: you can always add additional checks to check the existence of those buggy
++ properties/signals/methods by looking if they exist in the QQmlJSScope.
++ */
++ static std::optional<SignalOrProperty> resolveNameInQmlScope(const QString &name,
++ const QQmlJSScope::ConstPtr &owner)
++ {
++ if (owner->hasProperty(name)) {
++ return SignalOrProperty{ name, PropertyIdentifier };
++ }
++
++ if (const auto propertyName = QQmlSignalNames::changedHandlerNameToPropertyName(name)) {
++ if (owner->hasProperty(*propertyName)) {
++ const QString signalName = *QQmlSignalNames::changedHandlerNameToSignalName(name);
++ const QQmlJSMetaMethod signal = owner->methods(signalName).front();
++ // PropertyChangedHandlers don't have parameters: treat all other as regular signal
++ // handlers. Even if they appear in the notify of the property.
++ if (signal.parameterNames().size() == 0)
++ return SignalOrProperty{ *propertyName, PropertyChangedHandlerIdentifier };
++ else
++ return SignalOrProperty{ signalName, SignalHandlerIdentifier };
++ }
++ }
++
++ if (const auto signalName = QQmlSignalNames::handlerNameToSignalName(name)) {
++ if (auto methods = owner->methods(*signalName); !methods.isEmpty()) {
++ if (methods.front().methodType() == QQmlJSMetaMethodType::Signal) {
++ return SignalOrProperty{ *signalName, SignalHandlerIdentifier };
++ }
++ }
++ }
++
++ if (const auto propertyName = QQmlSignalNames::changedSignalNameToPropertyName(name)) {
++ if (owner->hasProperty(*propertyName)) {
++ return SignalOrProperty{ *propertyName, PropertyChangedSignalIdentifier };
++ }
++ }
++
++ if (auto methods = owner->methods(name); !methods.isEmpty()) {
++ if (methods.front().methodType() == QQmlJSMetaMethodType::Signal) {
++ return SignalOrProperty{ name, SignalIdentifier };
++ }
++ return SignalOrProperty{ name, MethodIdentifier };
++ }
++ return std::nullopt;
++ }
++
++ /*!
++ \internal
++ Returns a list of names, that when belonging to the same targetType, should be considered equal.
++ This is used to find signal handlers as usages of their corresponding signals, for example.
++ */
++ static QStringList namesOfPossibleUsages(const QString &name,
++ const DomItem &item,
++ const QQmlJSScope::ConstPtr &targetType)
++ {
++ QStringList namesToCheck = { name };
++ if (item.internalKind() == DomType::EnumItem || item.internalKind() == DomType::EnumDecl)
++ return namesToCheck;
++
++ auto namings = resolveNameInQmlScope(name, targetType);
++ if (!namings)
++ return namesToCheck;
++ switch (namings->type) {
++ case PropertyIdentifier: {
++ // for a property, also find bindings to its onPropertyChanged handler + propertyChanged
++ // signal
++ const QString propertyChangedHandler =
++ QQmlSignalNames::propertyNameToChangedHandlerName(namings->name);
++ namesToCheck.append(propertyChangedHandler);
++
++ const QString propertyChangedSignal =
++ QQmlSignalNames::propertyNameToChangedSignalName(namings->name);
++ namesToCheck.append(propertyChangedSignal);
++ break;
++ }
++ case PropertyChangedHandlerIdentifier: {
++ // for a property changed handler, also find the usages of its property + propertyChanged
++ // signal
++ namesToCheck.append(namings->name);
++ namesToCheck.append(QQmlSignalNames::propertyNameToChangedSignalName(namings->name));
++ break;
++ }
++ case PropertyChangedSignalIdentifier: {
++ // for a property changed signal, also find the usages of its property + onPropertyChanged
++ // handlers
++ namesToCheck.append(namings->name);
++ namesToCheck.append(QQmlSignalNames::propertyNameToChangedHandlerName(namings->name));
++ break;
++ }
++ case SignalIdentifier: {
++ // for a signal, also find bindings to its onSignalHandler.
++ namesToCheck.append(QQmlSignalNames::signalNameToHandlerName(namings->name));
++ break;
++ }
++ case SignalHandlerIdentifier: {
++ // for a signal handler, also find the usages of the signal it handles
++ namesToCheck.append(namings->name);
++ break;
++ }
++ default: {
++ break;
++ }
++ }
++ return namesToCheck;
++ }
++
++ template<typename Predicate>
++ QQmlJSScope::ConstPtr findDefiningScopeIf(
++ const QQmlJSScope::ConstPtr &referrerScope, Predicate &&check)
++ {
++ QQmlJSScope::ConstPtr result;
++ QQmlJSUtils::searchBaseAndExtensionTypes(
++ referrerScope, [&](const QQmlJSScope::ConstPtr &scope) {
++ if (check(scope)) {
++ result = scope;
++ return true;
++ }
++ return false;
++ });
++
++ return result;
++ }
++
++ /*!
++ \internal
++ \brief Finds the scope where a property is first defined.
++
++ Starts looking for the name starting from the given scope and traverse through base and
++ extension types.
++ */
++ QQmlJSScope::ConstPtr findDefiningScopeForProperty(const QQmlJSScope::ConstPtr &referrerScope,
++ const QString &nameToCheck)
++ {
++ return findDefiningScopeIf(referrerScope, [&nameToCheck](const QQmlJSScope::ConstPtr &scope) {
++ return scope->hasOwnProperty(nameToCheck);
++ });
++ }
++
++ /*!
++ \internal
++ See also findDefiningScopeForProperty().
++
++ Special case: you can also bind to a signal handler.
++ */
++ QQmlJSScope::ConstPtr findDefiningScopeForBinding(const QQmlJSScope::ConstPtr &referrerScope,
++ const QString &nameToCheck)
++ {
++ return findDefiningScopeIf(referrerScope, [&nameToCheck](const QQmlJSScope::ConstPtr &scope) {
++ return scope->hasOwnProperty(nameToCheck) || scope->hasOwnMethod(nameToCheck);
++ });
++ }
++
++ /*!
++ \internal
++ See also findDefiningScopeForProperty().
++ */
++ QQmlJSScope::ConstPtr findDefiningScopeForMethod(const QQmlJSScope::ConstPtr &referrerScope,
++ const QString &nameToCheck)
++ {
++ return findDefiningScopeIf(referrerScope, [&nameToCheck](const QQmlJSScope::ConstPtr &scope) {
++ return scope->hasOwnMethod(nameToCheck);
++ });
++ }
++
++ /*!
++ \internal
++ See also findDefiningScopeForProperty().
++ */
++ QQmlJSScope::ConstPtr findDefiningScopeForEnumeration(const QQmlJSScope::ConstPtr &referrerScope,
++ const QString &nameToCheck)
++ {
++ return findDefiningScopeIf(referrerScope, [&nameToCheck](const QQmlJSScope::ConstPtr &scope) {
++ return scope->hasOwnEnumeration(nameToCheck);
++ });
++ }
++
++ /*!
++ \internal
++ See also findDefiningScopeForProperty().
++ */
++ QQmlJSScope::ConstPtr findDefiningScopeForEnumerationKey(const QQmlJSScope::ConstPtr &referrerScope,
++ const QString &nameToCheck)
++ {
++ return findDefiningScopeIf(referrerScope, [&nameToCheck](const QQmlJSScope::ConstPtr &scope) {
++ return scope->hasOwnEnumerationKey(nameToCheck);
++ });
++ }
++
++ /*!
++ Filter away the parts of the Dom not needed for find usages, by following the profiler's
++ information.
++ 1. "propertyInfos" tries to require all inherited properties of some QmlObject. That is super
++ slow (profiler says it eats 90% of the time needed by `tst_qmlls_utils findUsages`!) and is not
++ needed for usages.
++ 2. "get" tries to resolve references, like base types saved in prototypes for example, and is not
++ needed to find usages. Profiler says it eats 70% of the time needed by `tst_qmlls_utils
++ findUsages`.
++ 3. "defaultPropertyName" also recurses through base types and is not needed to find usages.
++ */
++ static FieldFilter filterForFindUsages()
++ {
++ FieldFilter filter{ {},
++ {
++ { QString(), Fields::propertyInfos.toString() },
++ { QString(), Fields::defaultPropertyName.toString() },
++ { QString(), Fields::get.toString() },
++ } };
++ return filter;
++ };
++
++ static void findUsagesOfNonJSIdentifiers(const DomItem &item, const QString &name, Usages &result)
++ {
++ const auto expressionType = resolveExpressionType(item, ResolveOwnerType);
++ if (!expressionType)
++ return;
++
++ // for Qml file components: add their filename as an usage for the renaming operation
++ if (expressionType->type == QmlComponentIdentifier
++ && !expressionType->semanticScope->isInlineComponent()) {
++ result.appendFilenameUsage(expressionType->semanticScope->filePath());
++ }
++
++ const QStringList namesToCheck = namesOfPossibleUsages(name, item, expressionType->semanticScope);
++
++ const auto addLocationIfTypeMatchesTarget =
++ [&result, &expressionType, &item](const DomItem &toBeResolved, FileLocationRegion subRegion) {
++ const auto currentType =
++ resolveExpressionType(toBeResolved, ResolveOptions::ResolveOwnerType);
++ if (!currentType)
++ return;
++
++ const QQmlJSScope::ConstPtr target = expressionType->semanticScope;
++ const QQmlJSScope::ConstPtr current = currentType->semanticScope;
++ if (target == current) {
++ auto tree = FileLocations::treeOf(toBeResolved);
++ QQmlJS::SourceLocation sourceLocation;
++
++ sourceLocation = FileLocations::region(tree, subRegion);
++ if (!sourceLocation.isValid())
++ return;
++
++ if (auto location = Location::tryFrom(toBeResolved.canonicalFilePath(),
++ sourceLocation, item)) {
++ result.appendUsage(*location);
++ }
++ }
++ };
++
++ auto findUsages = [&addLocationIfTypeMatchesTarget, &name,
++ &namesToCheck](Path, const DomItem ¤t, bool) -> bool {
++ bool continueForChildren = true;
++ if (auto scope = current.semanticScope()) {
++ // is the current property shadowed by some JS identifier? ignore current + its children
++ if (scope->ownJSIdentifier(name)) {
++ return false;
++ }
++ }
++ switch (current.internalKind()) {
++ case DomType::QmlObject:
++ case DomType::Binding:
++ case DomType::MethodInfo:
++ case DomType::PropertyDefinition: {
++ const QString propertyName = current.field(Fields::name).value().toString();
++ if (namesToCheck.contains(propertyName))
++ addLocationIfTypeMatchesTarget(current, IdentifierRegion);
++ return continueForChildren;
++ }
++ case DomType::ScriptIdentifierExpression: {
++ const QString identifierName = current.field(Fields::identifier).value().toString();
++ if (namesToCheck.contains(identifierName))
++ addLocationIfTypeMatchesTarget(current, MainRegion);
++ return continueForChildren;
++ }
++ case DomType::ScriptLiteral: {
++ const QString literal = current.field(Fields::value).value().toString();
++ if (namesToCheck.contains(literal))
++ addLocationIfTypeMatchesTarget(current, MainRegion);
++ return continueForChildren;
++ }
++ case DomType::EnumItem: {
++ // Only look for the first enum defined. The inner enums
++ // have no way to be accessed.
++ const auto parentPath = current.containingObject().pathFromOwner();
++ const auto index = parentPath.last().headIndex();
++ if (index != 0)
++ return continueForChildren;
++ const QString enumValue = current.field(Fields::name).value().toString();
++ if (namesToCheck.contains(enumValue))
++ addLocationIfTypeMatchesTarget(current, IdentifierRegion);
++ return continueForChildren;
++ }
++ case DomType::EnumDecl: {
++ // Only look for the first enum defined. The inner enums
++ // have no way to be accessed.
++ const auto parentPath = current.pathFromOwner();
++ const auto index = parentPath.last().headIndex();
++ if (index != 0)
++ return continueForChildren;
++ const QString enumValue = current.field(Fields::name).value().toString();
++ if (namesToCheck.contains(enumValue))
++ addLocationIfTypeMatchesTarget(current, IdentifierRegion);
++ return continueForChildren;
++ }
++ default:
++ return continueForChildren;
++ };
++
++ Q_UNREACHABLE_RETURN(continueForChildren);
++ };
++
++ const DomItem qmlFiles = item.top().field(Fields::qmlFileWithPath);
++ const auto filter = filterForFindUsages();
++ for (const QString &file : qmlFiles.keys()) {
++ const DomItem currentFileComponents =
++ qmlFiles.key(file).field(Fields::currentItem).field(Fields::components);
++ currentFileComponents.visitTree(Path(), emptyChildrenVisitor,
++ VisitOption::Recurse | VisitOption::VisitSelf, findUsages,
++ emptyChildrenVisitor, filter);
++ }
++ }
++
++ static std::optional<Location> locationFromJSIdentifierDefinition(const DomItem &definitionOfItem,
++ const QString &name)
++ {
++ Q_ASSERT_X(!definitionOfItem.semanticScope().isNull()
++ && definitionOfItem.semanticScope()->ownJSIdentifier(name).has_value(),
++ "QQmlLSUtils::locationFromJSIdentifierDefinition",
++ "JS definition does not actually define the JS identifier. "
++ "Did you obtain definitionOfItem from findJSIdentifierDefinition() ?");
++ const QQmlJS::SourceLocation location =
++ definitionOfItem.semanticScope()->ownJSIdentifier(name).value().location;
++
++ return Location::tryFrom(definitionOfItem.canonicalFilePath(), location, definitionOfItem);
++ }
++
++ static void findUsagesHelper(const DomItem &item, const QString &name, Usages &result)
++ {
++ qCDebug(QQmlLSUtilsLog) << "Looking for JS identifier with name" << name;
++ DomItem definitionOfItem = findJSIdentifierDefinition(item, name);
++
++ // if there is no definition found: check if name was a property or an id instead
++ if (!definitionOfItem) {
++ qCDebug(QQmlLSUtilsLog) << "No defining JS-Scope found!";
++ findUsagesOfNonJSIdentifiers(item, name, result);
++ return;
++ }
++
++ definitionOfItem.visitTree(
++ Path(), emptyChildrenVisitor, VisitOption::VisitAdopted | VisitOption::Recurse,
++ [&name, &result](Path, const DomItem &item, bool) -> bool {
++ qCDebug(QQmlLSUtilsLog) << "Visiting a " << item.internalKindStr();
++ if (item.internalKind() == DomType::ScriptIdentifierExpression
++ && item.field(Fields::identifier).value().toString() == name) {
++ // add this usage
++ auto fileLocation = FileLocations::treeOf(item);
++ if (!fileLocation) {
++ qCWarning(QQmlLSUtilsLog) << "Failed finding filelocation of found usage";
++ return true;
++ }
++ const QQmlJS::SourceLocation sourceLocation = fileLocation->info().fullRegion;
++ const QString fileName = item.canonicalFilePath();
++ if (auto location = Location::tryFrom(fileName, sourceLocation, item))
++ result.appendUsage(*location);
++ return true;
++ }
++ QQmlJSScope::ConstPtr scope = item.semanticScope();
++ // Do not visit children if current JS identifier has been redefined.
++ if (scope && scope != item.directParent().semanticScope()
++ && scope->ownJSIdentifier(name)) {
++ // Note that if the current semantic scope == parent's semantic scope, then no
++ // redefinition actually took place. This happens for example in
++ // FunctionExpressions, where the body's semantic scope is the
++ // FunctionExpression's semantic scope.
++ return false;
++ }
++ return true;
++ },
++ emptyChildrenVisitor, filterForFindUsages());
++
++ if (const auto definition = locationFromJSIdentifierDefinition(definitionOfItem, name))
++ result.appendUsage(*definition);
++ }
++
++ Usages findUsagesOf(const DomItem &item)
++ {
++ Usages result;
++
++ switch (item.internalKind()) {
++ case DomType::ScriptIdentifierExpression: {
++ const QString name = item.field(Fields::identifier).value().toString();
++ findUsagesHelper(item, name, result);
++ break;
++ }
++ case DomType::ScriptVariableDeclarationEntry: {
++ const QString name = item.field(Fields::identifier).value().toString();
++ findUsagesHelper(item, name, result);
++ break;
++ }
++ case DomType::EnumDecl:
++ case DomType::EnumItem:
++ case DomType::QmlObject:
++ case DomType::PropertyDefinition:
++ case DomType::Binding:
++ case DomType::MethodInfo: {
++ const QString name = item.field(Fields::name).value().toString();
++ findUsagesHelper(item, name, result);
++ break;
++ }
++ case DomType::QmlComponent: {
++ QString name = item.field(Fields::name).value().toString();
++
++ // get rid of extra qualifiers
++ if (const auto dotIndex = name.indexOf(u'.'); dotIndex != -1)
++ name = name.sliced(dotIndex + 1);
++ findUsagesHelper(item, name, result);
++ break;
++ }
++ default:
++ qCDebug(QQmlLSUtilsLog) << item.internalKindStr()
++ << "was not implemented for QQmlLSUtils::findUsagesOf";
++ return result;
++ }
++
++ result.sort();
++
++ if (QQmlLSUtilsLog().isDebugEnabled()) {
++ qCDebug(QQmlLSUtilsLog) << "Found following usages in files:";
++ for (auto r : result.usagesInFile()) {
++ qCDebug(QQmlLSUtilsLog) << r.filename() << " @ " << r.sourceLocation().startLine << ":"
++ << r.sourceLocation().startColumn << " with length "
++ << r.sourceLocation().length;
++ }
++ qCDebug(QQmlLSUtilsLog) << "And following usages in file names:"
++ << result.usagesInFilename();
++ }
++
++ return result;
++ }
++
++ static std::optional<IdentifierType> hasMethodOrSignal(const QQmlJSScope::ConstPtr &scope,
++ const QString &name)
++ {
++ auto methods = scope->methods(name);
++ if (methods.isEmpty())
++ return {};
++
++ const bool isSignal = methods.front().methodType() == QQmlJSMetaMethodType::Signal;
++ IdentifierType type =
++ isSignal ? IdentifierType::SignalIdentifier : IdentifierType::MethodIdentifier;
++ return type;
++ }
++
++ /*!
++ \internal
++ Searches for a method by traversing the parent scopes.
++
++ We assume here that it is possible to call methods from parent scope to simplify things, as the
++ linting module already warns about calling methods from parent scopes.
++
++ Note: in QML, one can only call methods from the current scope, and from the QML file root scope.
++ Everything else needs a qualifier.
++ */
++ static std::optional<ExpressionType>
++ methodFromReferrerScope(const QQmlJSScope::ConstPtr &referrerScope, const QString &name,
++ ResolveOptions options)
++ {
++ for (QQmlJSScope::ConstPtr current = referrerScope; current; current = current->parentScope()) {
++ if (auto type = hasMethodOrSignal(current, name)) {
++ switch (options) {
++ case ResolveOwnerType:
++ return ExpressionType{ name, findDefiningScopeForMethod(current, name), *type };
++ case ResolveActualTypeForFieldMemberExpression:
++ // QQmlJSScopes were not implemented for methods yet, but JS functions have methods
++ // and properties see
++ // for the list of properties/methods of functions. Therefore return a null scope.
++ // see also code below for non-qualified method access
++ return ExpressionType{ name, {}, *type };
++ }
++ }
++
++ if (const auto signalName = QQmlSignalNames::handlerNameToSignalName(name)) {
++ if (auto type = hasMethodOrSignal(current, *signalName)) {
++ switch (options) {
++ case ResolveOwnerType:
++ return ExpressionType{ name, findDefiningScopeForMethod(current, *signalName),
++ SignalHandlerIdentifier };
++ case ResolveActualTypeForFieldMemberExpression:
++ // Properties and methods of JS methods are not supported yet
++ return ExpressionType{ name, {}, SignalHandlerIdentifier };
++ }
++ }
++ }
++ }
++ return {};
++ }
++
++
++ /*!
++ \internal
++ See comment on methodFromReferrerScope: the same applies to properties.
++ */
++ static std::optional<ExpressionType>
++ propertyFromReferrerScope(const QQmlJSScope::ConstPtr &referrerScope, const QString &propertyName,
++ ResolveOptions options)
++ {
++ for (QQmlJSScope::ConstPtr current = referrerScope; current; current = current->parentScope()) {
++ const auto resolved = resolveNameInQmlScope(propertyName, current);
++ if (!resolved)
++ continue;
++
++ if (auto property = current->property(resolved->name); property.isValid()) {
++ switch (options) {
++ case ResolveOwnerType:
++ return ExpressionType{ propertyName,
++ findDefiningScopeForProperty(current, resolved->name),
++ resolved->type };
++ case ResolveActualTypeForFieldMemberExpression:
++ return ExpressionType{ propertyName, property.type(), resolved->type };
++ }
++ }
++ }
++ return {};
++ }
++
++ /*!
++ \internal
++ See comment on methodFromReferrerScope: the same applies to property bindings.
++
++ If resolver is not null then it is used to resolve the id with which a generalized grouped
++ properties starts.
++ */
++ static std::optional<ExpressionType>
++ propertyBindingFromReferrerScope(const QQmlJSScope::ConstPtr &referrerScope, const QString &name,
++ ResolveOptions options, QQmlJSTypeResolver *resolverForIds)
++ {
++ const auto bindings = referrerScope->propertyBindings(name);
++ if (bindings.isEmpty())
++ return {};
++
++ const auto binding = bindings.begin();
++ const auto bindingType = binding->bindingType();
++ const bool bindingIsAttached = bindingType == QQmlSA::BindingType::AttachedProperty;
++ if (!bindingIsAttached && bindingType != QQmlSA::BindingType::GroupedProperty)
++ return {};
++
++ // Generalized grouped properties, like Bindings or PropertyChanges, for example, have bindings
++ // starting in an id (like `someId.someProperty: ...`).
++ // If `someid` is not a property and is a deferred name, then it should be an id.
++ if (!bindingIsAttached && !referrerScope->hasProperty(name)
++ && referrerScope->isNameDeferred(name)) {
++ if (!resolverForIds)
++ return {};
++
++ return ExpressionType {
++ name,
++ resolverForIds->typeForId(referrerScope, name,
++ QQmlJSScopesByIdOption::AssumeComponentsAreBound),
++ QmlObjectIdIdentifier
++ };
++ }
++
++ const auto typeIdentifier =
++ bindingIsAttached ? AttachedTypeIdentifierInBindingTarget : GroupedPropertyIdentifier;
++
++ const auto getScope = [bindingIsAttached, binding]() -> QQmlJSScope::ConstPtr {
++ if (bindingIsAttached)
++ return binding->attachedType();
++
++ return binding->groupType();
++ };
++
++ switch (options) {
++ case ResolveOwnerType: {
++ return ExpressionType{ name,
++ // note: always return the type of the attached type as the owner.
++ // Find usages on "Keys.", for example, should yield all usages of
++ // the "Keys" attached property.
++ bindingIsAttached
++ ? getScope()
++ : findDefiningScopeForProperty(referrerScope, name),
++ typeIdentifier };
++ }
++ case ResolveActualTypeForFieldMemberExpression:
++ return ExpressionType{ name, getScope(), typeIdentifier };
++ }
++ Q_UNREACHABLE_RETURN({});
++ }
++
++ /*! \internal
++ Finds the scope within the special elements like Connections,
++ PropertyChanges, Bindings or AnchorChanges.
++ */
++ static QQmlJSScope::ConstPtr findScopeOfSpecialItems(
++ const QQmlJSScope::ConstPtr &scope, const DomItem &item)
++ {
++ if (!scope)
++ return {};
++
++ const QSet<QString> specialItems = {u"QQmlConnections"_s,
++ u"QQuickPropertyChanges"_s,
++ u"QQmlBind"_s,
++ u"QQuickAnchorChanges"_s};
++
++ const auto special = QQmlJSUtils::searchBaseAndExtensionTypes(
++ scope, [&specialItems](const QQmlJSScope::ConstPtr &visitedScope) {
++ const auto typeName = visitedScope->internalName();
++ if (specialItems.contains(typeName))
++ return true;
++ return false;
++ });
++
++ if (!special)
++ return {};
++
++ // Perform target name search if there is binding to property "target"
++ QString targetName;
++ if (scope->hasOwnPropertyBindings(u"target"_s)) {
++ // TODO: propagate the whole binding.
++ // We can figure out the meaning of target in more cases.
++
++ DomItem current = item.qmlObject();
++ auto target = current.bindings().key(u"target"_s).index(0);
++ if (target) {
++ targetName = target.field(Fields::value)
++ .field(Fields::scriptElement)
++ .field(Fields::identifier)
++ .value()
++ .toString();
++ }
++ }
++
++ if (!targetName.isEmpty()) {
++ // target: someId
++ auto resolver = item.containingFile().ownerAs<QmlFile>()->typeResolver();
++ if (!resolver)
++ return {};
++
++ // Note: It does not have to be an ID. It can be a property.
++ return resolver->scopedType(scope, targetName);
++ }
++
++ if (item.internalKind() == DomType::Binding &&
++ item.field(Fields::bindingType).value().toInteger() == int(BindingType::OnBinding)) {
++ // Binding on sth : {} syntax
++ // Target scope is the current scope
++ return scope;
++ }
++ return scope->parentScope();
++ }
++
++ /*!
++ \internal
++ \brief Distinguishes singleton types from attached types and "regular" qml components.
++ */
++ static std::optional<ExpressionType>
++ resolveTypeName(const std::shared_ptr<QQmlJSTypeResolver> &resolver, const QString &name,
++ const DomItem &item, ResolveOptions options)
++ {
++ const auto scope = resolver->typeForName(name);
++ if (!scope)
++ return {};
++
++ if (scope->isSingleton())
++ return ExpressionType{ name, scope, IdentifierType::SingletonIdentifier };
++
++ // A type not followed by a field member expression is just a type. Otherwise, it could either
++ // be a type or an attached type!
++ if (!isFieldMemberBase(item))
++ return ExpressionType{ name, scope, QmlComponentIdentifier };
++
++ // take the right hand side and unwrap in case its a nested fieldmemberexpression
++ const DomItem rightHandSide = [&item]() {
++ const DomItem candidate = item.directParent().field(Fields::right);
++ // case B(a,b) for the right hand side of `a` in `a.b`
++ if (candidate != item)
++ return candidate;
++ // case B(B(a,b),c) for the right hand side of `b` in `a.b.c`
++ return item.directParent().directParent().field(Fields::right);
++ }();
++
++ if (rightHandSide.internalKind() != DomType::ScriptIdentifierExpression)
++ return ExpressionType{ name, scope, QmlComponentIdentifier };
++
++ const QString fieldMemberAccessName = rightHandSide.value().toString();
++ if (fieldMemberAccessName.isEmpty() || !fieldMemberAccessName.front().isLower())
++ return ExpressionType{ name, scope, QmlComponentIdentifier };
++
++ if (scope->attachedType()) {
++ return ExpressionType{ name, options == ResolveOwnerType ? scope : scope->attachedType(),
++ IdentifierType::AttachedTypeIdentifier };
++ }
++ return {};
++ }
++
++ static std::optional<ExpressionType> resolveFieldMemberExpressionType(const DomItem &item,
++ ResolveOptions options)
++ {
++ const QString name = item.field(Fields::identifier).value().toString();
++ DomItem parent = item.directParent();
++ auto owner = resolveExpressionType(parent.field(Fields::left),
++ ResolveOptions::ResolveActualTypeForFieldMemberExpression);
++ if (!owner)
++ return {};
++
++ if (!owner->semanticScope) {
++ // JS objects can get new members and methods during runtime and therefore has no
++ // qqmljsscopes. Therefore, just label everything inside a JavaScriptIdentifier as being
++ // another JavaScriptIdentifier.
++ if (owner->type == JavaScriptIdentifier) {
++ return ExpressionType{ name, {}, JavaScriptIdentifier };
++ } else if (owner->type == QualifiedModuleIdentifier) {
++ const QmlFile *qmlFile = item.fileObject().as<QmlFile>();
++ if (!qmlFile)
++ return {};
++ if (auto scope = resolveTypeName(
++ qmlFile->typeResolver(), u"%1.%2"_s.arg(*owner->name, name), item,
++ options)) {
++ // remove the qualified module name from the type name
++ scope->name = name;
++ return scope;
++ }
++ }
++ return {};
++ }
++
++ if (auto scope = methodFromReferrerScope(owner->semanticScope, name, options))
++ return *scope;
++
++ if (auto scope = propertyBindingFromReferrerScope(owner->semanticScope, name, options, nullptr))
++ return *scope;
++
++ if (auto scope = propertyFromReferrerScope(owner->semanticScope, name, options))
++ return *scope;
++
++ if (owner->type == QmlComponentIdentifier || owner->type == EnumeratorIdentifier) {
++ // Check if name is a enum value <TypeName>.<EnumValue> or ...<EnumName>.<EnumValue>
++ // Enumerations should live under the root element scope of the file that defines the enum,
++ // therefore use the DomItem to find the root element of the qml file instead of directly
++ // using owner->semanticScope.
++ if (const auto scope = owner->semanticScope->isComposite()
++ ? item.goToFile(owner->semanticScope->filePath())
++ .rootQmlObject(GoTo::MostLikely)
++ .semanticScope()
++ : owner->semanticScope) {
++ if (scope->hasEnumerationKey(name)) {
++ return ExpressionType{ name, scope, EnumeratorValueIdentifier };
++ }
++ // Or it is a enum name <TypeName>.<EnumName>.<EnumValue>
++ else if (scope->hasEnumeration(name)) {
++ return ExpressionType{ name, scope, EnumeratorIdentifier };
++ }
++ }
++
++ // check inline components <TypeName>.<InlineComponentName>
++ for (auto it = owner->semanticScope->childScopesBegin(),
++ end = owner->semanticScope->childScopesEnd();
++ it != end; ++it) {
++ if ((*it)->inlineComponentName() == name) {
++ return ExpressionType{ name, *it, QmlComponentIdentifier };
++ }
++ }
++ return {};
++ }
++
++ qCDebug(QQmlLSUtilsLog) << "Could not find identifier expression for" << item.internalKindStr();
++ return owner;
++ }
++
++ /*!
++ \internal
++ Resolves the expression type of a binding for signal handlers, like the function expression
++ \c{(x) => ...} in
++
++ \qml
++ onHelloSignal: (x) => ...
++ \endqml
++
++ would be resolved to the \c{onHelloSignal} expression type, for example.
++ */
++ static std::optional<ExpressionType> resolveBindingIfSignalHandler(const DomItem &functionExpression)
++ {
++ if (functionExpression.internalKind() != DomType::ScriptFunctionExpression)
++ return {};
++
++ const DomItem parent = functionExpression.directParent();
++ if (parent.internalKind() != DomType::ScriptExpression)
++ return {};
++
++ const DomItem grandParent = parent.directParent();
++ if (grandParent.internalKind() != DomType::Binding)
++ return {};
++
++ auto bindingType = resolveExpressionType(grandParent, ResolveOwnerType);
++ return bindingType;
++ }
++
++ /*!
++ \internal
++ In a signal handler
++
++ \qml
++ onSomeSignal: (x, y, z) => ....
++ \endqml
++
++ the parameters \c x, \c y and \c z are not allowed to have type annotations: instead, their type is
++ defined by the signal definition itself.
++
++ This code detects signal handler parameters and resolves their type using the signal's definition.
++ */
++ static std::optional<ExpressionType>
++ resolveSignalHandlerParameterType(const DomItem ¶meterDefinition, const QString &name,
++ ResolveOptions options)
++ {
++ const std::optional<QQmlJSScope::JavaScriptIdentifier> jsIdentifier =
++ parameterDefinition.semanticScope()->jsIdentifier(name);
++ if (!jsIdentifier || jsIdentifier->kind != QQmlJSScope::JavaScriptIdentifier::Parameter)
++ return {};
++
++ const DomItem handlerFunctionExpression =
++ parameterDefinition.internalKind() == DomType::ScriptBlockStatement
++ ? parameterDefinition.directParent()
++ : parameterDefinition;
++
++ const std::optional<ExpressionType> bindingType =
++ resolveBindingIfSignalHandler(handlerFunctionExpression);
++ if (!bindingType)
++ return {};
++
++ if (bindingType->type == PropertyChangedHandlerIdentifier)
++ return ExpressionType{};
++
++ if (bindingType->type != SignalHandlerIdentifier)
++ return {};
++
++ const DomItem parameters = handlerFunctionExpression[Fields::parameters];
++ const int indexOfParameter = [¶meters, &name]() {
++ for (int i = 0; i < parameters.indexes(); ++i) {
++ if (parameters[i][Fields::identifier].value().toString() == name)
++ return i;
++ }
++ Q_ASSERT_X(false, "resolveSignalHandlerParameter",
++ "can't find JS identifier with Parameter kind in the parameters");
++ Q_UNREACHABLE_RETURN(-1);
++ }();
++
++ const std::optional<QString> signalName =
++ QQmlSignalNames::handlerNameToSignalName(*bindingType->name);
++ Q_ASSERT_X(signalName.has_value(), "resolveSignalHandlerParameterType",
++ "handlerNameToSignalName failed on a SignalHandler");
++
++ const QQmlJSMetaMethod signalDefinition =
++ bindingType->semanticScope->methods(*signalName).front();
++ const QList<QQmlJSMetaParameter> parameterList = signalDefinition.parameters();
++
++ // not a signal handler parameter after all
++ if (parameterList.size() <= indexOfParameter)
++ return {};
++
++ // now we can return an ExpressionType, even if the indexOfParameter calculation result is only
++ // needed to check whether this is a signal handler parameter or not.
++ if (options == ResolveOwnerType)
++ return ExpressionType{ name, bindingType->semanticScope, JavaScriptIdentifier };
++ else {
++ const QQmlJSScope::ConstPtr parameterType = parameterList[indexOfParameter].type();
++ return ExpressionType{ name, parameterType, JavaScriptIdentifier };
++ }
++ }
++
++ /*!
++ \internal
++ resolve an identifier in the same order the QML engine would, that is:
++ * first check for local JS variables (`let x = ...`)
++ * then check for ids
++ * then check for properties
++ * then check for methods
++ * then check for properties and methods of the global object
++ */
++ static std::optional<ExpressionType> resolveIdentifierExpressionType(const DomItem &item,
++ ResolveOptions options)
++ {
++ if (isFieldMemberAccess(item) || isFieldMemberExpression(item)) {
++ return resolveFieldMemberExpressionType(item, options);
++ }
++
++ const QString name = item.field(Fields::identifier).value().toString();
++
++ if (const DomItem definitionOfItem = findJSIdentifierDefinition(item, name)) {
++ Q_ASSERT_X(!definitionOfItem.semanticScope().isNull()
++ && definitionOfItem.semanticScope()->ownJSIdentifier(name),
++ "QQmlLSUtils::findDefinitionOf",
++ "JS definition does not actually define the JS identifer. "
++ "It should be empty.");
++ if (auto parameter = resolveSignalHandlerParameterType(definitionOfItem, name, options))
++ return parameter;
++
++ const auto scope = definitionOfItem.semanticScope();
++ return ExpressionType{ name,
++ options == ResolveOwnerType
++ ? scope
++ : QQmlJSScope::ConstPtr(
++ scope->ownJSIdentifier(name)->scope.toStrongRef()),
++ IdentifierType::JavaScriptIdentifier };
++ }
++
++ const auto referrerScope = item.nearestSemanticScope();
++ if (!referrerScope)
++ return {};
++
++ const auto qmlFile = item.containingFile().ownerAs<QmlFile>();
++ if (!qmlFile)
++ return {};
++
++ const auto resolver = qmlFile->typeResolver();
++ if (!resolver)
++ return {};
++
++ // check if its an id
++ if (const QQmlJSScope::ConstPtr fromId =
++ resolver->typeForId(referrerScope, name, QQmlJSScopesByIdOption::AssumeComponentsAreBound)) {
++ return ExpressionType{ name, fromId, QmlObjectIdIdentifier };
++ }
++
++ // check if its found as a property binding
++ if (const auto scope = propertyBindingFromReferrerScope(
++ referrerScope, name, options, resolver.get())) {
++ return *scope;
++ }
++
++ // check if its an (unqualified) property
++ if (const auto scope = propertyFromReferrerScope(referrerScope, name, options))
++ return *scope;
++
++ // check if its a method
++ if (auto scope = methodFromReferrerScope(referrerScope, name, options))
++ return scope;
++
++ if (resolver->seenModuleQualifiers().contains(name))
++ return ExpressionType{ name, {}, QualifiedModuleIdentifier };
++
++ if (const auto scope = resolveTypeName(resolver, name, item, options))
++ return scope;
++
++ // the global object doesn't have properties and methods with the same name, therefore the
++ // order in which we resolve methods and properties on the global object doesn't matter
++ const QQmlJSScope::ConstPtr jsGlobal = resolver->jsGlobalObject();
++ // check if its a JS global method
++ if (auto scope = methodFromReferrerScope(jsGlobal, name, options)) {
++ scope->type = JavaScriptIdentifier;
++ return scope;
++ }
++
++ // check if its an JS global property
++ if (auto scope = propertyFromReferrerScope(jsGlobal, name, options)) {
++ scope->type = JavaScriptIdentifier;
++ return scope;
++ }
++
++ return {};
++ }
++
++ static std::optional<ExpressionType>
++ resolveSignalOrPropertyExpressionType(const QString &name, const QQmlJSScope::ConstPtr &scope,
++ ResolveOptions options)
++ {
++ const auto signalOrProperty = resolveNameInQmlScope(name, scope);
++ if (!signalOrProperty)
++ return {};
++
++ switch (signalOrProperty->type) {
++ case PropertyIdentifier:
++ switch (options) {
++ case ResolveOwnerType:
++ return ExpressionType{ name, findDefiningScopeForProperty(scope, name),
++ signalOrProperty->type };
++ case ResolveActualTypeForFieldMemberExpression:
++ return ExpressionType{ name, scope->property(name).type(), signalOrProperty->type };
++ }
++ Q_UNREACHABLE_RETURN({});
++ case PropertyChangedHandlerIdentifier:
++ switch (options) {
++ case ResolveOwnerType:
++ return ExpressionType{ name,
++ findDefiningScopeForProperty(scope, signalOrProperty->name),
++ signalOrProperty->type };
++ case ResolveActualTypeForFieldMemberExpression:
++ // Properties and methods are not implemented on methods.
++ Q_UNREACHABLE_RETURN({});
++ }
++ Q_UNREACHABLE_RETURN({});
++ case SignalHandlerIdentifier:
++ case PropertyChangedSignalIdentifier:
++ case SignalIdentifier:
++ case MethodIdentifier:
++ switch (options) {
++ case ResolveOwnerType: {
++ return ExpressionType{ name, findDefiningScopeForMethod(scope, signalOrProperty->name),
++ signalOrProperty->type };
++ }
++ case ResolveActualTypeForFieldMemberExpression:
++ // Properties and methods are not implemented on methods.
++ Q_UNREACHABLE_RETURN({});
++ }
++ Q_UNREACHABLE_RETURN({});
++ default:
++ Q_UNREACHABLE_RETURN({});
++ }
++ }
++
++ /*!
++ \internal
++ \brief
++ Resolves the type of the given DomItem, when possible (e.g., when there are enough type
++ annotations).
++
++ Might return an ExpressionType without(!) semantic scope when no type information is available, for
++ example resolving the type of x in someJSObject.x (where `let someJSObject = { x: 42 };`) then
++ the name and type of x is known but no semantic scope can be obtained.
++ */
++ std::optional<ExpressionType> resolveExpressionType(const QQmlJS::Dom::DomItem &item,
++ ResolveOptions options)
++ {
++ switch (item.internalKind()) {
++ case DomType::ScriptIdentifierExpression: {
++ return resolveIdentifierExpressionType(item, options);
++ }
++ case DomType::PropertyDefinition: {
++ auto propertyDefinition = item.as<PropertyDefinition>();
++ if (propertyDefinition && propertyDefinition->semanticScope()) {
++ const auto &scope = propertyDefinition->semanticScope();
++ switch (options) {
++ case ResolveOwnerType:
++ return ExpressionType{ propertyDefinition->name, scope, PropertyIdentifier };
++ case ResolveActualTypeForFieldMemberExpression:
++ // There should not be any PropertyDefinition inside a FieldMemberExpression.
++ Q_UNREACHABLE_RETURN({});
++ }
++ Q_UNREACHABLE_RETURN({});
++ }
++ return {};
++ }
++ case DomType::Binding: {
++ auto binding = item.as<Binding>();
++ if (binding) {
++ std::optional<QQmlJSScope::ConstPtr> owner = item.qmlObject().semanticScope();
++ if (!owner)
++ return {};
++ const QString name = binding->name();
++
++ if (name == u"id")
++ return ExpressionType{ name, owner.value(), QmlObjectIdIdentifier };
++
++ if (const QQmlJSScope::ConstPtr targetScope
++ = findScopeOfSpecialItems(owner.value(), item)) {
++ const auto signalOrProperty = resolveNameInQmlScope(name, targetScope);
++ if (!signalOrProperty)
++ return {};
++ switch (options) {
++ case ResolveOwnerType:
++ return ExpressionType{
++ name, findDefiningScopeForBinding(targetScope, signalOrProperty->name),
++ signalOrProperty->type
++ };
++ case ResolveActualTypeForFieldMemberExpression:
++ // Bindings can't be inside of FieldMemberExpressions.
++ Q_UNREACHABLE_RETURN({});
++ }
++ }
++ if (auto result = resolveSignalOrPropertyExpressionType(name, owner.value(), options)) {
++ return result;
++ }
++ qDebug(QQmlLSUtilsLog) << "QQmlLSUtils::resolveExpressionType() could not resolve the"
++ "type of a Binding.";
++ }
++
++ return {};
++ }
++ case DomType::QmlObject: {
++ auto object = item.as<QmlObject>();
++ if (!object)
++ return {};
++ if (auto scope = object->semanticScope()) {
++ const auto name = item.name();
++ const bool isComponent = name.front().isUpper();
++ if (isComponent)
++ scope = scope->baseType();
++ const IdentifierType type =
++ isComponent ? QmlComponentIdentifier : GroupedPropertyIdentifier;
++ switch (options) {
++ case ResolveOwnerType:
++ return ExpressionType{ name, scope, type };
++ case ResolveActualTypeForFieldMemberExpression:
++ return ExpressionType{ name, scope, type };
++ }
++ }
++ return {};
++ }
++ case DomType::QmlComponent: {
++ auto component = item.as<QmlComponent>();
++ if (!component)
++ return {};
++ const auto scope = component->semanticScope();
++ if (!scope)
++ return {};
++
++ QString name = item.name();
++ if (auto dotIndex = name.indexOf(u'.'); dotIndex != -1)
++ name = name.sliced(dotIndex + 1);
++ switch (options) {
++ case ResolveOwnerType:
++ return ExpressionType{ name, scope, QmlComponentIdentifier };
++ case ResolveActualTypeForFieldMemberExpression:
++ return ExpressionType{ name, scope, QmlComponentIdentifier };
++ }
++ Q_UNREACHABLE_RETURN({});
++ }
++ case DomType::ScriptFunctionExpression: {
++ return ExpressionType{ {}, item.semanticScope(), LambdaMethodIdentifier };
++ }
++ case DomType::MethodInfo: {
++ const auto object = item.as<MethodInfo>();
++ if (object && object->semanticScope()) {
++ std::optional<QQmlJSScope::ConstPtr> scope = object->semanticScope();
++ if (!scope)
++ return {};
++
++ if (const QQmlJSScope::ConstPtr targetScope =
++ findScopeOfSpecialItems(scope.value()->parentScope(), item)) {
++ const auto signalOrProperty = resolveNameInQmlScope(object->name, targetScope);
++ if (!signalOrProperty)
++ return {};
++
++ switch (options) {
++ case ResolveOwnerType:
++ return ExpressionType{ object->name,
++ findDefiningScopeForMethod(targetScope,
++ signalOrProperty->name),
++ signalOrProperty->type };
++ case ResolveActualTypeForFieldMemberExpression:
++ // not supported for methods
++ return {};
++ }
++ }
++
++ // in case scope is the semantic scope for the function bodies: grab the owner's scope
++ // this happens for all methods but not for signals (they do not have a body)
++ if (QQmlSA::isFunctionScope(scope.value()->scopeType()))
++ scope = scope.value()->parentScope();
++
++ if (const auto result = resolveSignalOrPropertyExpressionType(
++ object->name, scope.value(), options)) {
++ return result;
++ }
++ qDebug(QQmlLSUtilsLog) << "QQmlLSUtils::resolveExpressionType() could not resolve the"
++ "type of a MethodInfo.";
++ }
++
++ return {};
++ }
++ case DomType::ScriptBinaryExpression: {
++ if (isFieldMemberExpression(item)) {
++ return resolveExpressionType(item.field(Fields::right), options);
++ }
++ return {};
++ }
++ case DomType::ScriptLiteral: {
++ /* special case
++ Binding { target: someId; property: "someProperty"}
++ */
++ const auto scope = item.qmlObject().semanticScope();
++ const auto name = item.field(Fields::value).value().toString();
++ if (const QQmlJSScope::ConstPtr targetScope = findScopeOfSpecialItems(scope, item)) {
++ const auto signalOrProperty = resolveNameInQmlScope(name, targetScope);
++ if (!signalOrProperty)
++ return {};
++ switch (options) {
++ case ResolveOwnerType:
++ return ExpressionType{
++ name, findDefiningScopeForProperty(targetScope, signalOrProperty->name),
++ signalOrProperty->type
++ };
++ case ResolveActualTypeForFieldMemberExpression:
++ // ScriptLiteral's can't be inside of FieldMemberExpression's, especially when they
++ // are inside a special item.
++ Q_UNREACHABLE_RETURN({});
++ }
++ }
++ return {};
++ }
++ case DomType::EnumItem: {
++ const QString enumValue = item.field(Fields::name).value().toString();
++ const QQmlJSScope::ConstPtr referrerScope
++ = item.rootQmlObject(GoTo::MostLikely).semanticScope();
++ if (!referrerScope->hasEnumerationKey(enumValue))
++ return {};
++ switch (options) {
++ // special case: use the owner's scope here, as enums do not have their own
++ // QQmlJSScope.
++ case ResolveActualTypeForFieldMemberExpression:
++ case ResolveOwnerType:
++ return ExpressionType {
++ enumValue,
++ findDefiningScopeForEnumerationKey(referrerScope, enumValue),
++ EnumeratorValueIdentifier
++ };
++ }
++ Q_UNREACHABLE_RETURN({});
++ }
++ case DomType::EnumDecl: {
++ const QString enumName = item.field(Fields::name).value().toString();
++ const QQmlJSScope::ConstPtr referrerScope
++ = item.rootQmlObject(GoTo::MostLikely).semanticScope();
++ if (!referrerScope->hasEnumeration(enumName))
++ return {};
++ switch (options) {
++ // special case: use the owner's scope here, as enums do not have their own QQmlJSScope.
++ case ResolveActualTypeForFieldMemberExpression:
++ case ResolveOwnerType:
++ return ExpressionType {
++ enumName,
++ findDefiningScopeForEnumeration(referrerScope, enumName),
++ EnumeratorIdentifier
++ };
++ }
++
++ Q_UNREACHABLE_RETURN({});
++ }
++ case DomType::Import: {
++ // we currently only support qualified module identifiers
++ if (!item[Fields::importId])
++ return {};
++ return ExpressionType{ item[Fields::importId].value().toString(),
++ {},
++ QualifiedModuleIdentifier };
++ }
++ case DomType::ScriptNewMemberExpression: {
++ const auto name = item.field(Fields::base).value().toString();
++ return ExpressionType{ name, {}, JavaScriptIdentifier };
++ }
++ case DomType::ScriptCallExpression: {
++ const DomItem callee = item.field(Fields::callee);
++ const auto calleeExpressionType = resolveExpressionType(callee, ResolveOwnerType);
++
++ if (!calleeExpressionType || !calleeExpressionType->semanticScope
++ || !calleeExpressionType->name || calleeExpressionType->type != MethodIdentifier) {
++ return {};
++ }
++
++ const auto methods =
++ calleeExpressionType->semanticScope->methods(*calleeExpressionType->name);
++ if (methods.isEmpty())
++ return {};
++
++ const auto returnType = methods.front().returnType();
++ return ExpressionType{ {}, returnType, NotAnIdentifier };
++ }
++ default: {
++ qCDebug(QQmlLSUtilsLog) << "Type" << item.internalKindStr()
++ << "is unimplemented in QQmlLSUtils::resolveExpressionType";
++ return {};
++ }
++ }
++ Q_UNREACHABLE();
++ }
++
++ DomItem sourceLocationToDomItem(const DomItem &file, const QQmlJS::SourceLocation &location)
++ {
++ // QQmlJS::SourceLocation starts counting at 1 but the utils and the LSP start at 0.
++ auto items = QQmlLSUtils::itemsFromTextLocation(file, location.startLine - 1,
++ location.startColumn - 1);
++ switch (items.size()) {
++ case 0:
++ return {};
++ case 1:
++ return items.front().domItem;
++ case 2: {
++ // special case: because location points to the beginning of the type definition,
++ // itemsFromTextLocation might also return the type on its left, in case it is directly
++ // adjacent to it. In this case always take the right (=with the higher column-number)
++ // item.
++ auto &first = items.front();
++ auto &second = items.back();
++ Q_ASSERT_X(first.fileLocation->info().fullRegion.startLine
++ == second.fileLocation->info().fullRegion.startLine,
++ "QQmlLSUtils::findTypeDefinitionOf(DomItem)",
++ "QQmlLSUtils::itemsFromTextLocation returned non-adjacent items.");
++ if (first.fileLocation->info().fullRegion.startColumn
++ > second.fileLocation->info().fullRegion.startColumn)
++ return first.domItem;
++ else
++ return second.domItem;
++ break;
++ }
++ default:
++ qDebug() << "Found multiple candidates for type of scriptidentifierexpression";
++ break;
++ }
++ return {};
++ }
++
++ static std::optional<Location>
++ findEnumDefinitionOf(const DomItem &file, QQmlJS::SourceLocation location, const QString &name)
++ {
++ const DomItem enumeration = [&file, &location, &name]() -> DomItem {
++ const DomItem enumerations = QQmlLSUtils::sourceLocationToDomItem(file, location)
++ .qmlObject()
++ .component()
++ .field(Fields::enumerations);
++ const QSet<QString> enumerationNames = enumerations.keys();
++ for (const QString &enumName : enumerationNames) {
++ const DomItem currentKey = enumerations.key(enumName).index(0);
++ if (enumName == name)
++ return currentKey;
++ const DomItem values = currentKey.field(Fields::values);
++ for (int i = 0, end = values.size(); i < end; ++i) {
++ const DomItem currentValue = values.index(i);
++ if (currentValue.field(Fields::name).value().toStringView() == name)
++ return currentValue;
++ }
++ }
++ return {};
++ }();
++
++ auto fileLocation = FileLocations::treeOf(enumeration);
++
++ if (!fileLocation)
++ return {};
++
++ auto regions = fileLocation->info().regions;
++
++ if (auto it = regions.constFind(IdentifierRegion); it != regions.constEnd()) {
++ return Location::tryFrom(enumeration.canonicalFilePath(), *it, file);
++ }
++
++ return {};
++ }
++
++ static std::optional<Location>
++ findMethodDefinitionOf(const DomItem &file, QQmlJS::SourceLocation location, const QString &name)
++ {
++ DomItem owner = QQmlLSUtils::sourceLocationToDomItem(file, location).qmlObject();
++ DomItem method = owner.field(Fields::methods).key(name).index(0);
++ auto fileLocation = FileLocations::treeOf(method);
++ if (!fileLocation)
++ return {};
++
++ auto regions = fileLocation->info().regions;
++
++ if (auto it = regions.constFind(IdentifierRegion); it != regions.constEnd()) {
++ return Location::tryFrom(method.canonicalFilePath(), *it, file);
++ }
++
++ return {};
++ }
++
++ static std::optional<Location>
++ findPropertyDefinitionOf(const DomItem &file, QQmlJS::SourceLocation propertyDefinitionLocation,
++ const QString &name)
++ {
++ DomItem propertyOwner =
++ QQmlLSUtils::sourceLocationToDomItem(file, propertyDefinitionLocation).qmlObject();
++ DomItem propertyDefinition = propertyOwner.field(Fields::propertyDefs).key(name).index(0);
++ auto fileLocation = FileLocations::treeOf(propertyDefinition);
++ if (!fileLocation)
++ return {};
++
++ auto regions = fileLocation->info().regions;
++
++ if (auto it = regions.constFind(IdentifierRegion); it != regions.constEnd()) {
++ return Location::tryFrom(propertyDefinition.canonicalFilePath(), *it, file);
++ }
++
++ return {};
++ }
++
++ static QQmlJS::SourceLocation sourceLocationOrDefault(const QQmlJS::SourceLocation &location)
++ {
++ return location.startLine == 0 ? QQmlJS::s_documentOrigin : location;
++ }
++
++ static std::optional<Location> createCppTypeLocation(const QString &fileName,
++ const QStringList &headerLocations,
++ const QQmlJS::SourceLocation &location)
++ {
++ const TextPosition endPosition{ static_cast<int>(location.startLine) + 1, 1 };
++ const QFileInfo filePathInfo(fileName);
++ if (filePathInfo.isAbsolute() && filePathInfo.exists())
++ return Location{ fileName, location, endPosition };
++
++ const QString filePath = findFilePathFromFileName(headerLocations, fileName, { });
++ if (filePath.isEmpty()) {
++ qCWarning(QQmlLSUtilsLog) << "Couldn't find file '%1'."_L1.arg(fileName);
++ return {};
++ }
++
++ return Location{ filePath, location, endPosition };
++ }
++
++ /*!
++ \internal
++ Note that C++ types with foreign can have 2 locations for the definition: one where the
++ QML(_NAMED|)_ELEMENT macro is used and the second is the type targeted by QML_FOREIGN.
++ */
++ static QList<Location> findDefinitionOfType(const QQmlJSScope::ConstPtr &scope, const DomItem &item,
++ const QStringList &headerDirectories)
++ {
++ if (!scope)
++ return {};
++ if (scope->isComposite()) {
++ if (const auto result =
++ Location::tryFrom(scope->filePath(), scope->sourceLocation(), item)) {
++ return { *result };
++ }
++ }
++ QList<Location> result;
++ auto locationOfTypeOrForeign = createCppTypeLocation(
++ scope->filePath(), headerDirectories, sourceLocationOrDefault(scope->sourceLocation()));
++ if (locationOfTypeOrForeign)
++ result << *locationOfTypeOrForeign;
++
++ auto resolvedTypeLocation = createCppTypeLocation(
++ scope->resolvedFilePath(), headerDirectories,
++ QQmlJS::SourceLocation{ 0u, 0u, scope->lineNumberInResolvedFile(), 1u });
++ if (resolvedTypeLocation)
++ result << *resolvedTypeLocation;
++
++ return result;
++ }
++
++ template <typename T>
++ QList<T> optionalToList(std::optional<T> &&optional)
++ {
++ if (optional)
++ return { *optional };
++ return { };
++ }
++
++ QList<Location> findDefinitionOf(const DomItem &item, const QStringList &headerDirectories)
++ {
++ auto resolvedExpression = resolveExpressionType(item, ResolveOptions::ResolveOwnerType);
++
++ if (!resolvedExpression || !resolvedExpression->name
++ || (!resolvedExpression->semanticScope
++ && resolvedExpression->type != QualifiedModuleIdentifier)) {
++ qCDebug(QQmlLSUtilsLog) << "QQmlLSUtils::findDefinitionOf: Type could not be resolved.";
++ return {};
++ }
++
++ switch (resolvedExpression->type) {
++ case JavaScriptIdentifier: {
++ const auto jsIdentifier =
++ resolvedExpression->semanticScope->ownJSIdentifier(*resolvedExpression->name);
++ if (!jsIdentifier)
++ return {};
++
++ return optionalToList(Location::tryFrom(resolvedExpression->semanticScope->filePath(),
++ jsIdentifier->location, item));
++ }
++
++ case GroupedPropertyIdentifier:
++ case PropertyIdentifier: {
++ if (!resolvedExpression->semanticScope->isComposite()) {
++ return optionalToList(createCppTypeLocation(
++ resolvedExpression->semanticScope->filePath(), headerDirectories,
++ resolvedExpression->semanticScope->property(*resolvedExpression->name)
++ .sourceLocation()));
++ }
++ const DomItem ownerFile = item.goToFile(resolvedExpression->semanticScope->filePath());
++ const QQmlJS::SourceLocation ownerLocation =
++ resolvedExpression->semanticScope->sourceLocation();
++ return optionalToList(
++ findPropertyDefinitionOf(ownerFile, ownerLocation, *resolvedExpression->name));
++ }
++ case PropertyChangedSignalIdentifier:
++ case PropertyChangedHandlerIdentifier:
++ case SignalIdentifier:
++ case SignalHandlerIdentifier:
++ case MethodIdentifier: {
++ if (!resolvedExpression->semanticScope->isComposite()) {
++ const auto methods =
++ resolvedExpression->semanticScope->methods(*resolvedExpression->name);
++ if (methods.isEmpty())
++ return {};
++ return optionalToList(
++ createCppTypeLocation(resolvedExpression->semanticScope->filePath(),
++ headerDirectories, methods.front().sourceLocation()));
++ }
++ const DomItem ownerFile = item.goToFile(resolvedExpression->semanticScope->filePath());
++ const QQmlJS::SourceLocation ownerLocation =
++ resolvedExpression->semanticScope->sourceLocation();
++ return optionalToList(
++ findMethodDefinitionOf(ownerFile, ownerLocation, *resolvedExpression->name));
++ }
++ case QmlObjectIdIdentifier: {
++ DomItem qmlObject = QQmlLSUtils::sourceLocationToDomItem(
++ item.containingFile(), resolvedExpression->semanticScope->sourceLocation());
++ // in the Dom, the id is saved in a QMultiHash inside the Component of an QmlObject.
++ const DomItem domId = qmlObject.component()
++ .field(Fields::ids)
++ .key(*resolvedExpression->name)
++ .index(0)
++ .field(Fields::value);
++ if (!domId) {
++ qCDebug(QQmlLSUtilsLog)
++ << "QmlComponent in Dom structure has no id, was it misconstructed?";
++ return {};
++ }
++
++ return optionalToList(Location::tryFrom(
++ domId.canonicalFilePath(), FileLocations::treeOf(domId)->info().fullRegion, domId));
++ }
++ case AttachedTypeIdentifier:
++ return findDefinitionOfType(resolvedExpression->semanticScope->attachedType(), item,
++ headerDirectories);
++ case AttachedTypeIdentifierInBindingTarget:
++ return findDefinitionOfType(resolvedExpression->semanticScope->baseType(), item,
++ headerDirectories);
++ case QmlComponentIdentifier:
++ case SingletonIdentifier:
++ return findDefinitionOfType(resolvedExpression->semanticScope, item, headerDirectories);
++ case QualifiedModuleIdentifier: {
++ const DomItem imports = item.fileObject().field(Fields::imports);
++ for (int i = 0; i < imports.indexes(); ++i) {
++ if (imports[i][Fields::importId].value().toString() == resolvedExpression->name) {
++ const auto fileLocations = FileLocations::treeOf(imports[i]);
++ if (!fileLocations)
++ continue;
++
++ return optionalToList(Location::tryFrom(item.canonicalFilePath(),
++ fileLocations->info().regions[IdNameRegion],
++ item));
++ }
++ }
++ return {};
++ }
++ case EnumeratorIdentifier:
++ case EnumeratorValueIdentifier: {
++ if (!resolvedExpression->semanticScope->isComposite()) {
++ const auto enumerations = resolvedExpression->semanticScope->enumerations();
++ for (const auto &enumeration : enumerations) {
++ if (enumeration.hasKey(*resolvedExpression->name)
++ || enumeration.name() == *resolvedExpression->name) {
++ return optionalToList(createCppTypeLocation(
++ resolvedExpression->semanticScope->filePath(), headerDirectories,
++ sourceLocationOrDefault(QQmlJS::SourceLocation::fromQSizeType(
++ 0, 0, enumeration.lineNumber(), 1))));
++ }
++ }
++ return {};
++ }
++ const DomItem ownerFile = item.goToFile(resolvedExpression->semanticScope->filePath());
++ const QQmlJS::SourceLocation ownerLocation =
++ resolvedExpression->semanticScope->sourceLocation();
++ return optionalToList(
++ findEnumDefinitionOf(ownerFile, ownerLocation, *resolvedExpression->name));
++ }
++
++ case LambdaMethodIdentifier:
++ case NotAnIdentifier:
++ qCDebug(QQmlLSUtilsLog) << "QQmlLSUtils::findDefinitionOf was not implemented for type"
++ << resolvedExpression->type;
++ return {};
++ }
++ Q_UNREACHABLE_RETURN({});
++ }
++
++ static QQmlJSScope::ConstPtr propertyOwnerFrom(const QQmlJSScope::ConstPtr &type,
++ const QString &name)
++ {
++ Q_ASSERT(!name.isEmpty());
++ Q_ASSERT(type);
++
++ QQmlJSScope::ConstPtr typeWithDefinition = type;
++ while (typeWithDefinition && !typeWithDefinition->hasOwnProperty(name))
++ typeWithDefinition = typeWithDefinition->baseType();
++
++ if (!typeWithDefinition) {
++ qCDebug(QQmlLSUtilsLog)
++ << "QQmlLSUtils::checkNameForRename cannot find property definition,"
++ " ignoring.";
++ }
++
++ return typeWithDefinition;
++ }
++
++ static QQmlJSScope::ConstPtr methodOwnerFrom(const QQmlJSScope::ConstPtr &type,
++ const QString &name)
++ {
++ Q_ASSERT(!name.isEmpty());
++ Q_ASSERT(type);
++
++ QQmlJSScope::ConstPtr typeWithDefinition = type;
++ while (typeWithDefinition && !typeWithDefinition->hasOwnMethod(name))
++ typeWithDefinition = typeWithDefinition->baseType();
++
++ if (!typeWithDefinition) {
++ qCDebug(QQmlLSUtilsLog)
++ << "QQmlLSUtils::checkNameForRename cannot find method definition,"
++ " ignoring.";
++ }
++
++ return typeWithDefinition;
++ }
++
++ static QQmlJSScope::ConstPtr expressionTypeWithDefinition(const ExpressionType &ownerType)
++ {
++ 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 SingletonIdentifier:
++ case EnumeratorIdentifier:
++ case EnumeratorValueIdentifier:
++ case AttachedTypeIdentifier:
++ case AttachedTypeIdentifierInBindingTarget:
++ case GroupedPropertyIdentifier:
++ case QmlComponentIdentifier:
++ case LambdaMethodIdentifier:
++ case QualifiedModuleIdentifier:
++ case NotAnIdentifier:
++ return ownerType.semanticScope;
++ }
++ return {};
++ }
++
++ std::optional<ErrorMessage> checkNameForRename(const DomItem &item, const QString &dirtyNewName,
++ const std::optional<ExpressionType> &ownerType)
++ {
++ if (!ownerType) {
++ if (const auto resolved = resolveExpressionType(item, ResolveOwnerType))
++ return checkNameForRename(item, dirtyNewName, resolved);
++ }
++
++ // general checks for ECMAscript identifiers
++ if (!isValidEcmaScriptIdentifier(dirtyNewName))
++ return ErrorMessage{ 0, u"Invalid EcmaScript identifier!"_s };
++
++ const auto userSemanticScope = item.nearestSemanticScope();
++
++ if (!ownerType || !userSemanticScope) {
++ return ErrorMessage{ 0, u"Requested item cannot be renamed."_s };
++ }
++
++ // type specific checks
++ switch (ownerType->type) {
++ case PropertyChangedSignalIdentifier: {
++ if (!QQmlSignalNames::isChangedSignalName(dirtyNewName)) {
++ return ErrorMessage{ 0, u"Invalid name for a property changed signal."_s };
++ }
++ break;
++ }
++ case PropertyChangedHandlerIdentifier: {
++ if (!QQmlSignalNames::isChangedHandlerName(dirtyNewName)) {
++ return ErrorMessage{ 0, u"Invalid name for a property changed handler identifier."_s };
++ }
++ break;
++ }
++ case SignalHandlerIdentifier: {
++ if (!QQmlSignalNames::isHandlerName(dirtyNewName)) {
++ return ErrorMessage{ 0, u"Invalid name for a signal handler identifier."_s };
++ }
++ break;
++ }
++ // TODO: any other specificities?
++ case QmlObjectIdIdentifier:
++ if (dirtyNewName.front().isLetter() && !dirtyNewName.front().isLower()) {
++ return ErrorMessage{ 0, u"Object id names cannot start with an upper case letter."_s };
++ }
++ break;
++ case JavaScriptIdentifier:
++ case PropertyIdentifier:
++ case SignalIdentifier:
++ case MethodIdentifier:
++ default:
++ break;
++ };
++
++ auto typeWithDefinition = expressionTypeWithDefinition(*ownerType);
++
++ if (!typeWithDefinition) {
++ return ErrorMessage{
++ 0,
++ u"Renaming has not been implemented for the requested item."_s,
++ };
++ }
++
++ // is it not defined in QML?
++ if (!typeWithDefinition->isComposite()) {
++ return ErrorMessage{ 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->moduleName();
++ if (moduleOfDefinition != moduleOfCurrentItem) {
++ return ErrorMessage{
++ 0,
++ u"Cannot rename items defined in the \"%1\" module from a usage in the \"%2\" module."_s
++ .arg(moduleOfDefinition, moduleOfCurrentItem),
++ };
++ }
++
++ return {};
++ }
++
++ static std::optional<QString> oldNameFrom(const DomItem &item)
++ {
++ switch (item.internalKind()) {
++ case DomType::ScriptIdentifierExpression: {
++ return item.field(Fields::identifier).value().toString();
++ }
++ case DomType::ScriptVariableDeclarationEntry: {
++ return item.field(Fields::identifier).value().toString();
++ }
++ case DomType::PropertyDefinition:
++ case DomType::Binding:
++ case DomType::MethodInfo: {
++ return item.field(Fields::name).value().toString();
++ }
++ default:
++ qCDebug(QQmlLSUtilsLog) << item.internalKindStr()
++ << "was not implemented for QQmlLSUtils::renameUsagesOf";
++ return std::nullopt;
++ }
++ Q_UNREACHABLE_RETURN(std::nullopt);
++ }
++
++ static std::optional<QString> newNameFrom(const QString &dirtyNewName, IdentifierType alternative)
++ {
++ // When renaming signal/property changed handlers and property changed signals:
++ // Get the actual corresponding signal name (for signal handlers) or property name (for
++ // property changed signal + handlers) that will be used for the renaming.
++ switch (alternative) {
++ case SignalHandlerIdentifier: {
++ return QQmlSignalNames::handlerNameToSignalName(dirtyNewName);
++ }
++ case PropertyChangedHandlerIdentifier: {
++ return QQmlSignalNames::changedHandlerNameToPropertyName(dirtyNewName);
++ }
++ case PropertyChangedSignalIdentifier: {
++ return QQmlSignalNames::changedSignalNameToPropertyName(dirtyNewName);
++ }
++ case SignalIdentifier:
++ case PropertyIdentifier:
++ default:
++ return std::nullopt;
++ }
++ Q_UNREACHABLE_RETURN(std::nullopt);
++ }
++
++ /*!
++ \internal
++ \brief Rename the appearance of item to newName.
++
++ Special cases:
++ \list
++ \li Renaming a property changed signal or property changed handler does the same as renaming
++ the underlying property, except that newName gets
++ \list
++ \li its "on"-prefix and "Changed"-suffix chopped of if item is a property changed handlers
++ \li its "Changed"-suffix chopped of if item is a property changed signals
++ \endlist
++ \li Renaming a signal handler does the same as renaming a signal, but the "on"-prefix in newName
++ is chopped of.
++
++ All of the chopping operations are done using the static helpers from QQmlSignalNames.
++ \endlist
++ */
++ RenameUsages renameUsagesOf(const DomItem &item, const QString &dirtyNewName,
++ const std::optional<ExpressionType> &targetType)
++ {
++ RenameUsages result;
++ const Usages locations = findUsagesOf(item);
++ if (locations.isEmpty())
++ return result;
++
++ auto oldName = oldNameFrom(item);
++ if (!oldName)
++ return result;
++
++ QQmlJSScope::ConstPtr semanticScope;
++ if (targetType) {
++ semanticScope = targetType->semanticScope;
++ } else if (const auto resolved =
++ QQmlLSUtils::resolveExpressionType(item, ResolveOptions::ResolveOwnerType)) {
++ semanticScope = resolved->semanticScope;
++ } else {
++ return result;
++ }
++
++ QString newName;
++ if (const auto resolved = resolveNameInQmlScope(*oldName, semanticScope)) {
++ newName = newNameFrom(dirtyNewName, resolved->type).value_or(dirtyNewName);
++ oldName = resolved->name;
++ } else {
++ newName = dirtyNewName;
++ }
++
++ const qsizetype oldNameLength = oldName->length();
++ const qsizetype oldHandlerNameLength =
++ QQmlSignalNames::signalNameToHandlerName(*oldName).length();
++ const qsizetype oldChangedSignalNameLength =
++ QQmlSignalNames::propertyNameToChangedSignalName(*oldName).length();
++ const qsizetype oldChangedHandlerNameLength =
++ QQmlSignalNames::propertyNameToChangedHandlerName(*oldName).length();
++
++ const QString newHandlerName = QQmlSignalNames::signalNameToHandlerName(newName);
++ const QString newChangedSignalName = QQmlSignalNames::propertyNameToChangedSignalName(newName);
++ const QString newChangedHandlerName =
++ QQmlSignalNames::propertyNameToChangedHandlerName(newName);
++
++ // set the new name at the found usages, but add "on"-prefix and "Changed"-suffix if needed
++ for (const auto &location : locations.usagesInFile()) {
++ const qsizetype currentLength = location.sourceLocation().length;
++ Edit edit;
++ edit.location = location;
++ if (oldNameLength == currentLength) {
++ // normal case, nothing to do
++ edit.replacement = newName;
++
++ } else if (oldHandlerNameLength == currentLength) {
++ // signal handler location
++ edit.replacement = newHandlerName;
++
++ } else if (oldChangedSignalNameLength == currentLength) {
++ // property changed signal location
++ edit.replacement = newChangedSignalName;
++
++ } else if (oldChangedHandlerNameLength == currentLength) {
++ // property changed handler location
++ edit.replacement = newChangedHandlerName;
++
++ } else {
++ qCDebug(QQmlLSUtilsLog) << "Found usage with wrong identifier length, ignoring...";
++ continue;
++ }
++ result.appendRename(edit);
++ }
++
++ for (const auto &filename : locations.usagesInFilename()) {
++ // assumption: we only rename files ending in .qml or .ui.qml in qmlls
++ QString extension;
++ if (filename.endsWith(u".ui.qml"_s))
++ extension = u".ui.qml"_s;
++ else if (filename.endsWith(u".qml"_s))
++ extension = u".qml"_s;
++ else
++ continue;
++
++ QFileInfo info(filename);
++ // do not rename the file if it has a custom type name in the qmldir
++ if (!info.isFile() || info.baseName() != oldName)
++ continue;
++
++ const QString newFilename =
++ QDir::cleanPath(filename + "/.."_L1) + '/'_L1 + newName + extension;
++ result.appendRename({ filename, newFilename });
++ }
++
++ return result;
++ }
++
++ std::optional<Location> Location::tryFrom(const QString &fileName,
++ const QQmlJS::SourceLocation &sourceLocation,
++ const QQmlJS::Dom::DomItem &someItem)
++ {
++ auto qmlFile = someItem.goToFile(fileName).ownerAs<QQmlJS::Dom::QmlFile>();
++ if (!qmlFile) {
++ qDebug() << "Could not find file" << fileName << "in the dom!";
++ return {};
++ }
++ return Location{ fileName, sourceLocation,
++ textRowAndColumnFrom(qmlFile->code(), sourceLocation.end()) };
++ }
++
++ Location Location::from(const QString &fileName, const QQmlJS::SourceLocation &sourceLocation, const QString &code)
++ {
++ return Location{ fileName, sourceLocation, textRowAndColumnFrom(code, sourceLocation.end()) };
++ }
++
++ Location Location::from(const QString &fileName, const QString &code, qsizetype startLine,
++ qsizetype startCharacter, qsizetype length)
++ {
++ const auto offset = QQmlLSUtils::textOffsetFrom(code, startLine - 1, startCharacter - 1);
++ return from(fileName,
++ QQmlJS::SourceLocation::fromQSizeType(offset, length, startLine, startCharacter),
++ code);
++ }
++
++ Edit Edit::from(const QString &fileName, const QString &code, qsizetype startLine,
++ qsizetype startCharacter, qsizetype length, const QString &newName)
++ {
++ Edit rename;
++ rename.location = Location::from(fileName, code, startLine, startCharacter, length);
++ rename.replacement = newName;
++ return rename;
++ }
++
++ bool 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);
++ }
++
++
++ /*!
++ \internal
++ Returns the name of the cmake program along with the arguments needed to build the
++ qmltyperegistration. This command generates the .qmltypes, qmldir and .qrc files required for qmlls
++ to provide correct information on C++ defined QML elements.
++
++ We assume here that CMake is available in the path. This should be the case for linux and macOS by
++ default.
++ For windows, having CMake in the path is not too unrealistic, for example,
++ https://doc.qt.io/qt-6/windows-building.html#step-2-install-build-requirements claims that you need
++ to have CMake in your path to build Qt. So a developer machine running qmlls has a high chance of
++ having CMake in their path, if CMake is installed and used.
++ */
++ std::pair<QString, QStringList> cmakeBuildCommand(const QString &path, int cmakeJobs)
++ {
++ std::pair<QString, QStringList> result{
++ u"cmake"_s, { u"--build"_s, path, u"-t"_s, u"all_qmltyperegistrations"_s }
++ };
++ if (cmakeJobs > 0)
++ result.second << "-j"_L1 << QString::number(cmakeJobs);
++ return result;
++ }
++
++ void Usages::sort()
++ {
++ std::sort(m_usagesInFile.begin(), m_usagesInFile.end());
++ std::sort(m_usagesInFilename.begin(), m_usagesInFilename.end());
++ }
++
++ bool Usages::isEmpty() const
++ {
++ return m_usagesInFilename.isEmpty() && m_usagesInFile.isEmpty();
++ }
++
++ Usages::Usages(const QList<Location> &usageInFile, const QList<QString> &usageInFilename)
++ : m_usagesInFile(usageInFile), m_usagesInFilename(usageInFilename)
++ {
++ std::sort(m_usagesInFile.begin(), m_usagesInFile.end());
++ std::sort(m_usagesInFilename.begin(), m_usagesInFilename.end());
++ }
++
++ RenameUsages::RenameUsages(const QList<Edit> &renamesInFile,
++ const QList<FileRename> &renamesInFilename)
++ : m_renamesInFile(renamesInFile), m_renamesInFilename(renamesInFilename)
++ {
++ std::sort(m_renamesInFile.begin(), m_renamesInFile.end());
++ std::sort(m_renamesInFilename.begin(), m_renamesInFilename.end());
++ }
++
++ enum SearchOption { FindFirst, FindAll };
++ static QStringList findFilePathsFromFileNamesImpl(const QStringList &rootDirs,
++ const QStringList &fileNamesToSearch,
++ SearchOption option,
++ const QSet<QString> &ignoredFilePaths)
++ {
++ if (fileNamesToSearch.isEmpty() || rootDirs.isEmpty())
++ return {};
++
++ const qint64 maxFilesToSearch =
++ qEnvironmentVariableIntegerValue("QMLLS_MAX_FILES_TO_SEARCH")
++ .value_or(20'000); // 20'000 entries need around one second on my machine
++ qint64 visitedItems = 0;
++
++ QStringList result;
++ std::queue<QString> toVisit;
++ for (const QString &rootDir : rootDirs)
++ toVisit.push(rootDir);
++
++ while (!toVisit.empty()) {
++ const QString current = toVisit.front();
++ toVisit.pop();
++
++ for (const auto &entry : QDirListing{ current, QDirListing::IteratorFlag::ExcludeOther }) {
++ // timeout to avoid qmlls from recursing too much, in case rootDir is the filesystem
++ // root for example
++ if (++visitedItems > maxFilesToSearch) {
++ qInfo(QQmlLSUtilsLog).noquote().nospace()
++ << "Aborting search for \"" << fileNamesToSearch.join("\", \""_L1)
++ << "\" inside \"" << rootDirs.join("\", \""_L1)
++ << "\" after reaching QMLLS_MAX_FILES_TO_SEARCH (currently set to "
++ << maxFilesToSearch
++ << "). Set the environment variable \"QMLLS_MAX_FILES_TO_SEARCH\" to a "
++ "higher value to spend more time on searching.";
++ return result;
++ }
++
++ if (entry.isDir()) {
++ toVisit.push(entry.filePath());
++ continue;
++ }
++ Q_ASSERT(entry.isFile());
++ if (!fileNamesToSearch.contains(entry.fileName()))
++ continue;
++
++ if (ignoredFilePaths.contains(entry.absoluteFilePath()))
++ continue;
++
++ if (option == FindFirst)
++ return { entry.absoluteFilePath() };
++
++ result << entry.absoluteFilePath();
++ }
++ }
++
++ return result;
++ }
++
++ QStringList findFilePathsFromFileNames(const QString &rootDir, const QStringList &fileNamesToSearch,
++ const QSet<QString> &ignoredFilePaths)
++ {
++ return findFilePathsFromFileNamesImpl({ rootDir }, fileNamesToSearch, FindAll,
++ ignoredFilePaths);
++ }
++
++ QString findFilePathFromFileName(const QStringList &rootDirs, const QString &fileNameToSearch,
++ const QSet<QString> &ignoredFilePaths)
++ {
++ const QStringList result = findFilePathsFromFileNamesImpl(rootDirs, { fileNameToSearch },
++ FindFirst, ignoredFilePaths);
++ return result.isEmpty() ? QString{} : result.front();
++ }
++
++ } // namespace QQmlLSUtils
++
++ QT_END_NAMESPACE