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,
1183 QQmlJSScopesByIdOption::AssumeComponentsAreBound),
1185 };
1186 }
1187
1188 const auto typeIdentifier =
1190
1191 const auto getScope = [bindingIsAttached, binding]() -> QQmlJSScope::ConstPtr {
1192 if (bindingIsAttached)
1193 return binding->attachedType();
1194
1195 return binding->groupType();
1196 };
1197
1198 switch (options) {
1199 case ResolveOwnerType: {
1200 return ExpressionType{ name,
1201 // note: always return the type of the attached type as the owner.
1202 // Find usages on "Keys.", for example, should yield all usages of
1203 // the "Keys" attached property.
1204 bindingIsAttached
1205 ? getScope()
1206 : findDefiningScopeForProperty(referrerScope, name),
1207 typeIdentifier };
1208 }
1210 return ExpressionType{ name, getScope(), typeIdentifier };
1211 }
1212 Q_UNREACHABLE_RETURN({});
1213}
1214
1215/*! \internal
1216 Finds the scope within the special elements like Connections,
1217 PropertyChanges, Bindings or AnchorChanges.
1218*/
1220 const QQmlJSScope::ConstPtr &scope, const DomItem &item)
1221{
1222 if (!scope)
1223 return {};
1224
1225 const QSet<QString> specialItems = {u"QQmlConnections"_s,
1226 u"QQuickPropertyChanges"_s,
1227 u"QQmlBind"_s,
1228 u"QQuickAnchorChanges"_s};
1229
1230 const auto special = QQmlJSUtils::searchBaseAndExtensionTypes(
1231 scope, [&specialItems](const QQmlJSScope::ConstPtr &visitedScope) {
1232 const auto typeName = visitedScope->internalName();
1233 if (specialItems.contains(typeName))
1234 return true;
1235 return false;
1236 });
1237
1238 if (!special)
1239 return {};
1240
1241 // Perform target name search if there is binding to property "target"
1242 QString targetName;
1243 if (scope->hasOwnPropertyBindings(u"target"_s)) {
1244 // TODO: propagate the whole binding.
1245 // We can figure out the meaning of target in more cases.
1246
1247 DomItem current = item.qmlObject();
1248 auto target = current.bindings().key(u"target"_s).index(0);
1249 if (target) {
1250 targetName = target.field(Fields::value)
1251 .field(Fields::scriptElement)
1252 .field(Fields::identifier)
1253 .value()
1254 .toString();
1255 }
1256 }
1257
1258 if (!targetName.isEmpty()) {
1259 // target: someId
1260 auto resolver = item.containingFile().ownerAs<QmlFile>()->typeResolver();
1261 if (!resolver)
1262 return {};
1263
1264 // Note: It does not have to be an ID. It can be a property.
1265 return resolver->scopedType(scope, targetName);
1266 }
1267
1268 if (item.internalKind() == DomType::Binding &&
1269 item.field(Fields::bindingType).value().toInteger() == int(BindingType::OnBinding)) {
1270 // Binding on sth : {} syntax
1271 // Target scope is the current scope
1272 return scope;
1273 }
1274 return scope->parentScope();
1275}
1276
1277/*!
1278\internal
1279\brief Distinguishes singleton types from attached types and "regular" qml components.
1280 */
1281static std::optional<ExpressionType>
1282resolveTypeName(const std::shared_ptr<QQmlJSTypeResolver> &resolver, const QString &name,
1283 const DomItem &item, ResolveOptions options)
1284{
1285 const auto scope = resolver->typeForName(name);
1286 if (!scope)
1287 return {};
1288
1289 if (scope->isSingleton())
1291
1292 // A type not followed by a field member expression is just a type. Otherwise, it could either
1293 // be a type or an attached type!
1294 if (!isFieldMemberBase(item))
1295 return ExpressionType{ name, scope, QmlComponentIdentifier };
1296
1297 // take the right hand side and unwrap in case its a nested fieldmemberexpression
1298 const DomItem rightHandSide = [&item]() {
1299 const DomItem candidate = item.directParent().field(Fields::right);
1300 // case B(a,b) for the right hand side of `a` in `a.b`
1301 if (candidate != item)
1302 return candidate;
1303 // case B(B(a,b),c) for the right hand side of `b` in `a.b.c`
1304 return item.directParent().directParent().field(Fields::right);
1305 }();
1306
1307 if (rightHandSide.internalKind() != DomType::ScriptIdentifierExpression)
1308 return ExpressionType{ name, scope, QmlComponentIdentifier };
1309
1310 const QString fieldMemberAccessName = rightHandSide.value().toString();
1311 if (fieldMemberAccessName.isEmpty() || !fieldMemberAccessName.front().isLower())
1312 return ExpressionType{ name, scope, QmlComponentIdentifier };
1313
1314 if (scope->attachedType()) {
1315 return ExpressionType{ name, options == ResolveOwnerType ? scope : scope->attachedType(),
1317 }
1318 return {};
1319}
1320
1321static std::optional<ExpressionType> resolveFieldMemberExpressionType(const DomItem &item,
1322 ResolveOptions options)
1323{
1324 const QString name = item.field(Fields::identifier).value().toString();
1325 DomItem parent = item.directParent();
1326 auto owner = resolveExpressionType(parent.field(Fields::left),
1327 ResolveOptions::ResolveActualTypeForFieldMemberExpression);
1328 if (!owner)
1329 return {};
1330
1331 if (!owner->semanticScope) {
1332 // JS objects can get new members and methods during runtime and therefore has no
1333 // qqmljsscopes. Therefore, just label everything inside a JavaScriptIdentifier as being
1334 // another JavaScriptIdentifier.
1335 if (owner->type == JavaScriptIdentifier) {
1336 return ExpressionType{ name, {}, JavaScriptIdentifier };
1337 } else if (owner->type == QualifiedModuleIdentifier) {
1338 const QmlFile *qmlFile = item.fileObject().as<QmlFile>();
1339 if (!qmlFile)
1340 return {};
1341 if (auto scope = resolveTypeName(
1342 qmlFile->typeResolver(), u"%1.%2"_s.arg(*owner->name, name), item,
1343 options)) {
1344 // remove the qualified module name from the type name
1345 scope->name = name;
1346 return scope;
1347 }
1348 }
1349 return {};
1350 }
1351
1352 if (auto scope = methodFromReferrerScope(owner->semanticScope, name, options))
1353 return *scope;
1354
1355 if (auto scope = propertyBindingFromReferrerScope(owner->semanticScope, name, options, nullptr))
1356 return *scope;
1357
1358 if (auto scope = propertyFromReferrerScope(owner->semanticScope, name, options))
1359 return *scope;
1360
1361 if (owner->type == QmlComponentIdentifier || owner->type == EnumeratorIdentifier) {
1362 // Check if name is a enum value <TypeName>.<EnumValue> or ...<EnumName>.<EnumValue>
1363 // Enumerations should live under the root element scope of the file that defines the enum,
1364 // therefore use the DomItem to find the root element of the qml file instead of directly
1365 // using owner->semanticScope.
1366 if (const auto scope = owner->semanticScope->isComposite()
1367 ? item.goToFile(owner->semanticScope->filePath())
1368 .rootQmlObject(GoTo::MostLikely)
1369 .semanticScope()
1370 : owner->semanticScope) {
1371 if (scope->hasEnumerationKey(name)) {
1372 return ExpressionType{ name, scope, EnumeratorValueIdentifier };
1373 }
1374 // Or it is a enum name <TypeName>.<EnumName>.<EnumValue>
1375 else if (scope->hasEnumeration(name)) {
1376 return ExpressionType{ name, scope, EnumeratorIdentifier };
1377 }
1378 }
1379
1380 // check inline components <TypeName>.<InlineComponentName>
1381 for (auto it = owner->semanticScope->childScopesBegin(),
1382 end = owner->semanticScope->childScopesEnd();
1383 it != end; ++it) {
1384 if ((*it)->inlineComponentName() == name) {
1385 return ExpressionType{ name, *it, QmlComponentIdentifier };
1386 }
1387 }
1388 return {};
1389 }
1390
1391 qCDebug(QQmlLSUtilsLog) << "Could not find identifier expression for" << item.internalKindStr();
1392 return owner;
1393}
1394
1395/*!
1396\internal
1397Resolves the expression type of a binding for signal handlers, like the function expression
1398\c{(x) => ...} in
1399
1400\qml
1401onHelloSignal: (x) => ...
1402\endqml
1403
1404would be resolved to the \c{onHelloSignal} expression type, for example.
1405*/
1406static std::optional<ExpressionType> resolveBindingIfSignalHandler(const DomItem &functionExpression)
1407{
1408 if (functionExpression.internalKind() != DomType::ScriptFunctionExpression)
1409 return {};
1410
1411 const DomItem parent = functionExpression.directParent();
1412 if (parent.internalKind() != DomType::ScriptExpression)
1413 return {};
1414
1415 const DomItem grandParent = parent.directParent();
1416 if (grandParent.internalKind() != DomType::Binding)
1417 return {};
1418
1419 auto bindingType = resolveExpressionType(grandParent, ResolveOwnerType);
1420 return bindingType;
1421}
1422
1423/*!
1424\internal
1425In a signal handler
1426
1427\qml
1428 onSomeSignal: (x, y, z) => ....
1429\endqml
1430
1431the parameters \c x, \c y and \c z are not allowed to have type annotations: instead, their type is
1432defined by the signal definition itself.
1433
1434This code detects signal handler parameters and resolves their type using the signal's definition.
1435*/
1436static std::optional<ExpressionType>
1437resolveSignalHandlerParameterType(const DomItem &parameterDefinition, const QString &name,
1438 ResolveOptions options)
1439{
1440 const std::optional<QQmlJSScope::JavaScriptIdentifier> jsIdentifier =
1441 parameterDefinition.semanticScope()->jsIdentifier(name);
1442 if (!jsIdentifier || jsIdentifier->kind != QQmlJSScope::JavaScriptIdentifier::Parameter)
1443 return {};
1444
1445 const DomItem handlerFunctionExpression =
1446 parameterDefinition.internalKind() == DomType::ScriptBlockStatement
1447 ? parameterDefinition.directParent()
1448 : parameterDefinition;
1449
1450 const std::optional<ExpressionType> bindingType =
1451 resolveBindingIfSignalHandler(handlerFunctionExpression);
1452 if (!bindingType)
1453 return {};
1454
1455 if (bindingType->type == PropertyChangedHandlerIdentifier)
1456 return ExpressionType{};
1457
1458 if (bindingType->type != SignalHandlerIdentifier)
1459 return {};
1460
1461 const DomItem parameters = handlerFunctionExpression[Fields::parameters];
1462 const int indexOfParameter = [&parameters, &name]() {
1463 for (int i = 0; i < parameters.indexes(); ++i) {
1464 if (parameters[i][Fields::identifier].value().toString() == name)
1465 return i;
1466 }
1467 Q_ASSERT_X(false, "resolveSignalHandlerParameter",
1468 "can't find JS identifier with Parameter kind in the parameters");
1469 Q_UNREACHABLE_RETURN(-1);
1470 }();
1471
1472 const std::optional<QString> signalName =
1473 QQmlSignalNames::handlerNameToSignalName(*bindingType->name);
1474 Q_ASSERT_X(signalName.has_value(), "resolveSignalHandlerParameterType",
1475 "handlerNameToSignalName failed on a SignalHandler");
1476
1477 const QQmlJSMetaMethod signalDefinition =
1478 bindingType->semanticScope->methods(*signalName).front();
1479 const QList<QQmlJSMetaParameter> parameterList = signalDefinition.parameters();
1480
1481 // not a signal handler parameter after all
1482 if (parameterList.size() <= indexOfParameter)
1483 return {};
1484
1485 // now we can return an ExpressionType, even if the indexOfParameter calculation result is only
1486 // needed to check whether this is a signal handler parameter or not.
1487 if (options == ResolveOwnerType)
1488 return ExpressionType{ name, bindingType->semanticScope, JavaScriptIdentifier };
1489 else {
1490 const QQmlJSScope::ConstPtr parameterType = parameterList[indexOfParameter].type();
1491 return ExpressionType{ name, parameterType, JavaScriptIdentifier };
1492 }
1493}
1494
1495/*!
1496\internal
1497resolve an identifier in the same order the QML engine would, that is:
1498* first check for local JS variables (`let x = ...`)
1499* then check for ids
1500* then check for properties
1501* then check for methods
1502* then check for properties and methods of the global object
1503 */
1504static std::optional<ExpressionType> resolveIdentifierExpressionType(const DomItem &item,
1505 ResolveOptions options)
1506{
1507 if (isFieldMemberAccess(item) || isFieldMemberExpression(item)) {
1508 return resolveFieldMemberExpressionType(item, options);
1509 }
1510
1511 const QString name = item.field(Fields::identifier).value().toString();
1512
1513 if (const DomItem definitionOfItem = findJSIdentifierDefinition(item, name)) {
1514 Q_ASSERT_X(!definitionOfItem.semanticScope().isNull()
1515 && definitionOfItem.semanticScope()->ownJSIdentifier(name),
1516 "QQmlLSUtils::findDefinitionOf",
1517 "JS definition does not actually define the JS identifer. "
1518 "It should be empty.");
1519 if (auto parameter = resolveSignalHandlerParameterType(definitionOfItem, name, options))
1520 return parameter;
1521
1522 const auto scope = definitionOfItem.semanticScope();
1523 return ExpressionType{ name,
1524 options == ResolveOwnerType
1525 ? scope
1526 : QQmlJSScope::ConstPtr(
1527 scope->ownJSIdentifier(name)->scope.toStrongRef()),
1528 IdentifierType::JavaScriptIdentifier };
1529 }
1530
1531 const auto referrerScope = item.nearestSemanticScope();
1532 if (!referrerScope)
1533 return {};
1534
1535 const auto qmlFile = item.containingFile().ownerAs<QmlFile>();
1536 if (!qmlFile)
1537 return {};
1538
1539 const auto resolver = qmlFile->typeResolver();
1540 if (!resolver)
1541 return {};
1542
1543 // check if its an id
1544 if (const QQmlJSScope::ConstPtr fromId =
1545 resolver->typeForId(referrerScope, name, QQmlJSScopesByIdOption::AssumeComponentsAreBound)) {
1546 return ExpressionType{ name, fromId, QmlObjectIdIdentifier };
1547 }
1548
1549 // check if its found as a property binding
1550 if (const auto scope = propertyBindingFromReferrerScope(
1551 referrerScope, name, options, resolver.get())) {
1552 return *scope;
1553 }
1554
1555 // check if its an (unqualified) property
1556 if (const auto scope = propertyFromReferrerScope(referrerScope, name, options))
1557 return *scope;
1558
1559 // check if its a method
1560 if (auto scope = methodFromReferrerScope(referrerScope, name, options))
1561 return scope;
1562
1563 if (resolver->seenModuleQualifiers().contains(name))
1564 return ExpressionType{ name, {}, QualifiedModuleIdentifier };
1565
1566 if (const auto scope = resolveTypeName(resolver, name, item, options))
1567 return scope;
1568
1569 // the global object doesn't have properties and methods with the same name, therefore the
1570 // order in which we resolve methods and properties on the global object doesn't matter
1571 const QQmlJSScope::ConstPtr jsGlobal = resolver->jsGlobalObject();
1572 // check if its a JS global method
1573 if (auto scope = methodFromReferrerScope(jsGlobal, name, options)) {
1574 scope->type = JavaScriptIdentifier;
1575 return scope;
1576 }
1577
1578 // check if its an JS global property
1579 if (auto scope = propertyFromReferrerScope(jsGlobal, name, options)) {
1580 scope->type = JavaScriptIdentifier;
1581 return scope;
1582 }
1583
1584 return {};
1585}
1586
1587static std::optional<ExpressionType>
1588resolveSignalOrPropertyExpressionType(const QString &name, const QQmlJSScope::ConstPtr &scope,
1589 ResolveOptions options)
1590{
1591 const auto signalOrProperty = resolveNameInQmlScope(name, scope);
1592 if (!signalOrProperty)
1593 return {};
1594
1595 switch (signalOrProperty->type) {
1596 case PropertyIdentifier:
1597 switch (options) {
1598 case ResolveOwnerType:
1599 return ExpressionType{ name, findDefiningScopeForProperty(scope, name),
1600 signalOrProperty->type };
1602 return ExpressionType{ name, scope->property(name).type(), signalOrProperty->type };
1603 }
1604 Q_UNREACHABLE_RETURN({});
1606 switch (options) {
1607 case ResolveOwnerType:
1608 return ExpressionType{ name,
1609 findDefiningScopeForProperty(scope, signalOrProperty->name),
1610 signalOrProperty->type };
1612 // Properties and methods are not implemented on methods.
1613 Q_UNREACHABLE_RETURN({});
1614 }
1615 Q_UNREACHABLE_RETURN({});
1618 case SignalIdentifier:
1619 case MethodIdentifier:
1620 switch (options) {
1621 case ResolveOwnerType: {
1622 return ExpressionType{ name, findDefiningScopeForMethod(scope, signalOrProperty->name),
1623 signalOrProperty->type };
1624 }
1626 // Properties and methods are not implemented on methods.
1627 Q_UNREACHABLE_RETURN({});
1628 }
1629 Q_UNREACHABLE_RETURN({});
1630 default:
1631 Q_UNREACHABLE_RETURN({});
1632 }
1633}
1634
1635/*!
1636 \internal
1637 \brief
1638 Resolves the type of the given DomItem, when possible (e.g., when there are enough type
1639 annotations).
1640
1641 Might return an ExpressionType without(!) semantic scope when no type information is available, for
1642 example resolving the type of x in someJSObject.x (where `let someJSObject = { x: 42 };`) then
1643 the name and type of x is known but no semantic scope can be obtained.
1644*/
1645std::optional<ExpressionType> resolveExpressionType(const QQmlJS::Dom::DomItem &item,
1646 ResolveOptions options)
1647{
1648 switch (item.internalKind()) {
1649 case DomType::ScriptIdentifierExpression: {
1650 return resolveIdentifierExpressionType(item, options);
1651 }
1652 case DomType::PropertyDefinition: {
1653 auto propertyDefinition = item.as<PropertyDefinition>();
1654 if (propertyDefinition && propertyDefinition->semanticScope()) {
1655 const auto &scope = propertyDefinition->semanticScope();
1656 switch (options) {
1657 case ResolveOwnerType:
1658 return ExpressionType{ propertyDefinition->name, scope, PropertyIdentifier };
1660 // There should not be any PropertyDefinition inside a FieldMemberExpression.
1661 Q_UNREACHABLE_RETURN({});
1662 }
1663 Q_UNREACHABLE_RETURN({});
1664 }
1665 return {};
1666 }
1667 case DomType::Binding: {
1668 auto binding = item.as<Binding>();
1669 if (binding) {
1670 std::optional<QQmlJSScope::ConstPtr> owner = item.qmlObject().semanticScope();
1671 if (!owner)
1672 return {};
1673 const QString name = binding->name();
1674
1675 if (name == u"id")
1676 return ExpressionType{ name, owner.value(), QmlObjectIdIdentifier };
1677
1678 if (const QQmlJSScope::ConstPtr targetScope
1679 = findScopeOfSpecialItems(owner.value(), item)) {
1680 const auto signalOrProperty = resolveNameInQmlScope(name, targetScope);
1681 if (!signalOrProperty)
1682 return {};
1683 switch (options) {
1684 case ResolveOwnerType:
1685 return ExpressionType{
1686 name, findDefiningScopeForBinding(targetScope, signalOrProperty->name),
1687 signalOrProperty->type
1688 };
1690 // Bindings can't be inside of FieldMemberExpressions.
1691 Q_UNREACHABLE_RETURN({});
1692 }
1693 }
1694 if (auto result = resolveSignalOrPropertyExpressionType(name, owner.value(), options)) {
1695 return result;
1696 }
1697 qDebug(QQmlLSUtilsLog) << "QQmlLSUtils::resolveExpressionType() could not resolve the"
1698 "type of a Binding.";
1699 }
1700
1701 return {};
1702 }
1703 case DomType::QmlObject: {
1704 auto object = item.as<QmlObject>();
1705 if (!object)
1706 return {};
1707 if (auto scope = object->semanticScope()) {
1708 const auto name = item.name();
1709 const bool isComponent = name.front().isUpper();
1710 if (isComponent)
1711 scope = scope->baseType();
1712 const IdentifierType type =
1714 switch (options) {
1715 case ResolveOwnerType:
1716 return ExpressionType{ name, scope, type };
1718 return ExpressionType{ name, scope, type };
1719 }
1720 }
1721 return {};
1722 }
1723 case DomType::QmlComponent: {
1724 auto component = item.as<QmlComponent>();
1725 if (!component)
1726 return {};
1727 const auto scope = component->semanticScope();
1728 if (!scope)
1729 return {};
1730
1731 QString name = item.name();
1732 if (auto dotIndex = name.indexOf(u'.'); dotIndex != -1)
1733 name = name.sliced(dotIndex + 1);
1734 switch (options) {
1735 case ResolveOwnerType:
1736 return ExpressionType{ name, scope, QmlComponentIdentifier };
1738 return ExpressionType{ name, scope, QmlComponentIdentifier };
1739 }
1740 Q_UNREACHABLE_RETURN({});
1741 }
1742 case DomType::ScriptFunctionExpression: {
1743 return ExpressionType{ {}, item.semanticScope(), LambdaMethodIdentifier };
1744 }
1745 case DomType::MethodInfo: {
1746 const auto object = item.as<MethodInfo>();
1747 if (object && object->semanticScope()) {
1748 std::optional<QQmlJSScope::ConstPtr> scope = object->semanticScope();
1749 if (!scope)
1750 return {};
1751
1752 if (const QQmlJSScope::ConstPtr targetScope =
1753 findScopeOfSpecialItems(scope.value()->parentScope(), item)) {
1754 const auto signalOrProperty = resolveNameInQmlScope(object->name, targetScope);
1755 if (!signalOrProperty)
1756 return {};
1757
1758 switch (options) {
1759 case ResolveOwnerType:
1760 return ExpressionType{ object->name,
1761 findDefiningScopeForMethod(targetScope,
1762 signalOrProperty->name),
1763 signalOrProperty->type };
1765 // not supported for methods
1766 return {};
1767 }
1768 }
1769
1770 // in case scope is the semantic scope for the function bodies: grab the owner's scope
1771 // this happens for all methods but not for signals (they do not have a body)
1772 if (QQmlSA::isFunctionScope(scope.value()->scopeType()))
1773 scope = scope.value()->parentScope();
1774
1775 if (const auto result = resolveSignalOrPropertyExpressionType(
1776 object->name, scope.value(), options)) {
1777 return result;
1778 }
1779 qDebug(QQmlLSUtilsLog) << "QQmlLSUtils::resolveExpressionType() could not resolve the"
1780 "type of a MethodInfo.";
1781 }
1782
1783 return {};
1784 }
1785 case DomType::ScriptBinaryExpression: {
1786 if (isFieldMemberExpression(item)) {
1787 return resolveExpressionType(item.field(Fields::right), options);
1788 }
1789 return {};
1790 }
1791 case DomType::ScriptLiteral: {
1792 /* special case
1793 Binding { target: someId; property: "someProperty"}
1794 */
1795 const auto scope = item.qmlObject().semanticScope();
1796 const auto name = item.field(Fields::value).value().toString();
1797 if (const QQmlJSScope::ConstPtr targetScope = findScopeOfSpecialItems(scope, item)) {
1798 const auto signalOrProperty = resolveNameInQmlScope(name, targetScope);
1799 if (!signalOrProperty)
1800 return {};
1801 switch (options) {
1802 case ResolveOwnerType:
1803 return ExpressionType{
1804 name, findDefiningScopeForProperty(targetScope, signalOrProperty->name),
1805 signalOrProperty->type
1806 };
1808 // ScriptLiteral's can't be inside of FieldMemberExpression's, especially when they
1809 // are inside a special item.
1810 Q_UNREACHABLE_RETURN({});
1811 }
1812 }
1813 return {};
1814 }
1815 case DomType::EnumItem: {
1816 const QString enumValue = item.field(Fields::name).value().toString();
1817 const QQmlJSScope::ConstPtr referrerScope
1818 = item.rootQmlObject(GoTo::MostLikely).semanticScope();
1819 if (!referrerScope->hasEnumerationKey(enumValue))
1820 return {};
1821 switch (options) {
1822 // special case: use the owner's scope here, as enums do not have their own
1823 // QQmlJSScope.
1824 case ResolveActualTypeForFieldMemberExpression:
1825 case ResolveOwnerType:
1826 return ExpressionType {
1827 enumValue,
1828 findDefiningScopeForEnumerationKey(referrerScope, enumValue),
1829 EnumeratorValueIdentifier
1830 };
1831 }
1832 Q_UNREACHABLE_RETURN({});
1833 }
1834 case DomType::EnumDecl: {
1835 const QString enumName = item.field(Fields::name).value().toString();
1836 const QQmlJSScope::ConstPtr referrerScope
1837 = item.rootQmlObject(GoTo::MostLikely).semanticScope();
1838 if (!referrerScope->hasEnumeration(enumName))
1839 return {};
1840 switch (options) {
1841 // special case: use the owner's scope here, as enums do not have their own QQmlJSScope.
1842 case ResolveActualTypeForFieldMemberExpression:
1843 case ResolveOwnerType:
1844 return ExpressionType {
1845 enumName,
1846 findDefiningScopeForEnumeration(referrerScope, enumName),
1847 EnumeratorIdentifier
1848 };
1849 }
1850
1851 Q_UNREACHABLE_RETURN({});
1852 }
1853 case DomType::Import: {
1854 // we currently only support qualified module identifiers
1855 if (!item[Fields::importId])
1856 return {};
1857 return ExpressionType{ item[Fields::importId].value().toString(),
1858 {},
1859 QualifiedModuleIdentifier };
1860 }
1861 case DomType::ScriptNewMemberExpression: {
1862 const auto name = item.field(Fields::base).value().toString();
1863 return ExpressionType{ name, {}, JavaScriptIdentifier };
1864 }
1865 case DomType::ScriptCallExpression: {
1866 const DomItem callee = item.field(Fields::callee);
1867 const auto calleeExpressionType = resolveExpressionType(callee, ResolveOwnerType);
1868
1869 if (!calleeExpressionType || !calleeExpressionType->semanticScope
1870 || !calleeExpressionType->name || calleeExpressionType->type != MethodIdentifier) {
1871 return {};
1872 }
1873
1874 const auto methods =
1875 calleeExpressionType->semanticScope->methods(*calleeExpressionType->name);
1876 if (methods.isEmpty())
1877 return {};
1878
1879 const auto returnType = methods.front().returnType();
1880 return ExpressionType{ {}, returnType, NotAnIdentifier };
1881 }
1882 default: {
1883 qCDebug(QQmlLSUtilsLog) << "Type" << item.internalKindStr()
1884 << "is unimplemented in QQmlLSUtils::resolveExpressionType";
1885 return {};
1886 }
1887 }
1888 Q_UNREACHABLE();
1889}
1890
1891DomItem sourceLocationToDomItem(const DomItem &file, const QQmlJS::SourceLocation &location)
1892{
1893 // QQmlJS::SourceLocation starts counting at 1 but the utils and the LSP start at 0.
1894 auto items = QQmlLSUtils::itemsFromTextLocation(file, location.startLine - 1,
1895 location.startColumn - 1);
1896 switch (items.size()) {
1897 case 0:
1898 return {};
1899 case 1:
1900 return items.front().domItem;
1901 case 2: {
1902 // special case: because location points to the beginning of the type definition,
1903 // itemsFromTextLocation might also return the type on its left, in case it is directly
1904 // adjacent to it. In this case always take the right (=with the higher column-number)
1905 // item.
1906 auto &first = items.front();
1907 auto &second = items.back();
1908 Q_ASSERT_X(first.fileLocation->info().fullRegion.startLine
1909 == second.fileLocation->info().fullRegion.startLine,
1910 "QQmlLSUtils::findTypeDefinitionOf(DomItem)",
1911 "QQmlLSUtils::itemsFromTextLocation returned non-adjacent items.");
1912 if (first.fileLocation->info().fullRegion.startColumn
1913 > second.fileLocation->info().fullRegion.startColumn)
1914 return first.domItem;
1915 else
1916 return second.domItem;
1917 break;
1918 }
1919 default:
1920 qDebug() << "Found multiple candidates for type of scriptidentifierexpression";
1921 break;
1922 }
1923 return {};
1924}
1925
1926static std::optional<Location>
1927findEnumDefinitionOf(const DomItem &file, QQmlJS::SourceLocation location, const QString &name)
1928{
1929 const DomItem enumeration = [&file, &location, &name]() -> DomItem {
1930 const DomItem enumerations = QQmlLSUtils::sourceLocationToDomItem(file, location)
1931 .qmlObject()
1932 .component()
1933 .field(Fields::enumerations);
1934 const QSet<QString> enumerationNames = enumerations.keys();
1935 for (const QString &enumName : enumerationNames) {
1936 const DomItem currentKey = enumerations.key(enumName).index(0);
1937 if (enumName == name)
1938 return currentKey;
1939 const DomItem values = currentKey.field(Fields::values);
1940 for (int i = 0, end = values.size(); i < end; ++i) {
1941 const DomItem currentValue = values.index(i);
1942 if (currentValue.field(Fields::name).value().toStringView() == name)
1943 return currentValue;
1944 }
1945 }
1946 return {};
1947 }();
1948
1949 auto fileLocation = FileLocations::treeOf(enumeration);
1950
1951 if (!fileLocation)
1952 return {};
1953
1954 auto regions = fileLocation->info().regions;
1955
1956 if (auto it = regions.constFind(IdentifierRegion); it != regions.constEnd()) {
1957 return Location::tryFrom(enumeration.canonicalFilePath(), *it, file);
1958 }
1959
1960 return {};
1961}
1962
1963static std::optional<Location>
1964findMethodDefinitionOf(const DomItem &file, QQmlJS::SourceLocation location, const QString &name)
1965{
1966 DomItem owner = QQmlLSUtils::sourceLocationToDomItem(file, location).qmlObject();
1967 DomItem method = owner.field(Fields::methods).key(name).index(0);
1968 auto fileLocation = FileLocations::treeOf(method);
1969 if (!fileLocation)
1970 return {};
1971
1972 auto regions = fileLocation->info().regions;
1973
1974 if (auto it = regions.constFind(IdentifierRegion); it != regions.constEnd()) {
1975 return Location::tryFrom(method.canonicalFilePath(), *it, file);
1976 }
1977
1978 return {};
1979}
1980
1981static std::optional<Location>
1982findPropertyDefinitionOf(const DomItem &file, QQmlJS::SourceLocation propertyDefinitionLocation,
1983 const QString &name)
1984{
1985 DomItem propertyOwner =
1986 QQmlLSUtils::sourceLocationToDomItem(file, propertyDefinitionLocation).qmlObject();
1987 DomItem propertyDefinition = propertyOwner.field(Fields::propertyDefs).key(name).index(0);
1988 auto fileLocation = FileLocations::treeOf(propertyDefinition);
1989 if (!fileLocation)
1990 return {};
1991
1992 auto regions = fileLocation->info().regions;
1993
1994 if (auto it = regions.constFind(IdentifierRegion); it != regions.constEnd()) {
1995 return Location::tryFrom(propertyDefinition.canonicalFilePath(), *it, file);
1996 }
1997
1998 return {};
1999}
2000
2001static QQmlJS::SourceLocation sourceLocationOrDefault(const QQmlJS::SourceLocation &location)
2002{
2003 return location.startLine == 0 ? QQmlJS::s_documentOrigin : location;
2004}
2005
2006static std::optional<Location> createCppTypeLocation(const QString &fileName,
2007 const QStringList &headerLocations,
2008 const QQmlJS::SourceLocation &location)
2009{
2010 const TextPosition endPosition{ static_cast<int>(location.startLine) + 1, 1 };
2011 const QFileInfo filePathInfo(fileName);
2012 if (filePathInfo.isAbsolute() && filePathInfo.exists())
2013 return Location{ fileName, location, endPosition };
2014
2015 const QString filePath = findFilePathFromFileName(headerLocations, fileName, { });
2016 if (filePath.isEmpty()) {
2017 qCWarning(QQmlLSUtilsLog) << "Couldn't find file '%1'."_L1.arg(fileName);
2018 return {};
2019 }
2020
2021 return Location{ filePath, location, endPosition };
2022}
2023
2024/*!
2025\internal
2026Note that C++ types with foreign can have 2 locations for the definition: one where the
2027QML(_NAMED|)_ELEMENT macro is used and the second is the type targeted by QML_FOREIGN.
2028*/
2029static QList<Location> findDefinitionOfType(const QQmlJSScope::ConstPtr &scope, const DomItem &item,
2030 const QStringList &headerDirectories)
2031{
2032 if (!scope)
2033 return {};
2034 if (scope->isComposite()) {
2035 if (const auto result =
2036 Location::tryFrom(scope->filePath(), scope->sourceLocation(), item)) {
2037 return { *result };
2038 }
2039 }
2040 QList<Location> result;
2041 auto locationOfTypeOrForeign = createCppTypeLocation(
2042 scope->filePath(), headerDirectories, sourceLocationOrDefault(scope->sourceLocation()));
2043 if (locationOfTypeOrForeign)
2044 result << *locationOfTypeOrForeign;
2045
2046 auto resolvedTypeLocation = createCppTypeLocation(
2047 scope->resolvedFilePath(), headerDirectories,
2048 QQmlJS::SourceLocation{ 0u, 0u, scope->lineNumberInResolvedFile(), 1u });
2049 if (resolvedTypeLocation)
2050 result << *resolvedTypeLocation;
2051
2052 return result;
2053}
2054
2055template <typename T>
2056QList<T> optionalToList(std::optional<T> &&optional)
2057{
2058 if (optional)
2059 return { *optional };
2060 return { };
2061}
2062
2063QList<Location> findDefinitionOf(const DomItem &item, const QStringList &headerDirectories)
2064{
2065 auto resolvedExpression = resolveExpressionType(item, ResolveOptions::ResolveOwnerType);
2066
2067 if (!resolvedExpression || !resolvedExpression->name
2068 || (!resolvedExpression->semanticScope
2069 && resolvedExpression->type != QualifiedModuleIdentifier)) {
2070 qCDebug(QQmlLSUtilsLog) << "QQmlLSUtils::findDefinitionOf: Type could not be resolved.";
2071 return {};
2072 }
2073
2074 switch (resolvedExpression->type) {
2075 case JavaScriptIdentifier: {
2076 const auto jsIdentifier =
2077 resolvedExpression->semanticScope->ownJSIdentifier(*resolvedExpression->name);
2078 if (!jsIdentifier)
2079 return {};
2080
2081 return optionalToList(Location::tryFrom(resolvedExpression->semanticScope->filePath(),
2082 jsIdentifier->location, item));
2083 }
2084
2086 case PropertyIdentifier: {
2087 if (!resolvedExpression->semanticScope->isComposite()) {
2088 return optionalToList(createCppTypeLocation(
2089 resolvedExpression->semanticScope->filePath(), headerDirectories,
2090 resolvedExpression->semanticScope->property(*resolvedExpression->name)
2091 .sourceLocation()));
2092 }
2093 const DomItem ownerFile = item.goToFile(resolvedExpression->semanticScope->filePath());
2094 const QQmlJS::SourceLocation ownerLocation =
2095 resolvedExpression->semanticScope->sourceLocation();
2096 return optionalToList(
2097 findPropertyDefinitionOf(ownerFile, ownerLocation, *resolvedExpression->name));
2098 }
2101 case SignalIdentifier:
2103 case MethodIdentifier: {
2104 if (!resolvedExpression->semanticScope->isComposite()) {
2105 const auto methods =
2106 resolvedExpression->semanticScope->methods(*resolvedExpression->name);
2107 if (methods.isEmpty())
2108 return {};
2109 return optionalToList(
2110 createCppTypeLocation(resolvedExpression->semanticScope->filePath(),
2111 headerDirectories, methods.front().sourceLocation()));
2112 }
2113 const DomItem ownerFile = item.goToFile(resolvedExpression->semanticScope->filePath());
2114 const QQmlJS::SourceLocation ownerLocation =
2115 resolvedExpression->semanticScope->sourceLocation();
2116 return optionalToList(
2117 findMethodDefinitionOf(ownerFile, ownerLocation, *resolvedExpression->name));
2118 }
2119 case QmlObjectIdIdentifier: {
2120 DomItem qmlObject = QQmlLSUtils::sourceLocationToDomItem(
2121 item.containingFile(), resolvedExpression->semanticScope->sourceLocation());
2122 // in the Dom, the id is saved in a QMultiHash inside the Component of an QmlObject.
2123 const DomItem domId = qmlObject.component()
2124 .field(Fields::ids)
2125 .key(*resolvedExpression->name)
2126 .index(0)
2127 .field(Fields::value);
2128 if (!domId) {
2129 qCDebug(QQmlLSUtilsLog)
2130 << "QmlComponent in Dom structure has no id, was it misconstructed?";
2131 return {};
2132 }
2133
2134 return optionalToList(Location::tryFrom(
2135 domId.canonicalFilePath(), FileLocations::treeOf(domId)->info().fullRegion, domId));
2136 }
2137 case AttachedTypeIdentifier:
2138 return findDefinitionOfType(resolvedExpression->semanticScope->attachedType(), item,
2139 headerDirectories);
2140 case AttachedTypeIdentifierInBindingTarget:
2141 return findDefinitionOfType(resolvedExpression->semanticScope->baseType(), item,
2142 headerDirectories);
2143 case QmlComponentIdentifier:
2144 case SingletonIdentifier:
2145 return findDefinitionOfType(resolvedExpression->semanticScope, item, headerDirectories);
2147 const DomItem imports = item.fileObject().field(Fields::imports);
2148 for (int i = 0; i < imports.indexes(); ++i) {
2149 if (imports[i][Fields::importId].value().toString() == resolvedExpression->name) {
2150 const auto fileLocations = FileLocations::treeOf(imports[i]);
2151 if (!fileLocations)
2152 continue;
2153
2154 return optionalToList(Location::tryFrom(item.canonicalFilePath(),
2155 fileLocations->info().regions[IdNameRegion],
2156 item));
2157 }
2158 }
2159 return {};
2160 }
2163 if (!resolvedExpression->semanticScope->isComposite()) {
2164 const auto enumerations = resolvedExpression->semanticScope->enumerations();
2165 for (const auto &enumeration : enumerations) {
2166 if (enumeration.hasKey(*resolvedExpression->name)
2167 || enumeration.name() == *resolvedExpression->name) {
2168 return optionalToList(createCppTypeLocation(
2169 resolvedExpression->semanticScope->filePath(), headerDirectories,
2170 sourceLocationOrDefault(QQmlJS::SourceLocation::fromQSizeType(
2171 0, 0, enumeration.lineNumber(), 1))));
2172 }
2173 }
2174 return {};
2175 }
2176 const DomItem ownerFile = item.goToFile(resolvedExpression->semanticScope->filePath());
2177 const QQmlJS::SourceLocation ownerLocation =
2178 resolvedExpression->semanticScope->sourceLocation();
2179 return optionalToList(
2180 findEnumDefinitionOf(ownerFile, ownerLocation, *resolvedExpression->name));
2181 }
2182
2184 case NotAnIdentifier:
2185 qCDebug(QQmlLSUtilsLog) << "QQmlLSUtils::findDefinitionOf was not implemented for type"
2186 << resolvedExpression->type;
2187 return {};
2188 }
2189 Q_UNREACHABLE_RETURN({});
2190}
2191
2192static QQmlJSScope::ConstPtr propertyOwnerFrom(const QQmlJSScope::ConstPtr &type,
2193 const QString &name)
2194{
2195 Q_ASSERT(!name.isEmpty());
2196 Q_ASSERT(type);
2197
2198 QQmlJSScope::ConstPtr typeWithDefinition = type;
2199 while (typeWithDefinition && !typeWithDefinition->hasOwnProperty(name))
2200 typeWithDefinition = typeWithDefinition->baseType();
2201
2202 if (!typeWithDefinition) {
2203 qCDebug(QQmlLSUtilsLog)
2204 << "QQmlLSUtils::checkNameForRename cannot find property definition,"
2205 " ignoring.";
2206 }
2207
2208 return typeWithDefinition;
2209}
2210
2211static QQmlJSScope::ConstPtr methodOwnerFrom(const QQmlJSScope::ConstPtr &type,
2212 const QString &name)
2213{
2214 Q_ASSERT(!name.isEmpty());
2215 Q_ASSERT(type);
2216
2217 QQmlJSScope::ConstPtr typeWithDefinition = type;
2218 while (typeWithDefinition && !typeWithDefinition->hasOwnMethod(name))
2219 typeWithDefinition = typeWithDefinition->baseType();
2220
2221 if (!typeWithDefinition) {
2222 qCDebug(QQmlLSUtilsLog)
2223 << "QQmlLSUtils::checkNameForRename cannot find method definition,"
2224 " ignoring.";
2225 }
2226
2227 return typeWithDefinition;
2228}
2229
2231{
2232 switch (ownerType.type) {
2233 case PropertyIdentifier:
2234 return propertyOwnerFrom(ownerType.semanticScope, *ownerType.name);
2236 const auto propertyName =
2237 QQmlSignalNames::changedHandlerNameToPropertyName(*ownerType.name);
2238 return propertyOwnerFrom(ownerType.semanticScope, *propertyName);
2239 break;
2240 }
2242 const auto propertyName = QQmlSignalNames::changedSignalNameToPropertyName(*ownerType.name);
2243 return propertyOwnerFrom(ownerType.semanticScope, *propertyName);
2244 }
2245 case MethodIdentifier:
2246 case SignalIdentifier:
2247 return methodOwnerFrom(ownerType.semanticScope, *ownerType.name);
2249 const auto signalName = QQmlSignalNames::handlerNameToSignalName(*ownerType.name);
2250 return methodOwnerFrom(ownerType.semanticScope, *signalName);
2251 }
2263 case NotAnIdentifier:
2264 return ownerType.semanticScope;
2265 }
2266 return {};
2267}
2268
2269std::optional<ErrorMessage> checkNameForRename(const DomItem &item, const QString &dirtyNewName,
2270 const std::optional<ExpressionType> &ownerType)
2271{
2272 if (!ownerType) {
2273 if (const auto resolved = resolveExpressionType(item, ResolveOwnerType))
2274 return checkNameForRename(item, dirtyNewName, resolved);
2275 }
2276
2277 // general checks for ECMAscript identifiers
2278 if (!isValidEcmaScriptIdentifier(dirtyNewName))
2279 return ErrorMessage{ 0, u"Invalid EcmaScript identifier!"_s };
2280
2281 const auto userSemanticScope = item.nearestSemanticScope();
2282
2283 if (!ownerType || !userSemanticScope) {
2284 return ErrorMessage{ 0, u"Requested item cannot be renamed."_s };
2285 }
2286
2287 // type specific checks
2288 switch (ownerType->type) {
2290 if (!QQmlSignalNames::isChangedSignalName(dirtyNewName)) {
2291 return ErrorMessage{ 0, u"Invalid name for a property changed signal."_s };
2292 }
2293 break;
2294 }
2296 if (!QQmlSignalNames::isChangedHandlerName(dirtyNewName)) {
2297 return ErrorMessage{ 0, u"Invalid name for a property changed handler identifier."_s };
2298 }
2299 break;
2300 }
2302 if (!QQmlSignalNames::isHandlerName(dirtyNewName)) {
2303 return ErrorMessage{ 0, u"Invalid name for a signal handler identifier."_s };
2304 }
2305 break;
2306 }
2307 // TODO: any other specificities?
2309 if (dirtyNewName.front().isLetter() && !dirtyNewName.front().isLower()) {
2310 return ErrorMessage{ 0, u"Object id names cannot start with an upper case letter."_s };
2311 }
2312 break;
2314 case PropertyIdentifier:
2315 case SignalIdentifier:
2316 case MethodIdentifier:
2317 default:
2318 break;
2319 };
2320
2321 auto typeWithDefinition = expressionTypeWithDefinition(*ownerType);
2322
2323 if (!typeWithDefinition) {
2324 return ErrorMessage{
2325 0,
2326 u"Renaming has not been implemented for the requested item."_s,
2327 };
2328 }
2329
2330 // is it not defined in QML?
2331 if (!typeWithDefinition->isComposite()) {
2332 return ErrorMessage{ 0, u"Cannot rename items defined in non-QML files."_s };
2333 }
2334
2335 // is it defined in the current module?
2336 const QString moduleOfDefinition = ownerType->semanticScope->moduleName();
2337 const QString moduleOfCurrentItem = userSemanticScope->moduleName();
2338 if (moduleOfDefinition != moduleOfCurrentItem) {
2339 return ErrorMessage{
2340 0,
2341 u"Cannot rename items defined in the \"%1\" module from a usage in the \"%2\" module."_s
2342 .arg(moduleOfDefinition, moduleOfCurrentItem),
2343 };
2344 }
2345
2346 return {};
2347}
2348
2349static std::optional<QString> oldNameFrom(const DomItem &item)
2350{
2351 switch (item.internalKind()) {
2352 case DomType::ScriptIdentifierExpression: {
2353 return item.field(Fields::identifier).value().toString();
2354 }
2355 case DomType::ScriptVariableDeclarationEntry: {
2356 return item.field(Fields::identifier).value().toString();
2357 }
2358 case DomType::PropertyDefinition:
2359 case DomType::Binding:
2360 case DomType::MethodInfo: {
2361 return item.field(Fields::name).value().toString();
2362 }
2363 default:
2364 qCDebug(QQmlLSUtilsLog) << item.internalKindStr()
2365 << "was not implemented for QQmlLSUtils::renameUsagesOf";
2366 return std::nullopt;
2367 }
2368 Q_UNREACHABLE_RETURN(std::nullopt);
2369}
2370
2371static std::optional<QString> newNameFrom(const QString &dirtyNewName, IdentifierType alternative)
2372{
2373 // When renaming signal/property changed handlers and property changed signals:
2374 // Get the actual corresponding signal name (for signal handlers) or property name (for
2375 // property changed signal + handlers) that will be used for the renaming.
2376 switch (alternative) {
2378 return QQmlSignalNames::handlerNameToSignalName(dirtyNewName);
2379 }
2381 return QQmlSignalNames::changedHandlerNameToPropertyName(dirtyNewName);
2382 }
2384 return QQmlSignalNames::changedSignalNameToPropertyName(dirtyNewName);
2385 }
2386 case SignalIdentifier:
2387 case PropertyIdentifier:
2388 default:
2389 return std::nullopt;
2390 }
2391 Q_UNREACHABLE_RETURN(std::nullopt);
2392}
2393
2394/*!
2395\internal
2396\brief Rename the appearance of item to newName.
2397
2398Special cases:
2399\list
2400 \li Renaming a property changed signal or property changed handler does the same as renaming
2401 the underlying property, except that newName gets
2402 \list
2403 \li its "on"-prefix and "Changed"-suffix chopped of if item is a property changed handlers
2404 \li its "Changed"-suffix chopped of if item is a property changed signals
2405 \endlist
2406 \li Renaming a signal handler does the same as renaming a signal, but the "on"-prefix in newName
2407 is chopped of.
2408
2409 All of the chopping operations are done using the static helpers from QQmlSignalNames.
2410\endlist
2411*/
2412RenameUsages renameUsagesOf(const DomItem &item, const QString &dirtyNewName,
2413 const std::optional<ExpressionType> &targetType)
2414{
2415 RenameUsages result;
2416 const Usages locations = findUsagesOf(item);
2417 if (locations.isEmpty())
2418 return result;
2419
2420 auto oldName = oldNameFrom(item);
2421 if (!oldName)
2422 return result;
2423
2424 QQmlJSScope::ConstPtr semanticScope;
2425 if (targetType) {
2426 semanticScope = targetType->semanticScope;
2427 } else if (const auto resolved =
2428 QQmlLSUtils::resolveExpressionType(item, ResolveOptions::ResolveOwnerType)) {
2429 semanticScope = resolved->semanticScope;
2430 } else {
2431 return result;
2432 }
2433
2434 QString newName;
2435 if (const auto resolved = resolveNameInQmlScope(*oldName, semanticScope)) {
2436 newName = newNameFrom(dirtyNewName, resolved->type).value_or(dirtyNewName);
2437 oldName = resolved->name;
2438 } else {
2439 newName = dirtyNewName;
2440 }
2441
2442 const qsizetype oldNameLength = oldName->length();
2443 const qsizetype oldHandlerNameLength =
2444 QQmlSignalNames::signalNameToHandlerName(*oldName).length();
2445 const qsizetype oldChangedSignalNameLength =
2446 QQmlSignalNames::propertyNameToChangedSignalName(*oldName).length();
2447 const qsizetype oldChangedHandlerNameLength =
2448 QQmlSignalNames::propertyNameToChangedHandlerName(*oldName).length();
2449
2450 const QString newHandlerName = QQmlSignalNames::signalNameToHandlerName(newName);
2451 const QString newChangedSignalName = QQmlSignalNames::propertyNameToChangedSignalName(newName);
2452 const QString newChangedHandlerName =
2453 QQmlSignalNames::propertyNameToChangedHandlerName(newName);
2454
2455 // set the new name at the found usages, but add "on"-prefix and "Changed"-suffix if needed
2456 for (const auto &location : locations.usagesInFile()) {
2457 const qsizetype currentLength = location.sourceLocation().length;
2458 Edit edit;
2459 edit.location = location;
2460 if (oldNameLength == currentLength) {
2461 // normal case, nothing to do
2462 edit.replacement = newName;
2463
2464 } else if (oldHandlerNameLength == currentLength) {
2465 // signal handler location
2466 edit.replacement = newHandlerName;
2467
2468 } else if (oldChangedSignalNameLength == currentLength) {
2469 // property changed signal location
2470 edit.replacement = newChangedSignalName;
2471
2472 } else if (oldChangedHandlerNameLength == currentLength) {
2473 // property changed handler location
2474 edit.replacement = newChangedHandlerName;
2475
2476 } else {
2477 qCDebug(QQmlLSUtilsLog) << "Found usage with wrong identifier length, ignoring...";
2478 continue;
2479 }
2480 result.appendRename(edit);
2481 }
2482
2483 for (const auto &filename : locations.usagesInFilename()) {
2484 // assumption: we only rename files ending in .qml or .ui.qml in qmlls
2485 QString extension;
2486 if (filename.endsWith(u".ui.qml"_s))
2487 extension = u".ui.qml"_s;
2488 else if (filename.endsWith(u".qml"_s))
2489 extension = u".qml"_s;
2490 else
2491 continue;
2492
2493 QFileInfo info(filename);
2494 // do not rename the file if it has a custom type name in the qmldir
2495 if (!info.isFile() || info.baseName() != oldName)
2496 continue;
2497
2498 const QString newFilename =
2499 QDir::cleanPath(filename + "/.."_L1) + '/'_L1 + newName + extension;
2500 result.appendRename({ filename, newFilename });
2501 }
2502
2503 return result;
2504}
2505
2506std::optional<Location> Location::tryFrom(const QString &fileName,
2507 const QQmlJS::SourceLocation &sourceLocation,
2508 const QQmlJS::Dom::DomItem &someItem)
2509{
2510 auto qmlFile = someItem.goToFile(fileName).ownerAs<QQmlJS::Dom::QmlFile>();
2511 if (!qmlFile) {
2512 qDebug() << "Could not find file" << fileName << "in the dom!";
2513 return {};
2514 }
2515 return Location{ fileName, sourceLocation,
2516 textRowAndColumnFrom(qmlFile->code(), sourceLocation.end()) };
2517}
2518
2519Location Location::from(const QString &fileName, const QQmlJS::SourceLocation &sourceLocation, const QString &code)
2520{
2521 return Location{ fileName, sourceLocation, textRowAndColumnFrom(code, sourceLocation.end()) };
2522}
2523
2524Location Location::from(const QString &fileName, const QString &code, qsizetype startLine,
2525 qsizetype startCharacter, qsizetype length)
2526{
2527 const auto offset = QQmlLSUtils::textOffsetFrom(code, startLine - 1, startCharacter - 1);
2528 return from(fileName,
2529 QQmlJS::SourceLocation::fromQSizeType(offset, length, startLine, startCharacter),
2530 code);
2531}
2532
2533Edit Edit::from(const QString &fileName, const QString &code, qsizetype startLine,
2534 qsizetype startCharacter, qsizetype length, const QString &newName)
2535{
2536 Edit rename;
2537 rename.location = Location::from(fileName, code, startLine, startCharacter, length);
2538 rename.replacement = newName;
2539 return rename;
2540}
2541
2542bool isValidEcmaScriptIdentifier(QStringView identifier)
2543{
2544 QQmlJS::Lexer lexer(nullptr);
2545 lexer.setCode(identifier.toString(), 0);
2546 const int token = lexer.lex();
2547 if (token != static_cast<int>(QQmlJS::Lexer::T_IDENTIFIER))
2548 return false;
2549 // make sure there is nothing following the lexed identifier
2550 const int eofToken = lexer.lex();
2551 return eofToken == static_cast<int>(QQmlJS::Lexer::EOF_SYMBOL);
2552}
2553
2554
2555/*!
2556\internal
2557Returns the name of the cmake program along with the arguments needed to build the
2558qmltyperegistration. This command generates the .qmltypes, qmldir and .qrc files required for qmlls
2559to provide correct information on C++ defined QML elements.
2560
2561We assume here that CMake is available in the path. This should be the case for linux and macOS by
2562default.
2563For windows, having CMake in the path is not too unrealistic, for example,
2564https://doc.qt.io/qt-6/windows-building.html#step-2-install-build-requirements claims that you need
2565to have CMake in your path to build Qt. So a developer machine running qmlls has a high chance of
2566having CMake in their path, if CMake is installed and used.
2567*/
2568std::pair<QString, QStringList> cmakeBuildCommand(const QString &path, int cmakeJobs)
2569{
2570 std::pair<QString, QStringList> result{
2571 u"cmake"_s, { u"--build"_s, path, u"-t"_s, u"all_qmltyperegistrations"_s }
2572 };
2573 if (cmakeJobs > 0)
2574 result.second << "-j"_L1 << QString::number(cmakeJobs);
2575 return result;
2576}
2577
2579{
2580 std::sort(m_usagesInFile.begin(), m_usagesInFile.end());
2581 std::sort(m_usagesInFilename.begin(), m_usagesInFilename.end());
2582}
2583
2584bool Usages::isEmpty() const
2585{
2586 return m_usagesInFilename.isEmpty() && m_usagesInFile.isEmpty();
2587}
2588
2589Usages::Usages(const QList<Location> &usageInFile, const QList<QString> &usageInFilename)
2591{
2592 std::sort(m_usagesInFile.begin(), m_usagesInFile.end());
2593 std::sort(m_usagesInFilename.begin(), m_usagesInFilename.end());
2594}
2595
2596RenameUsages::RenameUsages(const QList<Edit> &renamesInFile,
2597 const QList<FileRename> &renamesInFilename)
2599{
2600 std::sort(m_renamesInFile.begin(), m_renamesInFile.end());
2601 std::sort(m_renamesInFilename.begin(), m_renamesInFilename.end());
2602}
2603
2605static QStringList findFilePathsFromFileNamesImpl(const QStringList &rootDirs,
2606 const QStringList &fileNamesToSearch,
2607 SearchOption option,
2608 const QSet<QString> &ignoredFilePaths)
2609{
2610 if (fileNamesToSearch.isEmpty() || rootDirs.isEmpty())
2611 return {};
2612
2613 const qint64 maxFilesToSearch =
2614 qEnvironmentVariableIntegerValue("QMLLS_MAX_FILES_TO_SEARCH")
2615 .value_or(20'000); // 20'000 entries need around one second on my machine
2616 qint64 visitedItems = 0;
2617
2618 QStringList result;
2619 std::queue<QString> toVisit;
2620 for (const QString &rootDir : rootDirs)
2621 toVisit.push(rootDir);
2622
2623 while (!toVisit.empty()) {
2624 const QString current = toVisit.front();
2625 toVisit.pop();
2626
2627 for (const auto &entry : QDirListing{ current, QDirListing::IteratorFlag::ExcludeOther }) {
2628 // timeout to avoid qmlls from recursing too much, in case rootDir is the filesystem
2629 // root for example
2630 if (++visitedItems > maxFilesToSearch) {
2631 qInfo(QQmlLSUtilsLog).noquote().nospace()
2632 << "Aborting search for \"" << fileNamesToSearch.join("\", \""_L1)
2633 << "\" inside \"" << rootDirs.join("\", \""_L1)
2634 << "\" after reaching QMLLS_MAX_FILES_TO_SEARCH (currently set to "
2635 << maxFilesToSearch
2636 << "). Set the environment variable \"QMLLS_MAX_FILES_TO_SEARCH\" to a "
2637 "higher value to spend more time on searching.";
2638 return result;
2639 }
2640
2641 if (entry.isDir()) {
2642 toVisit.push(entry.filePath());
2643 continue;
2644 }
2645 Q_ASSERT(entry.isFile());
2646 if (!fileNamesToSearch.contains(entry.fileName()))
2647 continue;
2648
2649 if (ignoredFilePaths.contains(entry.absoluteFilePath()))
2650 continue;
2651
2652 if (option == FindFirst)
2653 return { entry.absoluteFilePath() };
2654
2655 result << entry.absoluteFilePath();
2656 }
2657 }
2658
2659 return result;
2660}
2661
2662QStringList findFilePathsFromFileNames(const QString &rootDir, const QStringList &fileNamesToSearch,
2663 const QSet<QString> &ignoredFilePaths)
2664{
2665 return findFilePathsFromFileNamesImpl({ rootDir }, fileNamesToSearch, FindAll,
2666 ignoredFilePaths);
2667}
2668
2669QString findFilePathFromFileName(const QStringList &rootDirs, const QString &fileNameToSearch,
2670 const QSet<QString> &ignoredFilePaths)
2671{
2672 const QStringList result = findFilePathsFromFileNamesImpl(rootDirs, { fileNameToSearch },
2673 FindFirst, ignoredFilePaths);
2674 return result.isEmpty() ? QString{} : result.front();
2675}
2676
2677} // namespace QQmlLSUtils
2678
2679QT_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 QString &fileName, const QStringList &headerLocations, const QQmlJS::SourceLocation &location)
static std::optional< ExpressionType > resolveFieldMemberExpressionType(const DomItem &item, ResolveOptions options)
static QList< Location > findDefinitionOfType(const QQmlJSScope::ConstPtr &scope, const DomItem &item, const QStringList &headerDirectories)
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)
DomItem baseObject(const DomItem &qmlObject)
QByteArray lspUriToQmlUrl(const QByteArray &uri)
DomItem sourceLocationToDomItem(const DomItem &file, const QQmlJS::SourceLocation &location)
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).
QList< Location > findDefinitionOf(const DomItem &item, const QStringList &headerDirectories)
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)
QList< T > optionalToList(std::optional< T > &&optional)
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")