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