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