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