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

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

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