1// Copyright (C) 2023 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qqmllsutils_p.h"
5
6#include <QtCore/qassert.h>
7#include <QtLanguageServer/private/qlanguageserverspectypes_p.h>
8#include <QtCore/qthreadpool.h>
9#include <QtCore/private/qduplicatetracker_p.h>
10#include <QtCore/QRegularExpression>
11#include <QtQmlDom/private/qqmldomexternalitems_p.h>
12#include <QtQmlDom/private/qqmldomtop_p.h>
13#include <QtQmlDom/private/qqmldomscriptelements_p.h>
14#include <QtQmlDom/private/qqmldom_utils_p.h>
15#include <QtQml/private/qqmlsignalnames_p.h>
16#include <QtQml/private/qqmljslexer_p.h>
17#include <QtQmlCompiler/private/qqmljsutils_p.h>
18
19#include <algorithm>
20#include <iterator>
21#include <memory>
22#include <optional>
23#include <set>
24#include <stack>
25#include <type_traits>
26#include <utility>
27#include <variant>
28
29using namespace QQmlJS::Dom;
30using namespace Qt::StringLiterals;
31
32QT_BEGIN_NAMESPACE
33
34Q_LOGGING_CATEGORY(QQmlLSUtilsLog, "qt.languageserver.utils")
35
36namespace QQmlLSUtils {
37QString qualifiersFrom(const DomItem &el)
38{
39 const bool isAccess = QQmlLSUtils::isFieldMemberAccess(item: el);
40 if (!isAccess && !QQmlLSUtils::isFieldMemberExpression(item: el))
41 return {};
42
43 const DomItem fieldMemberExpressionBeginning = el.filterUp(
44 filter: [](DomType, const DomItem &item) { return !QQmlLSUtils::isFieldMemberAccess(item); },
45 options: FilterUpOptions::ReturnOuter);
46 QStringList qualifiers =
47 QQmlLSUtils::fieldMemberExpressionBits(item: fieldMemberExpressionBeginning, stopAtChild: el);
48
49 QString result;
50 for (const QString &qualifier : qualifiers)
51 result.append(s: qualifier).append(c: QChar(u'.'));
52 return result;
53}
54
55/*!
56 \internal
57 Helper to check if item is a Field Member Expression \c {<someExpression>.propertyName}.
58*/
59bool isFieldMemberExpression(const DomItem &item)
60{
61 return item.internalKind() == DomType::ScriptBinaryExpression
62 && item.field(name: Fields::operation).value().toInteger()
63 == ScriptElements::BinaryExpression::FieldMemberAccess;
64}
65
66/*!
67 \internal
68 Helper to check if item is a Field Member Access \c memberAccess in
69 \c {<someExpression>.memberAccess}.
70*/
71bool isFieldMemberAccess(const DomItem &item)
72{
73 auto parent = item.directParent();
74 if (!isFieldMemberExpression(item: parent))
75 return false;
76
77 DomItem rightHandSide = parent.field(name: Fields::right);
78 return item == rightHandSide;
79}
80
81/*!
82 \internal
83 Helper to check if item is a Field Member Base \c base in
84 \c {base.memberAccess}.
85*/
86bool isFieldMemberBase(const DomItem &item)
87{
88 auto parent = item.directParent();
89 if (!isFieldMemberExpression(item: parent))
90 return false;
91
92 // First case, checking `a` for being a base in `a.b`: a is the left hand side of the binary
93 // expression B(a,b).
94 const DomItem leftHandSide = parent.field(name: Fields::left);
95 if (item == leftHandSide)
96 return true;
97
98 // Second case, checking `d` for being a base in `a.b.c.d.e.f.g`: the binary expressions are
99 // nested as following: B(B(B(B(B(B(a,b),c),d),e),f),g) so for `d`, check whether its
100 // grandparent B(B(B(B(a,b),c),d),e), which has `e` on its right hand side, is a binary
101 // expression.
102 const DomItem grandParent = parent.directParent();
103 return isFieldMemberExpression(item: grandParent) && grandParent.field(name: Fields::left) == parent;
104}
105
106/*!
107 \internal
108 Get the bits of a field member expression, like \c{a}, \c{b} and \c{c} for \c{a.b.c}.
109
110 stopAtChild can either be an FieldMemberExpression, a ScriptIdentifierExpression or a default
111 constructed DomItem: This exits early before processing Field::right of an
112 FieldMemberExpression stopAtChild, or before processing a ScriptIdentifierExpression stopAtChild.
113 No early exits if stopAtChild is default constructed.
114*/
115QStringList fieldMemberExpressionBits(const DomItem &item, const DomItem &stopAtChild)
116{
117 const bool isAccess = isFieldMemberAccess(item);
118 const bool isExpression = isFieldMemberExpression(item);
119
120 // assume it is a non-qualified name
121 if (!isAccess && !isExpression)
122 return { item.value().toString() };
123
124 const DomItem stopMarker =
125 isFieldMemberExpression(item: stopAtChild) ? stopAtChild : stopAtChild.directParent();
126
127 QStringList result;
128 DomItem current =
129 isAccess ? item.directParent() : (isFieldMemberExpression(item) ? item : DomItem{});
130
131 for (; isFieldMemberExpression(item: current); current = current.field(name: Fields::right)) {
132 result << current.field(name: Fields::left).value().toString();
133
134 if (current == stopMarker)
135 return result;
136 }
137 result << current.value().toString();
138
139 return result;
140}
141
142/*!
143 \internal
144 The language server protocol calls "URI" what QML calls "URL".
145 According to RFC 3986, a URL is a special case of URI that not only
146 identifies a resource but also shows how to access it.
147 In QML, however, URIs are distinct from URLs. URIs are the
148 identifiers of modules, for example "QtQuick.Controls".
149 In order to not confuse the terms we interpret language server URIs
150 as URLs in the QML code model.
151 This method marks a point of translation between the terms, but does
152 not have to change the actual URI/URL.
153
154 \sa QQmlLSUtils::qmlUriToLspUrl
155 */
156QByteArray lspUriToQmlUrl(const QByteArray &uri)
157{
158 return uri;
159}
160
161QByteArray qmlUrlToLspUri(const QByteArray &url)
162{
163 return url;
164}
165
166/*!
167 \internal
168 \brief Converts a QQmlJS::SourceLocation to a LSP Range.
169
170 QQmlJS::SourceLocation starts counting lines and rows at 1, but the LSP Range starts at 0.
171 */
172QLspSpecification::Range qmlLocationToLspLocation(Location qmlLocation)
173{
174 QLspSpecification::Range range;
175
176 range.start.line = qmlLocation.sourceLocation().startLine - 1;
177 range.start.character = qmlLocation.sourceLocation().startColumn - 1;
178 range.end.line = qmlLocation.end().line;
179 range.end.character = qmlLocation.end().character;
180
181 return range;
182}
183
184/*!
185 \internal
186 \brief Convert a text position from (line, column) into an offset.
187
188 Row, Column and the offset are all 0-based.
189 For example, \c{s[textOffsetFrom(s, 5, 55)]} returns the character of s at line 5 and column 55.
190
191 \sa QQmlLSUtils::textRowAndColumnFrom
192*/
193qsizetype textOffsetFrom(const QString &text, int row, int column)
194{
195 return QQmlJS::SourceLocation::offsetFrom(text, line: row + 1, column: column + 1);
196}
197
198/*!
199 \internal
200 \brief Convert a text position from an offset into (line, column).
201
202 Row, Column and the offset are all 0-based.
203 For example, \c{textRowAndColumnFrom(s, 55)} returns the line and columns of the
204 character at \c {s[55]}.
205
206 \sa QQmlLSUtils::textOffsetFrom
207*/
208TextPosition textRowAndColumnFrom(const QString &text, qsizetype offset)
209{
210 auto [row, column] = QQmlJS::SourceLocation::rowAndColumnFrom(text, offset);
211
212 // special case: return last character when accessing after end of file
213 if (offset >= text.size())
214 --column;
215
216 return TextPosition{ .line: int(row - 1), .character: int(column - 1) };
217}
218
219static QList<ItemLocation>::const_iterator
220handlePropertyDefinitionAndBindingOverlap(const QList<ItemLocation> &items, qsizetype offsetInFile)
221{
222 auto smallest = std::min_element(
223 first: items.begin(), last: items.end(), comp: [](const ItemLocation &a, const ItemLocation &b) {
224 return a.fileLocation->info().fullRegion.length
225 < b.fileLocation->info().fullRegion.length;
226 });
227
228 if (smallest->domItem.internalKind() == DomType::Binding) {
229 // weird edge case: the filelocations of property definitions and property bindings are
230 // actually overlapping, which means that qmlls cannot distinguish between bindings and
231 // bindings in property definitions. Those need to be treated differently for
232 // autocompletion, for example.
233 // Therefore: when inside a binding and a propertydefinition, choose the property definition
234 // if offsetInFile is before the colon, like for example:
235 // property var helloProperty: Rectangle { /*...*/ }
236 // |----return propertydef---|-- return Binding ---|
237
238 // get the smallest property definition to avoid getting the property definition that the
239 // current QmlObject is getting bound to!
240 auto smallestPropertyDefinition = std::min_element(
241 first: items.begin(), last: items.end(), comp: [](const ItemLocation &a, const ItemLocation &b) {
242 // make property definition smaller to avoid getting smaller items that are not
243 // property definitions
244 const bool aIsPropertyDefinition =
245 a.domItem.internalKind() == DomType::PropertyDefinition;
246 const bool bIsPropertyDefinition =
247 b.domItem.internalKind() == DomType::PropertyDefinition;
248 return aIsPropertyDefinition > bIsPropertyDefinition
249 && a.fileLocation->info().fullRegion.length
250 < b.fileLocation->info().fullRegion.length;
251 });
252
253 if (smallestPropertyDefinition->domItem.internalKind() != DomType::PropertyDefinition)
254 return smallest;
255
256 const auto propertyDefinitionColon =
257 smallestPropertyDefinition->fileLocation->info().regions[ColonTokenRegion];
258 const auto smallestColon = smallest->fileLocation->info().regions[ColonTokenRegion];
259 // sanity check: is it the definition of the current binding? check if they both have their
260 // ':' at the same location
261 if (propertyDefinitionColon.isValid() && propertyDefinitionColon == smallestColon
262 && offsetInFile < smallestColon.offset) {
263 return smallestPropertyDefinition;
264 }
265 }
266 return smallest;
267}
268
269static QList<ItemLocation> filterItemsFromTextLocation(const QList<ItemLocation> &items,
270 qsizetype offsetInFile)
271{
272 if (items.size() < 2)
273 return items;
274
275 // if there are multiple items, take the smallest one + its neighbors
276 // this allows to prefer inline components over main components, when both contain the
277 // current textposition, and to disregard internal structures like property maps, which
278 // "contain" everything from their first-appearing to last-appearing property (e.g. also
279 // other stuff in between those two properties).
280
281 QList<ItemLocation> filteredItems;
282
283 auto smallest = handlePropertyDefinitionAndBindingOverlap(items, offsetInFile);
284
285 filteredItems.append(t: *smallest);
286
287 const QQmlJS::SourceLocation smallestLoc = smallest->fileLocation->info().fullRegion;
288 const quint32 smallestBegin = smallestLoc.begin();
289 const quint32 smallestEnd = smallestLoc.end();
290
291 for (auto it = items.begin(); it != items.end(); it++) {
292 if (it == smallest)
293 continue;
294
295 const QQmlJS::SourceLocation itLoc = it->fileLocation->info().fullRegion;
296 const quint32 itBegin = itLoc.begin();
297 const quint32 itEnd = itLoc.end();
298 if (itBegin == smallestEnd || smallestBegin == itEnd) {
299 filteredItems.append(t: *it);
300 }
301 }
302 return filteredItems;
303}
304
305/*!
306\internal
307\brief Find the DomItem representing the object situated in file at given line and
308character/column.
309
310If line and character point between two objects, two objects might be returned.
311If line and character point to whitespace, it might return an inner node of the QmlDom-Tree.
312
313We usually assume that sourcelocations have inclusive ends, for example
314we assume that auto-completion on `\n` in `someName\n` wants suggestions
315for `someName`, even if its technically one position "outside" the
316sourcelocation of `someName`. This is not true for
317ScriptBinaryExpressions, where auto-completion on `.` in `someName.` should
318not return suggestions for `someName`.
319The same also applies to all other binary expressions `+`, `-`, and so on.
320 */
321QList<ItemLocation> itemsFromTextLocation(const DomItem &file, int line, int character)
322{
323 QList<ItemLocation> itemsFound;
324 std::shared_ptr<QmlFile> filePtr = file.ownerAs<QmlFile>();
325 if (!filePtr)
326 return itemsFound;
327 FileLocations::Tree t = filePtr->fileLocationsTree();
328 Q_ASSERT(t);
329 QString code = filePtr->code(); // do something more advanced wrt to changes wrt to this->code?
330 QList<ItemLocation> toDo;
331 qsizetype targetPos = textOffsetFrom(text: code, row: line, column: character);
332 Q_ASSERT(targetPos >= 0);
333
334 enum ComparisonOption { Normal, ExcludePositionAfterLast };
335 auto containsTarget = [targetPos](QQmlJS::SourceLocation l, ComparisonOption c) {
336 if constexpr (sizeof(qsizetype) <= sizeof(quint32)) {
337 return l.begin() <= quint32(targetPos) && quint32(targetPos) < l.end() + (c == Normal ? 1 : 0) ;
338 } else {
339 return l.begin() <= targetPos && targetPos < l.end() + (c == Normal ? 1 : 0);
340 }
341 };
342 if (containsTarget(t->info().fullRegion, Normal)) {
343 ItemLocation loc;
344 loc.domItem = file;
345 loc.fileLocation = t;
346 toDo.append(t: loc);
347 }
348 while (!toDo.isEmpty()) {
349 ItemLocation iLoc = toDo.last();
350 toDo.removeLast();
351
352 bool inParentButOutsideChildren = true;
353
354 // Exclude the position behind the source location in ScriptBinaryExpressions to avoid
355 // returning `owner` in `owner.member` when completion is triggered on the \c{.}. This
356 // tells the code for the completion if the completion was triggered on `owner` or on `.`.
357 // Same is true for templateliterals, where ScriptTemplateExpressionParts and
358 // ScriptTemplateStringParts stop overlapping when using ExcludePositionAfterLast.
359 const ComparisonOption comparisonOption =
360 iLoc.domItem.internalKind() == DomType::ScriptBinaryExpression
361 || iLoc.domItem.directParent().internalKind()
362 == DomType::ScriptTemplateLiteral
363 ? ExcludePositionAfterLast
364 : Normal;
365
366 auto subEls = iLoc.fileLocation->subItems();
367 for (auto it = subEls.begin(); it != subEls.end(); ++it) {
368 auto subLoc = std::static_pointer_cast<AttachedInfoT<FileLocations>>(r: it.value());
369 Q_ASSERT(subLoc);
370
371 if (containsTarget(subLoc->info().fullRegion, comparisonOption)) {
372 ItemLocation subItem;
373 subItem.domItem = iLoc.domItem.path(p: it.key());
374 if (!subItem.domItem) {
375 qCDebug(QQmlLSUtilsLog)
376 << "A DomItem child is missing or the FileLocationsTree structure does "
377 "not follow the DomItem Structure.";
378 continue;
379 }
380 // the parser inserts empty Script Expressions for bindings that are not completely
381 // written out yet. Ignore them here.
382 if (subItem.domItem.internalKind() == DomType::ScriptExpression
383 && subLoc->info().fullRegion.length == 0) {
384 continue;
385 }
386 subItem.fileLocation = subLoc;
387 toDo.append(t: subItem);
388 inParentButOutsideChildren = false;
389 }
390 }
391 if (inParentButOutsideChildren) {
392 itemsFound.append(t: iLoc);
393 }
394 }
395
396 // filtering step:
397 auto filtered = filterItemsFromTextLocation(items: itemsFound, offsetInFile: targetPos);
398 return filtered;
399}
400
401DomItem baseObject(const DomItem &object)
402{
403 DomItem prototypes;
404 DomItem qmlObject = object.qmlObject();
405 // object is (or is inside) an inline component definition
406 if (object.internalKind() == DomType::QmlComponent || !qmlObject) {
407 prototypes = object.component()
408 .field(name: Fields::objects)
409 .index(0)
410 .field(name: QQmlJS::Dom::Fields::prototypes);
411 } else {
412 // object is (or is inside) a QmlObject
413 prototypes = qmlObject.field(name: QQmlJS::Dom::Fields::prototypes);
414 }
415 switch (prototypes.indexes()) {
416 case 0:
417 return {};
418 case 1:
419 break;
420 default:
421 qDebug() << "Multiple prototypes found for " << object.name() << ", taking the first one.";
422 break;
423 }
424 QQmlJS::Dom::DomItem base = prototypes.index(0).proceedToScope();
425 return base;
426}
427
428static std::optional<Location> locationFromDomItem(const DomItem &item, FileLocationRegion region)
429{
430 auto tree = FileLocations::treeOf(item);
431 // tree is null for C++ defined types, for example
432 if (!tree)
433 return {};
434
435 QQmlJS::SourceLocation sourceLocation = FileLocations::region(fLoc: tree, region);
436 if (!sourceLocation.isValid() && region != QQmlJS::Dom::MainRegion)
437 sourceLocation = FileLocations::region(fLoc: tree, region: QQmlJS::Dom::MainRegion);
438
439 return Location::tryFrom(fileName: item.canonicalFilePath(), sourceLocation, someItem: item);
440}
441
442/*!
443 \internal
444 \brief Returns the location of the type definition pointed by object.
445
446 For a \c PropertyDefinition, return the location of the type of the property.
447 For a \c Binding, return the bound item's type location if an QmlObject is bound, and otherwise
448 the type of the property.
449 For a \c QmlObject, return the location of the QmlObject's base.
450 For an \c Id, return the location of the object to which the id resolves.
451 For a \c Methodparameter, return the location of the type of the parameter.
452 Otherwise, return std::nullopt.
453 */
454std::optional<Location> findTypeDefinitionOf(const DomItem &object)
455{
456 DomItem typeDefinition;
457
458 switch (object.internalKind()) {
459 case QQmlJS::Dom::DomType::QmlComponent:
460 typeDefinition = object.field(name: Fields::objects).index(0);
461 break;
462 case QQmlJS::Dom::DomType::QmlObject:
463 typeDefinition = baseObject(object);
464 break;
465 case QQmlJS::Dom::DomType::Binding: {
466 auto binding = object.as<Binding>();
467 Q_ASSERT(binding);
468
469 // try to grab the type from the bound object
470 if (binding->valueKind() == BindingValueKind::Object) {
471 typeDefinition = baseObject(object: object.field(name: Fields::value));
472 break;
473 } else {
474 // use the type of the property it is bound on for scriptexpression etc.
475 DomItem propertyDefinition;
476 const QString bindingName = binding->name();
477 object.containingObject().visitLookup(
478 symbolName: bindingName,
479 visitor: [&propertyDefinition](const DomItem &item) {
480 if (item.internalKind() == QQmlJS::Dom::DomType::PropertyDefinition) {
481 propertyDefinition = item;
482 return false;
483 }
484 return true;
485 },
486 type: LookupType::PropertyDef);
487 typeDefinition = propertyDefinition.field(name: Fields::type).proceedToScope();
488 break;
489 }
490 Q_UNREACHABLE();
491 }
492 case QQmlJS::Dom::DomType::Id:
493 typeDefinition = object.field(name: Fields::referredObject).proceedToScope();
494 break;
495 case QQmlJS::Dom::DomType::PropertyDefinition:
496 case QQmlJS::Dom::DomType::MethodParameter:
497 case QQmlJS::Dom::DomType::MethodInfo:
498 typeDefinition = object.field(name: Fields::type).proceedToScope();
499 break;
500 case QQmlJS::Dom::DomType::ScriptIdentifierExpression: {
501 if (DomItem type = object.filterUp(
502 filter: [](DomType k, const DomItem &) { return k == DomType::ScriptType; },
503 options: FilterUpOptions::ReturnOuter)) {
504
505 const QString name = fieldMemberExpressionBits(item: type.field(name: Fields::typeName)).join(sep: u'.');
506 switch (type.directParent().internalKind()) {
507 case DomType::QmlObject:
508 // is the type name of a QmlObject, like Item in `Item {...}`
509 typeDefinition = baseObject(object: type.directParent());
510 break;
511 case DomType::QmlComponent:
512 typeDefinition = type.directParent();
513 return locationFromDomItem(item: typeDefinition, region: FileLocationRegion::IdentifierRegion);
514 break;
515 default:
516 // is a type annotation, like Item in `function f(x: Item) { ... }`
517 typeDefinition = object.path(p: Paths::lookupTypePath(name));
518 if (typeDefinition.internalKind() == DomType::Export) {
519 typeDefinition = typeDefinition.field(name: Fields::type).get();
520 }
521 }
522 break;
523 }
524 if (DomItem id = object.filterUp(
525 filter: [](DomType k, const DomItem &) { return k == DomType::Id; },
526 options: FilterUpOptions::ReturnOuter)) {
527
528 typeDefinition = id.field(name: Fields::referredObject).proceedToScope();
529 break;
530 }
531
532 auto scope = resolveExpressionType(
533 item: object, ResolveOptions::ResolveActualTypeForFieldMemberExpression);
534 if (!scope || !scope->semanticScope)
535 return {};
536
537 if (scope->type == QmlObjectIdIdentifier) {
538 return Location::tryFrom(fileName: scope->semanticScope->filePath(),
539 sourceLocation: scope->semanticScope->sourceLocation(), someItem: object);
540 }
541
542 typeDefinition = sourceLocationToDomItem(file: object.containingFile(),
543 location: scope->semanticScope->sourceLocation());
544 return locationFromDomItem(item: typeDefinition.component(),
545 region: FileLocationRegion::IdentifierRegion);
546 }
547 default:
548 qDebug() << "QQmlLSUtils::findTypeDefinitionOf: Found unimplemented Type"
549 << object.internalKindStr();
550 return {};
551 }
552
553 return locationFromDomItem(item: typeDefinition, region: FileLocationRegion::MainRegion);
554}
555
556static bool findDefinitionFromItem(const DomItem &item, const QString &name)
557{
558 if (const QQmlJSScope::ConstPtr &scope = item.semanticScope()) {
559 qCDebug(QQmlLSUtilsLog) << "Searching for definition in" << item.internalKindStr();
560 if (auto jsIdentifier = scope->ownJSIdentifier(id: name)) {
561 qCDebug(QQmlLSUtilsLog) << "Found scope" << scope->baseTypeName();
562 return true;
563 }
564 }
565 return false;
566}
567
568static DomItem findJSIdentifierDefinition(const DomItem &item, const QString &name)
569{
570 DomItem definitionOfItem;
571 item.visitUp(visitor: [&name, &definitionOfItem](const DomItem &i) {
572 if (findDefinitionFromItem(item: i, name)) {
573 definitionOfItem = i;
574 return false;
575 }
576 // early exit: no JS definitions/usages outside the ScriptExpression DOM element.
577 if (i.internalKind() == DomType::ScriptExpression)
578 return false;
579 return true;
580 });
581
582 if (definitionOfItem)
583 return definitionOfItem;
584
585 // special case: somebody asks for usages of a function parameter from its definition
586 // function parameters are defined in the method's scope
587 if (DomItem res = item.filterUp(filter: [](DomType k, const DomItem &) { return k == DomType::MethodInfo; },
588 options: FilterUpOptions::ReturnOuter)) {
589 DomItem candidate = res.field(name: Fields::body).field(name: Fields::scriptElement);
590 if (findDefinitionFromItem(item: candidate, name)) {
591 return candidate;
592 }
593 }
594
595 // lambda function parameters are defined in the FunctionExpression scope
596 if (DomItem res = item.filterUp(
597 filter: [](DomType k, const DomItem &) { return k == DomType::ScriptFunctionExpression; },
598 options: FilterUpOptions::ReturnOuter)) {
599 if (findDefinitionFromItem(item: res, name)) {
600 return res;
601 }
602 }
603
604 return definitionOfItem;
605}
606
607/*!
608\internal
609Represents a signal, signal handler, property, property changed signal or a property changed
610handler.
611 */
612struct SignalOrProperty
613{
614 /*!
615 \internal The name of the signal or property, independent of whether this is a changed signal
616 or handler.
617 */
618 QString name;
619 IdentifierType type;
620};
621
622/*!
623\internal
624\brief Find out if \c{name} is a signal, signal handler, property, property changed signal, or a
625property changed handler in the given QQmlJSScope.
626
627Heuristic to find if name is a property, property changed signal, .... because those can appear
628under different names, for example \c{mySignal} and \c{onMySignal} for a signal.
629This will give incorrect results as soon as properties/signals/methods are called \c{onMySignal},
630\c{on<some already existing property>Changed}, ..., but the good news is that the engine also
631will act weird in these cases (e.g. one cannot bind to a property called like an already existing
632signal or a property changed handler).
633For future reference: you can always add additional checks to check the existence of those buggy
634properties/signals/methods by looking if they exist in the QQmlJSScope.
635*/
636static std::optional<SignalOrProperty> resolveNameInQmlScope(const QString &name,
637 const QQmlJSScope::ConstPtr &owner)
638{
639 if (owner->hasProperty(name)) {
640 return SignalOrProperty{ .name: name, .type: PropertyIdentifier };
641 }
642
643 if (const auto propertyName = QQmlSignalNames::changedHandlerNameToPropertyName(handler: name)) {
644 if (owner->hasProperty(name: *propertyName)) {
645 const QString signalName = *QQmlSignalNames::changedHandlerNameToSignalName(changedHandler: name);
646 const QQmlJSMetaMethod signal = owner->methods(name: signalName).front();
647 // PropertyChangedHandlers don't have parameters: treat all other as regular signal
648 // handlers. Even if they appear in the notify of the property.
649 if (signal.parameterNames().size() == 0)
650 return SignalOrProperty{ .name: *propertyName, .type: PropertyChangedHandlerIdentifier };
651 else
652 return SignalOrProperty{ .name: signalName, .type: SignalHandlerIdentifier };
653 }
654 }
655
656 if (const auto signalName = QQmlSignalNames::handlerNameToSignalName(handler: name)) {
657 if (auto methods = owner->methods(name: *signalName); !methods.isEmpty()) {
658 if (methods.front().methodType() == QQmlJSMetaMethodType::Signal) {
659 return SignalOrProperty{ .name: *signalName, .type: SignalHandlerIdentifier };
660 }
661 }
662 }
663
664 if (const auto propertyName = QQmlSignalNames::changedSignalNameToPropertyName(changeSignal: name)) {
665 if (owner->hasProperty(name: *propertyName)) {
666 return SignalOrProperty{ .name: *propertyName, .type: PropertyChangedSignalIdentifier };
667 }
668 }
669
670 if (auto methods = owner->methods(name); !methods.isEmpty()) {
671 if (methods.front().methodType() == QQmlJSMetaMethodType::Signal) {
672 return SignalOrProperty{ .name: name, .type: SignalIdentifier };
673 }
674 return SignalOrProperty{ .name: name, .type: MethodIdentifier };
675 }
676 return std::nullopt;
677}
678
679/*!
680\internal
681Returns a list of names, that when belonging to the same targetType, should be considered equal.
682This is used to find signal handlers as usages of their corresponding signals, for example.
683*/
684static QStringList namesOfPossibleUsages(const QString &name,
685 const DomItem &item,
686 const QQmlJSScope::ConstPtr &targetType)
687{
688 QStringList namesToCheck = { name };
689 if (item.internalKind() == DomType::EnumItem || item.internalKind() == DomType::EnumDecl)
690 return namesToCheck;
691
692 auto namings = resolveNameInQmlScope(name, owner: targetType);
693 if (!namings)
694 return namesToCheck;
695 switch (namings->type) {
696 case PropertyIdentifier: {
697 // for a property, also find bindings to its onPropertyChanged handler + propertyChanged
698 // signal
699 const QString propertyChangedHandler =
700 QQmlSignalNames::propertyNameToChangedHandlerName(property: namings->name);
701 namesToCheck.append(t: propertyChangedHandler);
702
703 const QString propertyChangedSignal =
704 QQmlSignalNames::propertyNameToChangedSignalName(property: namings->name);
705 namesToCheck.append(t: propertyChangedSignal);
706 break;
707 }
708 case PropertyChangedHandlerIdentifier: {
709 // for a property changed handler, also find the usages of its property + propertyChanged
710 // signal
711 namesToCheck.append(t: namings->name);
712 namesToCheck.append(t: QQmlSignalNames::propertyNameToChangedSignalName(property: namings->name));
713 break;
714 }
715 case PropertyChangedSignalIdentifier: {
716 // for a property changed signal, also find the usages of its property + onPropertyChanged
717 // handlers
718 namesToCheck.append(t: namings->name);
719 namesToCheck.append(t: QQmlSignalNames::propertyNameToChangedHandlerName(property: namings->name));
720 break;
721 }
722 case SignalIdentifier: {
723 // for a signal, also find bindings to its onSignalHandler.
724 namesToCheck.append(t: QQmlSignalNames::signalNameToHandlerName(signal: namings->name));
725 break;
726 }
727 case SignalHandlerIdentifier: {
728 // for a signal handler, also find the usages of the signal it handles
729 namesToCheck.append(t: namings->name);
730 break;
731 }
732 default: {
733 break;
734 }
735 }
736 return namesToCheck;
737}
738
739template<typename Predicate>
740QQmlJSScope::ConstPtr findDefiningScopeIf(QQmlJSScope::ConstPtr referrerScope, Predicate &&check)
741{
742 QQmlJSScope::ConstPtr result;
743 QQmlJSUtils::searchBaseAndExtensionTypes(referrerScope, [&](QQmlJSScope::ConstPtr scope) {
744 if (check(scope)) {
745 result = scope;
746 return true;
747 }
748 return false;
749 });
750
751 return result;
752}
753
754/*!
755\internal
756\brief Finds the scope where a property is first defined.
757
758Starts looking for the name starting from the given scope and traverse through base and
759extension types.
760*/
761QQmlJSScope::ConstPtr findDefiningScopeForProperty(const QQmlJSScope::ConstPtr &referrerScope,
762 const QString &nameToCheck)
763{
764 return findDefiningScopeIf(referrerScope, check: [&nameToCheck](const QQmlJSScope::ConstPtr &scope) {
765 return scope->hasOwnProperty(name: nameToCheck);
766 });
767}
768
769/*!
770\internal
771See also findDefiningScopeForProperty().
772
773Special case: you can also bind to a signal handler.
774*/
775QQmlJSScope::ConstPtr findDefiningScopeForBinding(const QQmlJSScope::ConstPtr &referrerScope,
776 const QString &nameToCheck)
777{
778 return findDefiningScopeIf(referrerScope, check: [&nameToCheck](const QQmlJSScope::ConstPtr &scope) {
779 return scope->hasOwnProperty(name: nameToCheck) || scope->hasOwnMethod(name: nameToCheck);
780 });
781}
782
783/*!
784\internal
785See also findDefiningScopeForProperty().
786*/
787QQmlJSScope::ConstPtr findDefiningScopeForMethod(const QQmlJSScope::ConstPtr &referrerScope,
788 const QString &nameToCheck)
789{
790 return findDefiningScopeIf(referrerScope, check: [&nameToCheck](const QQmlJSScope::ConstPtr &scope) {
791 return scope->hasOwnMethod(name: nameToCheck);
792 });
793}
794
795/*!
796\internal
797See also findDefiningScopeForProperty().
798*/
799QQmlJSScope::ConstPtr findDefiningScopeForEnumeration(const QQmlJSScope::ConstPtr &referrerScope,
800 const QString &nameToCheck)
801{
802 return findDefiningScopeIf(referrerScope, check: [&nameToCheck](const QQmlJSScope::ConstPtr &scope) {
803 return scope->hasOwnEnumeration(name: nameToCheck);
804 });
805}
806
807/*!
808\internal
809See also findDefiningScopeForProperty().
810*/
811QQmlJSScope::ConstPtr findDefiningScopeForEnumerationKey(const QQmlJSScope::ConstPtr &referrerScope,
812 const QString &nameToCheck)
813{
814 return findDefiningScopeIf(referrerScope, check: [&nameToCheck](const QQmlJSScope::ConstPtr &scope) {
815 return scope->hasOwnEnumerationKey(name: nameToCheck);
816 });
817}
818
819/*!
820 Filter away the parts of the Dom not needed for find usages, by following the profiler's
821 information.
822 1. "propertyInfos" tries to require all inherited properties of some QmlObject. That is super
823 slow (profiler says it eats 90% of the time needed by `tst_qmlls_utils findUsages`!) and is not
824 needed for usages.
825 2. "get" tries to resolve references, like base types saved in prototypes for example, and is not
826 needed to find usages. Profiler says it eats 70% of the time needed by `tst_qmlls_utils
827 findUsages`.
828 3. "defaultPropertyName" also recurses through base types and is not needed to find usages.
829*/
830static FieldFilter filterForFindUsages()
831{
832 FieldFilter filter{ {},
833 {
834 { QString(), QString::fromUtf16(Fields::propertyInfos) },
835 { QString(), QString::fromUtf16(Fields::defaultPropertyName) },
836 { QString(), QString::fromUtf16(Fields::get) },
837 } };
838 return filter;
839};
840
841static void findUsagesOfNonJSIdentifiers(const DomItem &item, const QString &name, Usages &result)
842{
843 const auto expressionType = resolveExpressionType(item, ResolveOwnerType);
844 if (!expressionType)
845 return;
846
847 // for Qml file components: add their filename as an usage for the renaming operation
848 if (expressionType->type == QmlComponentIdentifier
849 && !expressionType->semanticScope->isInlineComponent()) {
850 result.appendFilenameUsage(edit: expressionType->semanticScope->filePath());
851 }
852
853 const QStringList namesToCheck = namesOfPossibleUsages(name, item, targetType: expressionType->semanticScope);
854
855 const auto addLocationIfTypeMatchesTarget =
856 [&result, &expressionType, &item](const DomItem &toBeResolved, FileLocationRegion subRegion) {
857 const auto currentType =
858 resolveExpressionType(item: toBeResolved, ResolveOptions::ResolveOwnerType);
859 if (!currentType)
860 return;
861
862 const QQmlJSScope::ConstPtr target = expressionType->semanticScope;
863 const QQmlJSScope::ConstPtr current = currentType->semanticScope;
864 if (target == current) {
865 auto tree = FileLocations::treeOf(toBeResolved);
866 QQmlJS::SourceLocation sourceLocation;
867
868 sourceLocation = FileLocations::region(fLoc: tree, region: subRegion);
869 if (!sourceLocation.isValid())
870 return;
871
872 if (auto location = Location::tryFrom(fileName: toBeResolved.canonicalFilePath(),
873 sourceLocation, someItem: item)) {
874 result.appendUsage(edit: *location);
875 }
876 }
877 };
878
879 auto findUsages = [&addLocationIfTypeMatchesTarget, &name,
880 &namesToCheck](Path, const DomItem &current, bool) -> bool {
881 bool continueForChildren = true;
882 if (auto scope = current.semanticScope()) {
883 // is the current property shadowed by some JS identifier? ignore current + its children
884 if (scope->ownJSIdentifier(id: name)) {
885 return false;
886 }
887 }
888 switch (current.internalKind()) {
889 case DomType::QmlObject:
890 case DomType::Binding:
891 case DomType::MethodInfo:
892 case DomType::PropertyDefinition: {
893 const QString propertyName = current.field(name: Fields::name).value().toString();
894 if (namesToCheck.contains(str: propertyName))
895 addLocationIfTypeMatchesTarget(current, IdentifierRegion);
896 return continueForChildren;
897 }
898 case DomType::ScriptIdentifierExpression: {
899 const QString identifierName = current.field(name: Fields::identifier).value().toString();
900 if (namesToCheck.contains(str: identifierName))
901 addLocationIfTypeMatchesTarget(current, MainRegion);
902 return continueForChildren;
903 }
904 case DomType::ScriptLiteral: {
905 const QString literal = current.field(name: Fields::value).value().toString();
906 if (namesToCheck.contains(str: literal))
907 addLocationIfTypeMatchesTarget(current, MainRegion);
908 return continueForChildren;
909 }
910 case DomType::EnumItem: {
911 // Only look for the first enum defined. The inner enums
912 // have no way to be accessed.
913 const auto parentPath = current.containingObject().pathFromOwner();
914 const auto index = parentPath.last().headIndex();
915 if (index != 0)
916 return continueForChildren;
917 const QString enumValue = current.field(name: Fields::name).value().toString();
918 if (namesToCheck.contains(str: enumValue))
919 addLocationIfTypeMatchesTarget(current, IdentifierRegion);
920 return continueForChildren;
921 }
922 case DomType::EnumDecl: {
923 // Only look for the first enum defined. The inner enums
924 // have no way to be accessed.
925 const auto parentPath = current.pathFromOwner();
926 const auto index = parentPath.last().headIndex();
927 if (index != 0)
928 return continueForChildren;
929 const QString enumValue = current.field(name: Fields::name).value().toString();
930 if (namesToCheck.contains(str: enumValue))
931 addLocationIfTypeMatchesTarget(current, IdentifierRegion);
932 return continueForChildren;
933 }
934 default:
935 return continueForChildren;
936 };
937
938 Q_UNREACHABLE_RETURN(continueForChildren);
939 };
940
941 const DomItem qmlFiles = item.top().field(name: Fields::qmlFileWithPath);
942 const auto filter = filterForFindUsages();
943 for (const QString &file : qmlFiles.keys()) {
944 const DomItem currentFileComponents =
945 qmlFiles.key(name: file).field(name: Fields::currentItem).field(name: Fields::components);
946 currentFileComponents.visitTree(basePath: Path(), visitor: emptyChildrenVisitor,
947 options: VisitOption::Recurse | VisitOption::VisitSelf, openingVisitor: findUsages,
948 closingVisitor: emptyChildrenVisitor, filter);
949 }
950}
951
952static std::optional<Location> locationFromJSIdentifierDefinition(const DomItem &definitionOfItem,
953 const QString &name)
954{
955 Q_ASSERT_X(!definitionOfItem.semanticScope().isNull()
956 && definitionOfItem.semanticScope()->ownJSIdentifier(name).has_value(),
957 "QQmlLSUtils::locationFromJSIdentifierDefinition",
958 "JS definition does not actually define the JS identifier. "
959 "Did you obtain definitionOfItem from findJSIdentifierDefinition() ?");
960 const QQmlJS::SourceLocation location =
961 definitionOfItem.semanticScope()->ownJSIdentifier(id: name).value().location;
962
963 return Location::tryFrom(fileName: definitionOfItem.canonicalFilePath(), sourceLocation: location, someItem: definitionOfItem);
964}
965
966static void findUsagesHelper(const DomItem &item, const QString &name, Usages &result)
967{
968 qCDebug(QQmlLSUtilsLog) << "Looking for JS identifier with name" << name;
969 DomItem definitionOfItem = findJSIdentifierDefinition(item, name);
970
971 // if there is no definition found: check if name was a property or an id instead
972 if (!definitionOfItem) {
973 qCDebug(QQmlLSUtilsLog) << "No defining JS-Scope found!";
974 findUsagesOfNonJSIdentifiers(item, name, result);
975 return;
976 }
977
978 definitionOfItem.visitTree(
979 basePath: Path(), visitor: emptyChildrenVisitor, options: VisitOption::VisitAdopted | VisitOption::Recurse,
980 openingVisitor: [&name, &result](Path, const DomItem &item, bool) -> bool {
981 qCDebug(QQmlLSUtilsLog) << "Visiting a " << item.internalKindStr();
982 if (item.internalKind() == DomType::ScriptIdentifierExpression
983 && item.field(name: Fields::identifier).value().toString() == name) {
984 // add this usage
985 auto fileLocation = FileLocations::treeOf(item);
986 if (!fileLocation) {
987 qCWarning(QQmlLSUtilsLog) << "Failed finding filelocation of found usage";
988 return true;
989 }
990 const QQmlJS::SourceLocation sourceLocation = fileLocation->info().fullRegion;
991 const QString fileName = item.canonicalFilePath();
992 if (auto location = Location::tryFrom(fileName, sourceLocation, someItem: item))
993 result.appendUsage(edit: *location);
994 return true;
995 }
996 QQmlJSScope::ConstPtr scope = item.semanticScope();
997 // Do not visit children if current JS identifier has been redefined.
998 if (scope && scope != item.directParent().semanticScope()
999 && scope->ownJSIdentifier(id: name)) {
1000 // Note that if the current semantic scope == parent's semantic scope, then no
1001 // redefinition actually took place. This happens for example in
1002 // FunctionExpressions, where the body's semantic scope is the
1003 // FunctionExpression's semantic scope.
1004 return false;
1005 }
1006 return true;
1007 },
1008 closingVisitor: emptyChildrenVisitor, filter: filterForFindUsages());
1009
1010 if (const auto definition = locationFromJSIdentifierDefinition(definitionOfItem, name))
1011 result.appendUsage(edit: *definition);
1012}
1013
1014Usages findUsagesOf(const DomItem &item)
1015{
1016 Usages result;
1017
1018 switch (item.internalKind()) {
1019 case DomType::ScriptIdentifierExpression: {
1020 const QString name = item.field(name: Fields::identifier).value().toString();
1021 findUsagesHelper(item, name, result);
1022 break;
1023 }
1024 case DomType::ScriptVariableDeclarationEntry: {
1025 const QString name = item.field(name: Fields::identifier).value().toString();
1026 findUsagesHelper(item, name, result);
1027 break;
1028 }
1029 case DomType::EnumDecl:
1030 case DomType::EnumItem:
1031 case DomType::QmlObject:
1032 case DomType::PropertyDefinition:
1033 case DomType::Binding:
1034 case DomType::MethodInfo: {
1035 const QString name = item.field(name: Fields::name).value().toString();
1036 findUsagesHelper(item, name, result);
1037 break;
1038 }
1039 case DomType::QmlComponent: {
1040 QString name = item.field(name: Fields::name).value().toString();
1041
1042 // get rid of extra qualifiers
1043 if (const auto dotIndex = name.indexOf(ch: u'.'); dotIndex != -1)
1044 name = name.sliced(pos: dotIndex + 1);
1045 findUsagesHelper(item, name, result);
1046 break;
1047 }
1048 default:
1049 qCDebug(QQmlLSUtilsLog) << item.internalKindStr()
1050 << "was not implemented for QQmlLSUtils::findUsagesOf";
1051 return result;
1052 }
1053
1054 result.sort();
1055
1056 if (QQmlLSUtilsLog().isDebugEnabled()) {
1057 qCDebug(QQmlLSUtilsLog) << "Found following usages in files:";
1058 for (auto r : result.usagesInFile()) {
1059 qCDebug(QQmlLSUtilsLog) << r.filename() << " @ " << r.sourceLocation().startLine << ":"
1060 << r.sourceLocation().startColumn << " with length "
1061 << r.sourceLocation().length;
1062 }
1063 qCDebug(QQmlLSUtilsLog) << "And following usages in file names:"
1064 << result.usagesInFilename();
1065 }
1066
1067 return result;
1068}
1069
1070static std::optional<IdentifierType> hasMethodOrSignal(const QQmlJSScope::ConstPtr &scope,
1071 const QString &name)
1072{
1073 auto methods = scope->methods(name);
1074 if (methods.isEmpty())
1075 return {};
1076
1077 const bool isSignal = methods.front().methodType() == QQmlJSMetaMethodType::Signal;
1078 IdentifierType type =
1079 isSignal ? IdentifierType::SignalIdentifier : IdentifierType::MethodIdentifier;
1080 return type;
1081}
1082
1083/*!
1084\internal
1085Searches for a method by traversing the parent scopes.
1086
1087We assume here that it is possible to call methods from parent scope to simplify things, as the
1088linting module already warns about calling methods from parent scopes.
1089
1090Note: in QML, one can only call methods from the current scope, and from the QML file root scope.
1091Everything else needs a qualifier.
1092*/
1093static std::optional<ExpressionType>
1094methodFromReferrerScope(const QQmlJSScope::ConstPtr &referrerScope, const QString &name,
1095 ResolveOptions options)
1096{
1097 for (QQmlJSScope::ConstPtr current = referrerScope; current; current = current->parentScope()) {
1098 if (auto type = hasMethodOrSignal(scope: current, name)) {
1099 switch (options) {
1100 case ResolveOwnerType:
1101 return ExpressionType{ .name: name, .semanticScope: findDefiningScopeForMethod(referrerScope: current, nameToCheck: name), .type: *type };
1102 case ResolveActualTypeForFieldMemberExpression:
1103 // QQmlJSScopes were not implemented for methods yet, but JS functions have methods
1104 // and properties see
1105 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function
1106 // for the list of properties/methods of functions. Therefore return a null scope.
1107 // see also code below for non-qualified method access
1108 return ExpressionType{ .name: name, .semanticScope: {}, .type: *type };
1109 }
1110 }
1111
1112 if (const auto signalName = QQmlSignalNames::handlerNameToSignalName(handler: name)) {
1113 if (auto type = hasMethodOrSignal(scope: current, name: *signalName)) {
1114 switch (options) {
1115 case ResolveOwnerType:
1116 return ExpressionType{ .name: name, .semanticScope: findDefiningScopeForMethod(referrerScope: current, nameToCheck: *signalName),
1117 .type: SignalHandlerIdentifier };
1118 case ResolveActualTypeForFieldMemberExpression:
1119 // Properties and methods of JS methods are not supported yet
1120 return ExpressionType{ .name: name, .semanticScope: {}, .type: SignalHandlerIdentifier };
1121 }
1122 }
1123 }
1124 }
1125 return {};
1126}
1127
1128
1129/*!
1130\internal
1131See comment on methodFromReferrerScope: the same applies to properties.
1132*/
1133static std::optional<ExpressionType>
1134propertyFromReferrerScope(const QQmlJSScope::ConstPtr &referrerScope, const QString &propertyName,
1135 ResolveOptions options)
1136{
1137 for (QQmlJSScope::ConstPtr current = referrerScope; current; current = current->parentScope()) {
1138 const auto resolved = resolveNameInQmlScope(name: propertyName, owner: current);
1139 if (!resolved)
1140 continue;
1141
1142 if (auto property = current->property(name: resolved->name); property.isValid()) {
1143 switch (options) {
1144 case ResolveOwnerType:
1145 return ExpressionType{ .name: propertyName,
1146 .semanticScope: findDefiningScopeForProperty(referrerScope: current, nameToCheck: propertyName),
1147 .type: resolved->type };
1148 case ResolveActualTypeForFieldMemberExpression:
1149 return ExpressionType{ .name: propertyName, .semanticScope: property.type(), .type: resolved->type };
1150 }
1151 }
1152 }
1153 return {};
1154}
1155
1156/*!
1157\internal
1158See comment on methodFromReferrerScope: the same applies to property bindings.
1159
1160If resolver is not null then it is used to resolve the id with which a generalized grouped
1161properties starts.
1162*/
1163static std::optional<ExpressionType>
1164propertyBindingFromReferrerScope(const QQmlJSScope::ConstPtr &referrerScope, const QString &name,
1165 ResolveOptions options, QQmlJSTypeResolver *resolverForIds)
1166{
1167 auto bindings = referrerScope->propertyBindings(name);
1168 if (bindings.isEmpty())
1169 return {};
1170
1171 const auto binding = bindings.front();
1172
1173 if ((binding.bindingType() != QQmlSA::BindingType::AttachedProperty)
1174 && (binding.bindingType() != QQmlSA::BindingType::GroupProperty))
1175 return {};
1176
1177 const bool bindingIsAttached = binding.bindingType() == QQmlSA::BindingType::AttachedProperty;
1178
1179 // Generalized grouped properties, like Bindings or PropertyChanges, for example, have bindings
1180 // starting in an id (like `someId.someProperty: ...`).
1181 // If `someid` is not a property and is a deferred name, then it should be an id.
1182 if (!bindingIsAttached && !referrerScope->hasProperty(name)
1183 && referrerScope->isNameDeferred(name)) {
1184 if (!resolverForIds)
1185 return {};
1186
1187 QQmlJSRegisterContent fromId = resolverForIds->scopedType(
1188 scope: referrerScope, name, lookupIndex: QQmlJSRegisterContent::InvalidLookupIndex,
1189 options: AssumeComponentsAreBound);
1190 if (fromId.variant() == QQmlJSRegisterContent::ObjectById)
1191 return ExpressionType{ .name: name, .semanticScope: fromId.type(), .type: QmlObjectIdIdentifier };
1192
1193 return ExpressionType{ .name: name, .semanticScope: {}, .type: QmlObjectIdIdentifier };
1194 }
1195
1196 const auto typeIdentifier =
1197 bindingIsAttached ? AttachedTypeIdentifier : GroupedPropertyIdentifier;
1198
1199 const auto getScope = [&bindingIsAttached, &binding]() -> QQmlJSScope::ConstPtr {
1200 if (bindingIsAttached)
1201 return binding.attachingType();
1202
1203 return binding.groupType();
1204 };
1205
1206 switch (options) {
1207 case ResolveOwnerType: {
1208 return ExpressionType{ .name: name,
1209 // note: always return the type of the attached type as the owner.
1210 // Find usages on "Keys.", for example, should yield all usages of
1211 // the "Keys" attached property.
1212 .semanticScope: bindingIsAttached
1213 ? getScope()
1214 : findDefiningScopeForProperty(referrerScope, nameToCheck: name),
1215 .type: typeIdentifier };
1216 }
1217 case ResolveActualTypeForFieldMemberExpression:
1218 return ExpressionType{ .name: name, .semanticScope: getScope(), .type: typeIdentifier };
1219 }
1220 Q_UNREACHABLE_RETURN({});
1221}
1222
1223/*! \internal
1224 Finds the scope within the special elements like Connections,
1225 PropertyChanges, Bindings or AnchorChanges.
1226*/
1227static QQmlJSScope::ConstPtr findScopeOfSpecialItems(QQmlJSScope::ConstPtr scope, const DomItem &item)
1228{
1229 if (!scope)
1230 return {};
1231
1232 const QSet<QString> specialItems = {u"QQmlConnections"_s,
1233 u"QQuickPropertyChanges"_s,
1234 u"QQmlBind"_s,
1235 u"QQuickAnchorChanges"_s};
1236
1237 const auto special = QQmlJSUtils::searchBaseAndExtensionTypes(
1238 type: scope, check: [&specialItems](QQmlJSScope::ConstPtr visitedScope) {
1239 const auto typeName = visitedScope->internalName();
1240 if (specialItems.contains(value: typeName))
1241 return true;
1242 return false;
1243 });
1244
1245 if (!special)
1246 return {};
1247
1248 // Perform target name search if there is binding to property "target"
1249 QString targetName;
1250 if (scope->hasOwnPropertyBindings(name: u"target"_s)) {
1251 // TODO: propagate the whole binding.
1252 // We can figure out the meaning of target in more cases.
1253
1254 DomItem current = item.qmlObject();
1255 auto target = current.bindings().key(name: u"target"_s).index(0);
1256 if (target) {
1257 targetName = target.field(name: Fields::value)
1258 .field(name: Fields::scriptElement)
1259 .field(name: Fields::identifier)
1260 .value()
1261 .toString();
1262 }
1263 }
1264
1265 if (!targetName.isEmpty()) {
1266 // target: someId
1267 auto resolver = item.containingFile().ownerAs<QmlFile>()->typeResolver();
1268 if (!resolver)
1269 return {};
1270
1271 // Note: It does not have to be an ID. It can be a property.
1272 return resolver->containedType(container: resolver->scopedType(scope, name: targetName));
1273 } else {
1274 if (item.internalKind() == DomType::Binding &&
1275 item.field(name: Fields::bindingType).value().toInteger() == int(BindingType::OnBinding)) {
1276 // Binding on sth : {} syntax
1277 // Target scope is the current scope
1278 return scope;
1279 }
1280 return scope->parentScope();
1281 }
1282
1283 return {};
1284}
1285
1286/*!
1287\internal
1288\brief Distinguishes singleton types from attached types and "regular" qml components.
1289 */
1290static std::optional<ExpressionType>
1291resolveTypeName(const std::shared_ptr<QQmlJSTypeResolver> &resolver, const QString &name,
1292 const DomItem &item, ResolveOptions options)
1293{
1294 const auto scope = resolver->typeForName(name);
1295 if (!scope)
1296 return {};
1297
1298 if (scope->isSingleton())
1299 return ExpressionType{ .name: name, .semanticScope: scope, .type: IdentifierType::SingletonIdentifier };
1300
1301 // A type not followed by a field member expression is just a type. Otherwise, it could either
1302 // be a type or an attached type!
1303 if (!isFieldMemberBase(item))
1304 return ExpressionType{ .name: name, .semanticScope: scope, .type: QmlComponentIdentifier };
1305
1306 // take the right hand side and unwrap in case its a nested fieldmemberexpression
1307 const DomItem rightHandSide = [&item]() {
1308 const DomItem candidate = item.directParent().field(name: Fields::right);
1309 // case B(a,b) for the right hand side of `a` in `a.b`
1310 if (candidate != item)
1311 return candidate;
1312 // case B(B(a,b),c) for the right hand side of `b` in `a.b.c`
1313 return item.directParent().directParent().field(name: Fields::right);
1314 }();
1315
1316 if (rightHandSide.internalKind() != DomType::ScriptIdentifierExpression)
1317 return ExpressionType{ .name: name, .semanticScope: scope, .type: QmlComponentIdentifier };
1318
1319 const QString fieldMemberAccessName = rightHandSide.value().toString();
1320 if (fieldMemberAccessName.isEmpty() || !fieldMemberAccessName.front().isLower())
1321 return ExpressionType{ .name: name, .semanticScope: scope, .type: QmlComponentIdentifier };
1322
1323 return ExpressionType{ .name: name, .semanticScope: options == ResolveOwnerType ? scope : scope->attachedType(),
1324 .type: IdentifierType::AttachedTypeIdentifier };
1325}
1326
1327static std::optional<ExpressionType> resolveFieldMemberExpressionType(const DomItem &item,
1328 ResolveOptions options)
1329{
1330 const QString name = item.field(name: Fields::identifier).value().toString();
1331 DomItem parent = item.directParent();
1332 auto owner = resolveExpressionType(item: parent.field(name: Fields::left),
1333 ResolveOptions::ResolveActualTypeForFieldMemberExpression);
1334 if (!owner)
1335 return {};
1336
1337 if (!owner->semanticScope) {
1338 // JS objects can get new members and methods during runtime and therefore has no
1339 // qqmljsscopes. Therefore, just label everything inside a JavaScriptIdentifier as being
1340 // another JavaScriptIdentifier.
1341 if (owner->type == JavaScriptIdentifier) {
1342 return ExpressionType{ .name: name, .semanticScope: {}, .type: JavaScriptIdentifier };
1343 } else if (owner->type == QualifiedModuleIdentifier) {
1344 auto resolver = item.fileObject().as<QmlFile>()->typeResolver();
1345 if (auto scope = resolveTypeName(resolver, name: u"%1.%2"_s.arg(args&: *owner->name, args: name), item,
1346 options)) {
1347 // remove the qualified module name from the type name
1348 scope->name = name;
1349 return scope;
1350 }
1351 }
1352 return {};
1353 }
1354
1355 if (auto scope = methodFromReferrerScope(referrerScope: owner->semanticScope, name, options))
1356 return *scope;
1357
1358 if (auto scope = propertyBindingFromReferrerScope(referrerScope: owner->semanticScope, name, options, resolverForIds: nullptr))
1359 return *scope;
1360
1361 if (auto scope = propertyFromReferrerScope(referrerScope: owner->semanticScope, propertyName: name, options))
1362 return *scope;
1363
1364 if (owner->type == QmlComponentIdentifier || owner->type == EnumeratorIdentifier) {
1365 // Check if name is a enum value <TypeName>.<EnumValue> or ...<EnumName>.<EnumValue>
1366 // Enumerations should live under the root element scope of the file that defines the enum,
1367 // therefore use the DomItem to find the root element of the qml file instead of directly
1368 // using owner->semanticScope.
1369 const auto scope = item.goToFile(filePath: owner->semanticScope->filePath())
1370 .rootQmlObject(option: GoTo::MostLikely)
1371 .semanticScope();
1372 if (scope->hasEnumerationKey(name)) {
1373 return ExpressionType{ .name: name, .semanticScope: scope, .type: EnumeratorValueIdentifier };
1374 }
1375 // Or it is a enum name <TypeName>.<EnumName>.<EnumValue>
1376 else if (scope->hasEnumeration(name)) {
1377 return ExpressionType{ .name: name, .semanticScope: scope, .type: EnumeratorIdentifier };
1378 }
1379
1380 // check inline components <TypeName>.<InlineComponentName>
1381 for (auto it = owner->semanticScope->childScopesBegin(),
1382 end = owner->semanticScope->childScopesEnd();
1383 it != end; ++it) {
1384 if ((*it)->inlineComponentName() == name) {
1385 return ExpressionType{ .name: name, .semanticScope: *it, .type: QmlComponentIdentifier };
1386 }
1387 }
1388 return {};
1389 }
1390
1391 qCDebug(QQmlLSUtilsLog) << "Could not find identifier expression for" << item.internalKindStr();
1392 return owner;
1393}
1394
1395/*!
1396\internal
1397Resolves the expression type of a binding for signal handlers, like the function expression
1398\c{(x) => ...} in
1399
1400\qml
1401onHelloSignal: (x) => ...
1402\endqml
1403
1404would be resolved to the \c{onHelloSignal} expression type, for example.
1405*/
1406static std::optional<ExpressionType> resolveBindingIfSignalHandler(const DomItem &functionExpression)
1407{
1408 if (functionExpression.internalKind() != DomType::ScriptFunctionExpression)
1409 return {};
1410
1411 const DomItem parent = functionExpression.directParent();
1412 if (parent.internalKind() != DomType::ScriptExpression)
1413 return {};
1414
1415 const DomItem grandParent = parent.directParent();
1416 if (grandParent.internalKind() != DomType::Binding)
1417 return {};
1418
1419 auto bindingType = resolveExpressionType(item: grandParent, ResolveOwnerType);
1420 return bindingType;
1421}
1422
1423/*!
1424\internal
1425In a signal handler
1426
1427\qml
1428 onSomeSignal: (x, y, z) => ....
1429\endqml
1430
1431the parameters \c x, \c y and \c z are not allowed to have type annotations: instead, their type is
1432defined by the signal definition itself.
1433
1434This code detects signal handler parameters and resolves their type using the signal's definition.
1435*/
1436static std::optional<ExpressionType>
1437resolveSignalHandlerParameterType(const DomItem &parameterDefinition, const QString &name,
1438 ResolveOptions options)
1439{
1440 const std::optional<QQmlJSScope::JavaScriptIdentifier> jsIdentifier =
1441 parameterDefinition.semanticScope()->jsIdentifier(id: name);
1442 if (!jsIdentifier || jsIdentifier->kind != QQmlJSScope::JavaScriptIdentifier::Parameter)
1443 return {};
1444
1445 const DomItem handlerFunctionExpression =
1446 parameterDefinition.internalKind() == DomType::ScriptBlockStatement
1447 ? parameterDefinition.directParent()
1448 : parameterDefinition;
1449
1450 const std::optional<ExpressionType> bindingType =
1451 resolveBindingIfSignalHandler(functionExpression: handlerFunctionExpression);
1452 if (!bindingType)
1453 return {};
1454
1455 if (bindingType->type == PropertyChangedHandlerIdentifier)
1456 return ExpressionType{};
1457
1458 if (bindingType->type != SignalHandlerIdentifier)
1459 return {};
1460
1461 const DomItem parameters = handlerFunctionExpression[Fields::parameters];
1462 const int indexOfParameter = [&parameters, &name]() {
1463 for (int i = 0; i < parameters.indexes(); ++i) {
1464 if (parameters[i][Fields::identifier].value().toString() == name)
1465 return i;
1466 }
1467 Q_ASSERT_X(false, "resolveSignalHandlerParameter",
1468 "can't find JS identifier with Parameter kind in the parameters");
1469 Q_UNREACHABLE_RETURN(-1);
1470 }();
1471
1472 const std::optional<QString> signalName =
1473 QQmlSignalNames::handlerNameToSignalName(handler: *bindingType->name);
1474 Q_ASSERT_X(signalName.has_value(), "resolveSignalHandlerParameterType",
1475 "handlerNameToSignalName failed on a SignalHandler");
1476
1477 const QQmlJSMetaMethod signalDefinition =
1478 bindingType->semanticScope->methods(name: *signalName).front();
1479 const QList<QQmlJSMetaParameter> parameterList = signalDefinition.parameters();
1480
1481 // not a signal handler parameter after all
1482 if (parameterList.size() <= indexOfParameter)
1483 return {};
1484
1485 // now we can return an ExpressionType, even if the indexOfParameter calculation result is only
1486 // needed to check whether this is a signal handler parameter or not.
1487 if (options == ResolveOwnerType)
1488 return ExpressionType{ .name: name, .semanticScope: bindingType->semanticScope, .type: JavaScriptIdentifier };
1489 else {
1490 const QQmlJSScope::ConstPtr parameterType = parameterList[indexOfParameter].type();
1491 return ExpressionType{ .name: name, .semanticScope: parameterType, .type: JavaScriptIdentifier };
1492 }
1493}
1494
1495static std::optional<ExpressionType> resolveIdentifierExpressionType(const DomItem &item,
1496 ResolveOptions options)
1497{
1498 if (isFieldMemberAccess(item) || isFieldMemberExpression(item)) {
1499 return resolveFieldMemberExpressionType(item, options);
1500 }
1501
1502 const QString name = item.field(name: Fields::identifier).value().toString();
1503
1504 if (DomItem definitionOfItem = findJSIdentifierDefinition(item, name)) {
1505 Q_ASSERT_X(!definitionOfItem.semanticScope().isNull()
1506 && definitionOfItem.semanticScope()->ownJSIdentifier(name),
1507 "QQmlLSUtils::findDefinitionOf",
1508 "JS definition does not actually define the JS identifer. "
1509 "It should be empty.");
1510 if (auto parameter = resolveSignalHandlerParameterType(parameterDefinition: definitionOfItem, name, options))
1511 return parameter;
1512
1513 const auto scope = definitionOfItem.semanticScope();
1514 return ExpressionType{ .name: name,
1515 .semanticScope: options == ResolveOwnerType
1516 ? scope
1517 : QQmlJSScope::ConstPtr(
1518 scope->ownJSIdentifier(id: name)->scope.toStrongRef()),
1519 .type: IdentifierType::JavaScriptIdentifier };
1520 }
1521
1522 const auto referrerScope = item.nearestSemanticScope();
1523 if (!referrerScope)
1524 return {};
1525
1526 // check if its a method
1527 if (auto scope = methodFromReferrerScope(referrerScope, name, options))
1528 return scope;
1529
1530 const auto resolver = item.containingFile().ownerAs<QmlFile>()->typeResolver();
1531 if (!resolver)
1532 return {};
1533
1534 // check if its found as a property binding
1535 if (auto scope = propertyBindingFromReferrerScope(referrerScope, name, options, resolverForIds: resolver.get()))
1536 return *scope;
1537
1538 // check if its an (unqualified) property
1539 if (auto scope = propertyFromReferrerScope(referrerScope, propertyName: name, options))
1540 return *scope;
1541
1542 if (resolver->seenModuleQualifiers().contains(str: name))
1543 return ExpressionType{ .name: name, .semanticScope: {}, .type: QualifiedModuleIdentifier };
1544
1545 if (const auto scope = resolveTypeName(resolver, name, item, options))
1546 return scope;
1547
1548 // check if its an id
1549 QQmlJSRegisterContent fromId =
1550 resolver->scopedType(scope: referrerScope, name, lookupIndex: QQmlJSRegisterContent::InvalidLookupIndex,
1551 options: AssumeComponentsAreBound);
1552 if (fromId.variant() == QQmlJSRegisterContent::ObjectById)
1553 return ExpressionType{ .name: name, .semanticScope: fromId.type(), .type: QmlObjectIdIdentifier };
1554
1555 const QQmlJSScope::ConstPtr jsGlobal = resolver->jsGlobalObject();
1556 // check if its a JS global method
1557 if (auto scope = methodFromReferrerScope(referrerScope: jsGlobal, name, options)) {
1558 scope->type = JavaScriptIdentifier;
1559 return scope;
1560 }
1561
1562 // check if its an JS global property
1563 if (auto scope = propertyFromReferrerScope(referrerScope: jsGlobal, propertyName: name, options)) {
1564 scope->type = JavaScriptIdentifier;
1565 return scope;
1566 }
1567
1568 return {};
1569}
1570
1571static std::optional<ExpressionType>
1572resolveSignalOrPropertyExpressionType(const QString &name, const QQmlJSScope::ConstPtr &scope,
1573 ResolveOptions options)
1574{
1575 auto signalOrProperty = resolveNameInQmlScope(name, owner: scope);
1576 if (!signalOrProperty)
1577 return {};
1578
1579 switch (signalOrProperty->type) {
1580 case PropertyIdentifier:
1581 switch (options) {
1582 case ResolveOwnerType:
1583 return ExpressionType{ .name: name, .semanticScope: findDefiningScopeForProperty(referrerScope: scope, nameToCheck: name),
1584 .type: signalOrProperty->type };
1585 case ResolveActualTypeForFieldMemberExpression:
1586 return ExpressionType{ .name: name, .semanticScope: scope->property(name).type(), .type: signalOrProperty->type };
1587 }
1588 Q_UNREACHABLE_RETURN({});
1589 case PropertyChangedHandlerIdentifier:
1590 switch (options) {
1591 case ResolveOwnerType:
1592 return ExpressionType{ .name: name,
1593 .semanticScope: findDefiningScopeForProperty(referrerScope: scope, nameToCheck: signalOrProperty->name),
1594 .type: signalOrProperty->type };
1595 case ResolveActualTypeForFieldMemberExpression:
1596 // Properties and methods are not implemented on methods.
1597 Q_UNREACHABLE_RETURN({});
1598 }
1599 Q_UNREACHABLE_RETURN({});
1600 case SignalHandlerIdentifier:
1601 case PropertyChangedSignalIdentifier:
1602 case SignalIdentifier:
1603 case MethodIdentifier:
1604 switch (options) {
1605 case ResolveOwnerType: {
1606 return ExpressionType{ .name: name, .semanticScope: findDefiningScopeForMethod(referrerScope: scope, nameToCheck: signalOrProperty->name),
1607 .type: signalOrProperty->type };
1608 }
1609 case ResolveActualTypeForFieldMemberExpression:
1610 // Properties and methods are not implemented on methods.
1611 Q_UNREACHABLE_RETURN({});
1612 }
1613 Q_UNREACHABLE_RETURN({});
1614 default:
1615 Q_UNREACHABLE_RETURN({});
1616 }
1617}
1618
1619/*!
1620 \internal
1621 \brief
1622 Resolves the type of the given DomItem, when possible (e.g., when there are enough type
1623 annotations).
1624
1625 Might return an ExpressionType without(!) semantic scope when no type information is available, for
1626 example resolving the type of x in someJSObject.x (where `let someJSObject = { x: 42 };`) then
1627 the name and type of x is known but no semantic scope can be obtained.
1628*/
1629std::optional<ExpressionType> resolveExpressionType(const QQmlJS::Dom::DomItem &item,
1630 ResolveOptions options)
1631{
1632 switch (item.internalKind()) {
1633 case DomType::ScriptIdentifierExpression: {
1634 return resolveIdentifierExpressionType(item, options);
1635 }
1636 case DomType::PropertyDefinition: {
1637 auto propertyDefinition = item.as<PropertyDefinition>();
1638 if (propertyDefinition && propertyDefinition->semanticScope()) {
1639 const auto &scope = propertyDefinition->semanticScope();
1640 switch (options) {
1641 case ResolveOwnerType:
1642 return ExpressionType{ .name: propertyDefinition->name, .semanticScope: scope, .type: PropertyIdentifier };
1643 case ResolveActualTypeForFieldMemberExpression:
1644 // There should not be any PropertyDefinition inside a FieldMemberExpression.
1645 Q_UNREACHABLE_RETURN({});
1646 }
1647 Q_UNREACHABLE_RETURN({});
1648 }
1649 return {};
1650 }
1651 case DomType::Binding: {
1652 auto binding = item.as<Binding>();
1653 if (binding) {
1654 std::optional<QQmlJSScope::ConstPtr> owner = item.qmlObject().semanticScope();
1655 if (!owner)
1656 return {};
1657 const QString name = binding->name();
1658
1659 if (name == u"id")
1660 return ExpressionType{ .name: name, .semanticScope: owner.value(), .type: QmlObjectIdIdentifier };
1661
1662 if (QQmlJSScope::ConstPtr targetScope = findScopeOfSpecialItems(scope: owner.value(), item)) {
1663 const auto signalOrProperty = resolveNameInQmlScope(name, owner: targetScope);
1664 if (!signalOrProperty)
1665 return {};
1666 switch (options) {
1667 case ResolveOwnerType:
1668 return ExpressionType{
1669 .name: name, .semanticScope: findDefiningScopeForBinding(referrerScope: targetScope, nameToCheck: signalOrProperty->name),
1670 .type: signalOrProperty->type
1671 };
1672 case ResolveActualTypeForFieldMemberExpression:
1673 // Bindings can't be inside of FieldMemberExpressions.
1674 Q_UNREACHABLE_RETURN({});
1675 }
1676 }
1677 if (auto result = resolveSignalOrPropertyExpressionType(name, scope: owner.value(), options)) {
1678 return result;
1679 }
1680 qDebug(catFunc: QQmlLSUtilsLog) << "QQmlLSUtils::resolveExpressionType() could not resolve the"
1681 "type of a Binding.";
1682 }
1683
1684 return {};
1685 }
1686 case DomType::QmlObject: {
1687 auto object = item.as<QmlObject>();
1688 if (!object)
1689 return {};
1690 if (auto scope = object->semanticScope()) {
1691 const auto name = item.name();
1692 const bool isComponent = name.front().isUpper();
1693 if (isComponent)
1694 scope = scope->baseType();
1695 const IdentifierType type =
1696 isComponent ? QmlComponentIdentifier : GroupedPropertyIdentifier;
1697 switch (options) {
1698 case ResolveOwnerType:
1699 return ExpressionType{ .name: name, .semanticScope: scope, .type: type };
1700 case ResolveActualTypeForFieldMemberExpression:
1701 return ExpressionType{ .name: name, .semanticScope: scope, .type: type };
1702 }
1703 }
1704 return {};
1705 }
1706 case DomType::QmlComponent: {
1707 auto component = item.as<QmlComponent>();
1708 if (!component)
1709 return {};
1710 const auto scope = component->semanticScope();
1711 if (!scope)
1712 return {};
1713
1714 QString name = item.name();
1715 if (auto dotIndex = name.indexOf(ch: u'.'); dotIndex != -1)
1716 name = name.sliced(pos: dotIndex + 1);
1717 switch (options) {
1718 case ResolveOwnerType:
1719 return ExpressionType{ .name: name, .semanticScope: scope, .type: QmlComponentIdentifier };
1720 case ResolveActualTypeForFieldMemberExpression:
1721 return ExpressionType{ .name: name, .semanticScope: scope, .type: QmlComponentIdentifier };
1722 }
1723 Q_UNREACHABLE_RETURN({});
1724 }
1725 case DomType::ScriptFunctionExpression: {
1726 return ExpressionType{ .name: {}, .semanticScope: item.semanticScope(), .type: LambdaMethodIdentifier };
1727 }
1728 case DomType::MethodInfo: {
1729 auto object = item.as<MethodInfo>();
1730 if (object && object->semanticScope()) {
1731 std::optional<QQmlJSScope::ConstPtr> scope = object->semanticScope();
1732 if (!scope)
1733 return {};
1734
1735 if (QQmlJSScope::ConstPtr targetScope =
1736 findScopeOfSpecialItems(scope: scope.value()->parentScope(), item)) {
1737 const auto signalOrProperty = resolveNameInQmlScope(name: object->name, owner: targetScope);
1738 if (!signalOrProperty)
1739 return {};
1740
1741 switch (options) {
1742 case ResolveOwnerType:
1743 return ExpressionType{ .name: object->name,
1744 .semanticScope: findDefiningScopeForMethod(referrerScope: targetScope,
1745 nameToCheck: signalOrProperty->name),
1746 .type: signalOrProperty->type };
1747 case ResolveActualTypeForFieldMemberExpression:
1748 // not supported for methods
1749 return {};
1750 }
1751 }
1752
1753 // in case scope is the semantic scope for the function bodies: grab the owner's scope
1754 // this happens for all methods but not for signals (they do not have a body)
1755 if (scope.value()->scopeType() == QQmlJSScope::ScopeType::JSFunctionScope)
1756 scope = scope.value()->parentScope();
1757
1758 if (auto result = resolveSignalOrPropertyExpressionType(name: object->name, scope: scope.value(),
1759 options)) {
1760 return result;
1761 }
1762 qDebug(catFunc: QQmlLSUtilsLog) << "QQmlLSUtils::resolveExpressionType() could not resolve the"
1763 "type of a MethodInfo.";
1764 }
1765
1766 return {};
1767 }
1768 case DomType::ScriptBinaryExpression: {
1769 if (isFieldMemberExpression(item)) {
1770 return resolveExpressionType(item: item.field(name: Fields::right), options);
1771 }
1772 return {};
1773 }
1774 case DomType::ScriptLiteral: {
1775 /* special case
1776 Binding { target: someId; property: "someProperty"}
1777 */
1778 const auto scope = item.qmlObject().semanticScope();
1779 const auto name = item.field(name: Fields::value).value().toString();
1780 if (QQmlJSScope::ConstPtr targetScope = findScopeOfSpecialItems(scope, item)) {
1781 const auto signalOrProperty = resolveNameInQmlScope(name, owner: targetScope);
1782 if (!signalOrProperty)
1783 return {};
1784 switch (options) {
1785 case ResolveOwnerType:
1786 return ExpressionType{
1787 .name: name, .semanticScope: findDefiningScopeForProperty(referrerScope: targetScope, nameToCheck: signalOrProperty->name),
1788 .type: signalOrProperty->type
1789 };
1790 case ResolveActualTypeForFieldMemberExpression:
1791 // ScriptLiteral's can't be inside of FieldMemberExpression's, especially when they
1792 // are inside a special item.
1793 Q_UNREACHABLE_RETURN({});
1794 }
1795 }
1796 return {};
1797 }
1798 case DomType::EnumItem: {
1799 const QString enumValue = item.field(name: Fields::name).value().toString();
1800 QQmlJSScope::ConstPtr referrerScope = item.rootQmlObject(option: GoTo::MostLikely).semanticScope();
1801 if (!referrerScope->hasEnumerationKey(name: enumValue))
1802 return {};
1803 switch (options) {
1804 // special case: use the owner's scope here, as enums do not have their own
1805 // QQmlJSScope.
1806 case ResolveActualTypeForFieldMemberExpression:
1807 case ResolveOwnerType:
1808 return ExpressionType{ .name: enumValue,
1809 .semanticScope: findDefiningScopeForEnumerationKey(referrerScope, nameToCheck: enumValue),
1810 .type: EnumeratorValueIdentifier };
1811 }
1812 Q_UNREACHABLE_RETURN({});
1813 }
1814 case DomType::EnumDecl: {
1815 const QString enumName = item.field(name: Fields::name).value().toString();
1816 QQmlJSScope::ConstPtr referrerScope = item.rootQmlObject(option: GoTo::MostLikely).semanticScope();
1817 if (!referrerScope->hasEnumeration(name: enumName))
1818 return {};
1819 switch (options) {
1820 // special case: use the owner's scope here, as enums do not have their own QQmlJSScope.
1821 case ResolveActualTypeForFieldMemberExpression:
1822 case ResolveOwnerType:
1823 return ExpressionType{ .name: enumName,
1824 .semanticScope: findDefiningScopeForEnumeration(referrerScope, nameToCheck: enumName),
1825 .type: EnumeratorIdentifier };
1826 }
1827
1828 Q_UNREACHABLE_RETURN({});
1829 }
1830 case DomType::Import: {
1831 // we currently only support qualified module identifiers
1832 if (!item[Fields::importId])
1833 return {};
1834 return ExpressionType{ .name: item[Fields::importId].value().toString(),
1835 .semanticScope: {},
1836 .type: QualifiedModuleIdentifier };
1837 }
1838 case DomType::ScriptNewMemberExpression: {
1839 const auto name = item.field(name: Fields::base).value().toString();
1840 return ExpressionType{ .name: name, .semanticScope: {}, .type: JavaScriptIdentifier };
1841 }
1842 case DomType::ScriptCallExpression: {
1843 const DomItem callee = item.field(name: Fields::callee);
1844 const auto calleeExpressionType = resolveExpressionType(item: callee, options: ResolveOwnerType);
1845
1846 if (!calleeExpressionType || !calleeExpressionType->semanticScope
1847 || !calleeExpressionType->name || calleeExpressionType->type != MethodIdentifier) {
1848 return {};
1849 }
1850
1851 const auto methods =
1852 calleeExpressionType->semanticScope->methods(name: *calleeExpressionType->name);
1853 if (methods.isEmpty())
1854 return {};
1855
1856 const auto returnType = methods.front().returnType();
1857 return ExpressionType{ .name: {}, .semanticScope: returnType, .type: NotAnIdentifier };
1858 }
1859 default: {
1860 qCDebug(QQmlLSUtilsLog) << "Type" << item.internalKindStr()
1861 << "is unimplemented in QQmlLSUtils::resolveExpressionType";
1862 return {};
1863 }
1864 }
1865 Q_UNREACHABLE();
1866}
1867
1868DomItem sourceLocationToDomItem(const DomItem &file, const QQmlJS::SourceLocation &location)
1869{
1870 // QQmlJS::SourceLocation starts counting at 1 but the utils and the LSP start at 0.
1871 auto items = QQmlLSUtils::itemsFromTextLocation(file, line: location.startLine - 1,
1872 character: location.startColumn - 1);
1873 switch (items.size()) {
1874 case 0:
1875 return {};
1876 case 1:
1877 return items.front().domItem;
1878 case 2: {
1879 // special case: because location points to the beginning of the type definition,
1880 // itemsFromTextLocation might also return the type on its left, in case it is directly
1881 // adjacent to it. In this case always take the right (=with the higher column-number)
1882 // item.
1883 auto &first = items.front();
1884 auto &second = items.back();
1885 Q_ASSERT_X(first.fileLocation->info().fullRegion.startLine
1886 == second.fileLocation->info().fullRegion.startLine,
1887 "QQmlLSUtils::findTypeDefinitionOf(DomItem)",
1888 "QQmlLSUtils::itemsFromTextLocation returned non-adjacent items.");
1889 if (first.fileLocation->info().fullRegion.startColumn
1890 > second.fileLocation->info().fullRegion.startColumn)
1891 return first.domItem;
1892 else
1893 return second.domItem;
1894 break;
1895 }
1896 default:
1897 qDebug() << "Found multiple candidates for type of scriptidentifierexpression";
1898 break;
1899 }
1900 return {};
1901}
1902
1903static std::optional<Location>
1904findMethodDefinitionOf(const DomItem &file, QQmlJS::SourceLocation location, const QString &name)
1905{
1906 DomItem owner = QQmlLSUtils::sourceLocationToDomItem(file, location).qmlObject();
1907 DomItem method = owner.field(name: Fields::methods).key(name).index(0);
1908 auto fileLocation = FileLocations::treeOf(method);
1909 if (!fileLocation)
1910 return {};
1911
1912 auto regions = fileLocation->info().regions;
1913
1914 if (auto it = regions.constFind(key: IdentifierRegion); it != regions.constEnd()) {
1915 return Location::tryFrom(fileName: method.canonicalFilePath(), sourceLocation: *it, someItem: file);
1916 }
1917
1918 return {};
1919}
1920
1921static std::optional<Location>
1922findPropertyDefinitionOf(const DomItem &file, QQmlJS::SourceLocation propertyDefinitionLocation,
1923 const QString &name)
1924{
1925 DomItem propertyOwner =
1926 QQmlLSUtils::sourceLocationToDomItem(file, location: propertyDefinitionLocation).qmlObject();
1927 DomItem propertyDefinition = propertyOwner.field(name: Fields::propertyDefs).key(name).index(0);
1928 auto fileLocation = FileLocations::treeOf(propertyDefinition);
1929 if (!fileLocation)
1930 return {};
1931
1932 auto regions = fileLocation->info().regions;
1933
1934 if (auto it = regions.constFind(key: IdentifierRegion); it != regions.constEnd()) {
1935 return Location::tryFrom(fileName: propertyDefinition.canonicalFilePath(), sourceLocation: *it, someItem: file);
1936 }
1937
1938 return {};
1939}
1940
1941std::optional<Location> findDefinitionOf(const DomItem &item)
1942{
1943 auto resolvedExpression = resolveExpressionType(item, options: ResolveOptions::ResolveOwnerType);
1944
1945 if (!resolvedExpression || !resolvedExpression->name
1946 || (!resolvedExpression->semanticScope
1947 && resolvedExpression->type != QualifiedModuleIdentifier)) {
1948 qCDebug(QQmlLSUtilsLog) << "QQmlLSUtils::findDefinitionOf: Type could not be resolved.";
1949 return {};
1950 }
1951
1952 switch (resolvedExpression->type) {
1953 case JavaScriptIdentifier: {
1954 const auto jsIdentifier =
1955 resolvedExpression->semanticScope->ownJSIdentifier(id: *resolvedExpression->name);
1956 if (!jsIdentifier)
1957 return {};
1958
1959 return Location::tryFrom(fileName: resolvedExpression->semanticScope->filePath(),
1960 sourceLocation: jsIdentifier->location, someItem: item);
1961 }
1962
1963 case PropertyIdentifier: {
1964 const DomItem ownerFile = item.goToFile(filePath: resolvedExpression->semanticScope->filePath());
1965 const QQmlJS::SourceLocation ownerLocation =
1966 resolvedExpression->semanticScope->sourceLocation();
1967 return findPropertyDefinitionOf(file: ownerFile, propertyDefinitionLocation: ownerLocation, name: *resolvedExpression->name);
1968 }
1969 case PropertyChangedSignalIdentifier:
1970 case PropertyChangedHandlerIdentifier:
1971 case SignalIdentifier:
1972 case SignalHandlerIdentifier:
1973 case MethodIdentifier: {
1974 const DomItem ownerFile = item.goToFile(filePath: resolvedExpression->semanticScope->filePath());
1975 const QQmlJS::SourceLocation ownerLocation =
1976 resolvedExpression->semanticScope->sourceLocation();
1977 return findMethodDefinitionOf(file: ownerFile, location: ownerLocation, name: *resolvedExpression->name);
1978 }
1979 case QmlObjectIdIdentifier: {
1980 DomItem qmlObject = QQmlLSUtils::sourceLocationToDomItem(
1981 file: item.containingFile(), location: resolvedExpression->semanticScope->sourceLocation());
1982 // in the Dom, the id is saved in a QMultiHash inside the Component of an QmlObject.
1983 const DomItem domId = qmlObject.component()
1984 .field(name: Fields::ids)
1985 .key(name: *resolvedExpression->name)
1986 .index(0)
1987 .field(name: Fields::value);
1988 if (!domId) {
1989 qCDebug(QQmlLSUtilsLog)
1990 << "QmlComponent in Dom structure has no id, was it misconstructed?";
1991 return {};
1992 }
1993
1994 return Location::tryFrom(fileName: domId.canonicalFilePath(),
1995 sourceLocation: FileLocations::treeOf(domId)->info().fullRegion, someItem: domId);
1996 }
1997 case AttachedTypeIdentifier:
1998 case QmlComponentIdentifier: {
1999 return Location::tryFrom(fileName: resolvedExpression->semanticScope->filePath(),
2000 sourceLocation: resolvedExpression->semanticScope->sourceLocation(), someItem: item);
2001 }
2002 case QualifiedModuleIdentifier: {
2003 const DomItem imports = item.fileObject().field(name: Fields::imports);
2004 for (int i = 0; i < imports.indexes(); ++i) {
2005 if (imports[i][Fields::importId].value().toString() == resolvedExpression->name) {
2006 const auto fileLocations = FileLocations::treeOf(imports[i]);
2007 if (!fileLocations)
2008 continue;
2009
2010 return Location::tryFrom(fileName: item.canonicalFilePath(), sourceLocation: fileLocations->info().regions[IdNameRegion], someItem: item);
2011 }
2012 }
2013 return {};
2014 }
2015 case SingletonIdentifier:
2016 case EnumeratorIdentifier:
2017 case EnumeratorValueIdentifier:
2018 case GroupedPropertyIdentifier:
2019 case LambdaMethodIdentifier:
2020 case NotAnIdentifier:
2021 qCDebug(QQmlLSUtilsLog) << "QQmlLSUtils::findDefinitionOf was not implemented for type"
2022 << resolvedExpression->type;
2023 return {};
2024 }
2025 Q_UNREACHABLE_RETURN({});
2026}
2027
2028static QQmlJSScope::ConstPtr propertyOwnerFrom(const QQmlJSScope::ConstPtr &type,
2029 const QString &name)
2030{
2031 Q_ASSERT(!name.isEmpty());
2032 Q_ASSERT(type);
2033
2034 QQmlJSScope::ConstPtr typeWithDefinition = type;
2035 while (typeWithDefinition && !typeWithDefinition->hasOwnProperty(name))
2036 typeWithDefinition = typeWithDefinition->baseType();
2037
2038 if (!typeWithDefinition) {
2039 qCDebug(QQmlLSUtilsLog)
2040 << "QQmlLSUtils::checkNameForRename cannot find property definition,"
2041 " ignoring.";
2042 }
2043
2044 return typeWithDefinition;
2045}
2046
2047static QQmlJSScope::ConstPtr methodOwnerFrom(const QQmlJSScope::ConstPtr &type,
2048 const QString &name)
2049{
2050 Q_ASSERT(!name.isEmpty());
2051 Q_ASSERT(type);
2052
2053 QQmlJSScope::ConstPtr typeWithDefinition = type;
2054 while (typeWithDefinition && !typeWithDefinition->hasOwnMethod(name))
2055 typeWithDefinition = typeWithDefinition->baseType();
2056
2057 if (!typeWithDefinition) {
2058 qCDebug(QQmlLSUtilsLog)
2059 << "QQmlLSUtils::checkNameForRename cannot find method definition,"
2060 " ignoring.";
2061 }
2062
2063 return typeWithDefinition;
2064}
2065
2066static QQmlJSScope::ConstPtr expressionTypeWithDefinition(const ExpressionType &ownerType)
2067{
2068 switch (ownerType.type) {
2069 case PropertyIdentifier:
2070 return propertyOwnerFrom(type: ownerType.semanticScope, name: *ownerType.name);
2071 case PropertyChangedHandlerIdentifier: {
2072 const auto propertyName =
2073 QQmlSignalNames::changedHandlerNameToPropertyName(handler: *ownerType.name);
2074 return propertyOwnerFrom(type: ownerType.semanticScope, name: *propertyName);
2075 break;
2076 }
2077 case PropertyChangedSignalIdentifier: {
2078 const auto propertyName = QQmlSignalNames::changedSignalNameToPropertyName(changeSignal: *ownerType.name);
2079 return propertyOwnerFrom(type: ownerType.semanticScope, name: *propertyName);
2080 }
2081 case MethodIdentifier:
2082 case SignalIdentifier:
2083 return methodOwnerFrom(type: ownerType.semanticScope, name: *ownerType.name);
2084 case SignalHandlerIdentifier: {
2085 const auto signalName = QQmlSignalNames::handlerNameToSignalName(handler: *ownerType.name);
2086 return methodOwnerFrom(type: ownerType.semanticScope, name: *signalName);
2087 }
2088 case JavaScriptIdentifier:
2089 case QmlObjectIdIdentifier:
2090 case SingletonIdentifier:
2091 case EnumeratorIdentifier:
2092 case EnumeratorValueIdentifier:
2093 case AttachedTypeIdentifier:
2094 case GroupedPropertyIdentifier:
2095 case QmlComponentIdentifier:
2096 case QualifiedModuleIdentifier:
2097 case LambdaMethodIdentifier:
2098 case NotAnIdentifier:
2099 return ownerType.semanticScope;
2100 }
2101 return {};
2102}
2103
2104std::optional<ErrorMessage> checkNameForRename(const DomItem &item, const QString &dirtyNewName,
2105 const std::optional<ExpressionType> &ownerType)
2106{
2107 if (!ownerType) {
2108 if (const auto resolved = resolveExpressionType(item, options: ResolveOwnerType))
2109 return checkNameForRename(item, dirtyNewName, ownerType: resolved);
2110 }
2111
2112 // general checks for ECMAscript identifiers
2113 if (!isValidEcmaScriptIdentifier(view: dirtyNewName))
2114 return ErrorMessage{ .code: 0, .message: u"Invalid EcmaScript identifier!"_s };
2115
2116 const auto userSemanticScope = item.nearestSemanticScope();
2117
2118 if (!ownerType || !userSemanticScope) {
2119 return ErrorMessage{ .code: 0, .message: u"Requested item cannot be renamed"_s };
2120 }
2121
2122 // type specific checks
2123 switch (ownerType->type) {
2124 case PropertyChangedSignalIdentifier: {
2125 if (!QQmlSignalNames::isChangedSignalName(signalName: dirtyNewName)) {
2126 return ErrorMessage{ .code: 0, .message: u"Invalid name for a property changed signal."_s };
2127 }
2128 break;
2129 }
2130 case PropertyChangedHandlerIdentifier: {
2131 if (!QQmlSignalNames::isChangedHandlerName(signalName: dirtyNewName)) {
2132 return ErrorMessage{ .code: 0, .message: u"Invalid name for a property changed handler identifier."_s };
2133 }
2134 break;
2135 }
2136 case SignalHandlerIdentifier: {
2137 if (!QQmlSignalNames::isHandlerName(signalName: dirtyNewName)) {
2138 return ErrorMessage{ .code: 0, .message: u"Invalid name for a signal handler identifier."_s };
2139 }
2140 break;
2141 }
2142 // TODO: any other specificities?
2143 case QmlObjectIdIdentifier:
2144 if (dirtyNewName.front().isLetter() && !dirtyNewName.front().isLower()) {
2145 return ErrorMessage{ .code: 0, .message: u"Object id names cannot start with an upper case letter."_s };
2146 }
2147 break;
2148 case JavaScriptIdentifier:
2149 case PropertyIdentifier:
2150 case SignalIdentifier:
2151 case MethodIdentifier:
2152 default:
2153 break;
2154 };
2155
2156 auto typeWithDefinition = expressionTypeWithDefinition(ownerType: *ownerType);
2157
2158 if (!typeWithDefinition) {
2159 return ErrorMessage{
2160 .code: 0,
2161 .message: u"Renaming has not been implemented for the requested item."_s,
2162 };
2163 }
2164
2165 // is it not defined in QML?
2166 if (!typeWithDefinition->isComposite()) {
2167 return ErrorMessage{ .code: 0, .message: u"Cannot rename items defined in non-QML files."_s };
2168 }
2169
2170 // is it defined in the current module?
2171 const QString moduleOfDefinition = ownerType->semanticScope->moduleName();
2172 const QString moduleOfCurrentItem = userSemanticScope->moduleName();
2173 if (moduleOfDefinition != moduleOfCurrentItem) {
2174 return ErrorMessage{
2175 .code: 0,
2176 .message: u"Cannot rename items defined in the \"%1\" module from a usage in the \"%2\" module."_s
2177 .arg(args: moduleOfDefinition, args: moduleOfCurrentItem),
2178 };
2179 }
2180
2181 return {};
2182}
2183
2184static std::optional<QString> oldNameFrom(const DomItem &item)
2185{
2186 switch (item.internalKind()) {
2187 case DomType::ScriptIdentifierExpression: {
2188 return item.field(name: Fields::identifier).value().toString();
2189 }
2190 case DomType::ScriptVariableDeclarationEntry: {
2191 return item.field(name: Fields::identifier).value().toString();
2192 }
2193 case DomType::PropertyDefinition:
2194 case DomType::Binding:
2195 case DomType::MethodInfo: {
2196 return item.field(name: Fields::name).value().toString();
2197 }
2198 default:
2199 qCDebug(QQmlLSUtilsLog) << item.internalKindStr()
2200 << "was not implemented for QQmlLSUtils::renameUsagesOf";
2201 return std::nullopt;
2202 }
2203 Q_UNREACHABLE_RETURN(std::nullopt);
2204}
2205
2206static std::optional<QString> newNameFrom(const QString &dirtyNewName, IdentifierType alternative)
2207{
2208 // When renaming signal/property changed handlers and property changed signals:
2209 // Get the actual corresponding signal name (for signal handlers) or property name (for
2210 // property changed signal + handlers) that will be used for the renaming.
2211 switch (alternative) {
2212 case SignalHandlerIdentifier: {
2213 return QQmlSignalNames::handlerNameToSignalName(handler: dirtyNewName);
2214 }
2215 case PropertyChangedHandlerIdentifier: {
2216 return QQmlSignalNames::changedHandlerNameToPropertyName(handler: dirtyNewName);
2217 }
2218 case PropertyChangedSignalIdentifier: {
2219 return QQmlSignalNames::changedSignalNameToPropertyName(changeSignal: dirtyNewName);
2220 }
2221 case SignalIdentifier:
2222 case PropertyIdentifier:
2223 default:
2224 return std::nullopt;
2225 }
2226 Q_UNREACHABLE_RETURN(std::nullopt);
2227}
2228
2229/*!
2230\internal
2231\brief Rename the appearance of item to newName.
2232
2233Special cases:
2234\list
2235 \li Renaming a property changed signal or property changed handler does the same as renaming
2236 the underlying property, except that newName gets
2237 \list
2238 \li its "on"-prefix and "Changed"-suffix chopped of if item is a property changed handlers
2239 \li its "Changed"-suffix chopped of if item is a property changed signals
2240 \endlist
2241 \li Renaming a signal handler does the same as renaming a signal, but the "on"-prefix in newName
2242 is chopped of.
2243
2244 All of the chopping operations are done using the static helpers from QQmlSignalNames.
2245\endlist
2246*/
2247RenameUsages renameUsagesOf(const DomItem &item, const QString &dirtyNewName,
2248 const std::optional<ExpressionType> &targetType)
2249{
2250 RenameUsages result;
2251 const Usages locations = findUsagesOf(item);
2252 if (locations.isEmpty())
2253 return result;
2254
2255 auto oldName = oldNameFrom(item);
2256 if (!oldName)
2257 return result;
2258
2259 QQmlJSScope::ConstPtr semanticScope;
2260 if (targetType) {
2261 semanticScope = targetType->semanticScope;
2262 } else if (const auto resolved =
2263 QQmlLSUtils::resolveExpressionType(item, options: ResolveOptions::ResolveOwnerType)) {
2264 semanticScope = resolved->semanticScope;
2265 } else {
2266 return result;
2267 }
2268
2269 QString newName;
2270 if (const auto resolved = resolveNameInQmlScope(name: *oldName, owner: semanticScope)) {
2271 newName = newNameFrom(dirtyNewName, alternative: resolved->type).value_or(u: dirtyNewName);
2272 oldName = resolved->name;
2273 } else {
2274 newName = dirtyNewName;
2275 }
2276
2277 const qsizetype oldNameLength = oldName->length();
2278 const qsizetype oldHandlerNameLength =
2279 QQmlSignalNames::signalNameToHandlerName(signal: *oldName).length();
2280 const qsizetype oldChangedSignalNameLength =
2281 QQmlSignalNames::propertyNameToChangedSignalName(property: *oldName).length();
2282 const qsizetype oldChangedHandlerNameLength =
2283 QQmlSignalNames::propertyNameToChangedHandlerName(property: *oldName).length();
2284
2285 const QString newHandlerName = QQmlSignalNames::signalNameToHandlerName(signal: newName);
2286 const QString newChangedSignalName = QQmlSignalNames::propertyNameToChangedSignalName(property: newName);
2287 const QString newChangedHandlerName =
2288 QQmlSignalNames::propertyNameToChangedHandlerName(property: newName);
2289
2290 // set the new name at the found usages, but add "on"-prefix and "Changed"-suffix if needed
2291 for (const auto &location : locations.usagesInFile()) {
2292 const qsizetype currentLength = location.sourceLocation().length;
2293 Edit edit;
2294 edit.location = location;
2295 if (oldNameLength == currentLength) {
2296 // normal case, nothing to do
2297 edit.replacement = newName;
2298
2299 } else if (oldHandlerNameLength == currentLength) {
2300 // signal handler location
2301 edit.replacement = newHandlerName;
2302
2303 } else if (oldChangedSignalNameLength == currentLength) {
2304 // property changed signal location
2305 edit.replacement = newChangedSignalName;
2306
2307 } else if (oldChangedHandlerNameLength == currentLength) {
2308 // property changed handler location
2309 edit.replacement = newChangedHandlerName;
2310
2311 } else {
2312 qCDebug(QQmlLSUtilsLog) << "Found usage with wrong identifier length, ignoring...";
2313 continue;
2314 }
2315 result.appendRename(edit);
2316 }
2317
2318 for (const auto &filename : locations.usagesInFilename()) {
2319 // assumption: we only rename files ending in .qml or .ui.qml in qmlls
2320 QString extension;
2321 if (filename.endsWith(s: u".ui.qml"_s))
2322 extension = u".ui.qml"_s;
2323 else if (filename.endsWith(s: u".qml"_s))
2324 extension = u".qml"_s;
2325 else
2326 continue;
2327
2328 QFileInfo info(filename);
2329 // do not rename the file if it has a custom type name in the qmldir
2330 if (!info.isFile() || info.baseName() != oldName)
2331 continue;
2332
2333 const QString newFilename =
2334 QDir::cleanPath(path: filename + "/.."_L1) + '/'_L1 + newName + extension;
2335 result.appendRename(edit: { .oldFilename: filename, .newFilename: newFilename });
2336 }
2337
2338 return result;
2339}
2340
2341std::optional<Location> Location::tryFrom(const QString &fileName,
2342 const QQmlJS::SourceLocation &sourceLocation,
2343 const QQmlJS::Dom::DomItem &someItem)
2344{
2345 auto qmlFile = someItem.goToFile(filePath: fileName).ownerAs<QQmlJS::Dom::QmlFile>();
2346 if (!qmlFile) {
2347 qDebug() << "Could not find file" << fileName << "in the dom!";
2348 return {};
2349 }
2350 return Location{ fileName, sourceLocation,
2351 textRowAndColumnFrom(text: qmlFile->code(), offset: sourceLocation.end()) };
2352}
2353
2354Location Location::from(const QString &fileName, const QQmlJS::SourceLocation &sourceLocation, const QString &code)
2355{
2356 return Location{ fileName, sourceLocation, textRowAndColumnFrom(text: code, offset: sourceLocation.end()) };
2357}
2358
2359Location Location::from(const QString &fileName, const QString &code, quint32 startLine,
2360 quint32 startCharacter, quint32 length)
2361{
2362 const quint32 offset = QQmlLSUtils::textOffsetFrom(text: code, row: startLine - 1, column: startCharacter - 1);
2363 return from(fileName, sourceLocation: QQmlJS::SourceLocation{ offset, length, startLine, startCharacter },
2364 code);
2365}
2366
2367Edit Edit::from(const QString &fileName, const QString &code, quint32 startLine,
2368 quint32 startCharacter, quint32 length, const QString &newName)
2369{
2370 Edit rename;
2371 rename.location = Location::from(fileName, code, startLine, startCharacter, length);
2372 rename.replacement = newName;
2373 return rename;
2374}
2375
2376bool isValidEcmaScriptIdentifier(QStringView identifier)
2377{
2378 QQmlJS::Lexer lexer(nullptr);
2379 lexer.setCode(code: identifier.toString(), lineno: 0);
2380 const int token = lexer.lex();
2381 if (token != static_cast<int>(QQmlJS::Lexer::T_IDENTIFIER))
2382 return false;
2383 // make sure there is nothing following the lexed identifier
2384 const int eofToken = lexer.lex();
2385 return eofToken == static_cast<int>(QQmlJS::Lexer::EOF_SYMBOL);
2386}
2387
2388
2389/*!
2390\internal
2391Returns the name of the cmake program along with the arguments needed to build the
2392qmltyperegistration. This command generates the .qmltypes, qmldir and .qrc files required for qmlls
2393to provide correct information on C++ defined QML elements.
2394
2395We assume here that CMake is available in the path. This should be the case for linux and macOS by
2396default.
2397For windows, having CMake in the path is not too unrealistic, for example,
2398https://doc.qt.io/qt-6/windows-building.html#step-2-install-build-requirements claims that you need
2399to have CMake in your path to build Qt. So a developer machine running qmlls has a high chance of
2400having CMake in their path, if CMake is installed and used.
2401*/
2402QPair<QString, QStringList> cmakeBuildCommand(const QString &path)
2403{
2404 const QPair<QString, QStringList> result{
2405 u"cmake"_s, { u"--build"_s, path, u"-t"_s, u"all_qmltyperegistrations"_s }
2406 };
2407 return result;
2408}
2409
2410void Usages::sort()
2411{
2412 std::sort(first: m_usagesInFile.begin(), last: m_usagesInFile.end());
2413 std::sort(first: m_usagesInFilename.begin(), last: m_usagesInFilename.end());
2414}
2415
2416bool Usages::isEmpty() const
2417{
2418 return m_usagesInFilename.isEmpty() && m_usagesInFile.isEmpty();
2419}
2420
2421Usages::Usages(const QList<Location> &usageInFile, const QList<QString> &usageInFilename)
2422 : m_usagesInFile(usageInFile), m_usagesInFilename(usageInFilename)
2423{
2424 std::sort(first: m_usagesInFile.begin(), last: m_usagesInFile.end());
2425 std::sort(first: m_usagesInFilename.begin(), last: m_usagesInFilename.end());
2426}
2427
2428RenameUsages::RenameUsages(const QList<Edit> &renamesInFile,
2429 const QList<FileRename> &renamesInFilename)
2430 : m_renamesInFile(renamesInFile), m_renamesInFilename(renamesInFilename)
2431{
2432 std::sort(first: m_renamesInFile.begin(), last: m_renamesInFile.end());
2433 std::sort(first: m_renamesInFilename.begin(), last: m_renamesInFilename.end());
2434}
2435
2436} // namespace QQmlLSUtils
2437
2438QT_END_NAMESPACE
2439

source code of qtdeclarative/src/qmlls/qqmllsutils.cpp