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 <QtLanguageServer/private/qlanguageserverspectypes_p.h> |
7 | #include <QtCore/qthreadpool.h> |
8 | #include <QtCore/private/qduplicatetracker_p.h> |
9 | #include <QtCore/QRegularExpression> |
10 | #include <QtQmlDom/private/qqmldomexternalitems_p.h> |
11 | #include <QtQmlDom/private/qqmldomtop_p.h> |
12 | #include <QtQmlDom/private/qqmldomscriptelements_p.h> |
13 | #include <QtQmlDom/private/qqmldom_utils_p.h> |
14 | #include <memory> |
15 | #include <optional> |
16 | #include <stack> |
17 | #include <type_traits> |
18 | #include <utility> |
19 | #include <variant> |
20 | |
21 | using namespace QLspSpecification; |
22 | using namespace QQmlJS::Dom; |
23 | using namespace Qt::StringLiterals; |
24 | |
25 | QT_BEGIN_NAMESPACE |
26 | |
27 | Q_LOGGING_CATEGORY(QQmlLSUtilsLog, "qt.languageserver.utils" ) |
28 | |
29 | /*! |
30 | \internal |
31 | Helper to check if item is a Field Member Expression \c {<someExpression>.propertyName}. |
32 | */ |
33 | static bool isFieldMemberExpression(DomItem &item) |
34 | { |
35 | return item.internalKind() == DomType::ScriptBinaryExpression |
36 | && item.field(name: Fields::operation).value().toInteger() |
37 | == ScriptElements::BinaryExpression::FieldMemberAccess; |
38 | } |
39 | |
40 | /*! |
41 | \internal |
42 | Helper to check if item is a Field Member Access \c memberAccess in |
43 | \c {<someExpression>.memberAccess}. |
44 | */ |
45 | static bool isFieldMemberAccess(DomItem &item) |
46 | { |
47 | auto parent = item.directParent(); |
48 | if (!isFieldMemberExpression(item&: parent)) |
49 | return false; |
50 | |
51 | DomItem rightHandSide = parent.field(name: Fields::right); |
52 | return item == rightHandSide; |
53 | } |
54 | |
55 | /*! |
56 | \internal |
57 | The language server protocol calls "URI" what QML calls "URL". |
58 | According to RFC 3986, a URL is a special case of URI that not only |
59 | identifies a resource but also shows how to access it. |
60 | In QML, however, URIs are distinct from URLs. URIs are the |
61 | identifiers of modules, for example "QtQuick.Controls". |
62 | In order to not confuse the terms we interpret language server URIs |
63 | as URLs in the QML code model. |
64 | This method marks a point of translation between the terms, but does |
65 | not have to change the actual URI/URL. |
66 | |
67 | \sa QQmlLSUtils::qmlUriToLspUrl |
68 | */ |
69 | QByteArray QQmlLSUtils::lspUriToQmlUrl(const QByteArray &uri) |
70 | { |
71 | return uri; |
72 | } |
73 | |
74 | QByteArray QQmlLSUtils::qmlUrlToLspUri(const QByteArray &url) |
75 | { |
76 | return url; |
77 | } |
78 | |
79 | /*! |
80 | \internal |
81 | \brief Converts a QQmlJS::SourceLocation to a LSP Range. |
82 | |
83 | QQmlJS::SourceLocation starts counting lines and rows at 1, but the LSP Range starts at 0. |
84 | Also, the QQmlJS::SourceLocation contains startLine, startColumn and length while the LSP Range |
85 | contains startLine, startColumn, endLine and endColumn, which must be computed from the actual |
86 | qml code. |
87 | */ |
88 | QLspSpecification::Range QQmlLSUtils::qmlLocationToLspLocation(const QString &code, |
89 | QQmlJS::SourceLocation qmlLocation) |
90 | { |
91 | Range range; |
92 | |
93 | range.start.line = qmlLocation.startLine - 1; |
94 | range.start.character = qmlLocation.startColumn - 1; |
95 | |
96 | auto end = QQmlLSUtils::textRowAndColumnFrom(code, offset: qmlLocation.end()); |
97 | range.end.line = end.line; |
98 | range.end.character = end.character; |
99 | return range; |
100 | } |
101 | |
102 | /*! |
103 | \internal |
104 | \brief Convert a text position from (line, column) into an offset. |
105 | |
106 | Row, Column and the offset are all 0-based. |
107 | For example, \c{s[textOffsetFrom(s, 5, 55)]} returns the character of s at line 5 and column 55. |
108 | |
109 | \sa QQmlLSUtils::textRowAndColumnFrom |
110 | */ |
111 | qsizetype QQmlLSUtils::textOffsetFrom(const QString &text, int row, int column) |
112 | { |
113 | int targetLine = row; |
114 | qsizetype i = 0; |
115 | while (i != text.size() && targetLine != 0) { |
116 | QChar c = text.at(i: i++); |
117 | if (c == u'\n') { |
118 | --targetLine; |
119 | } |
120 | if (c == u'\r') { |
121 | if (i != text.size() && text.at(i) == u'\n') |
122 | ++i; |
123 | --targetLine; |
124 | } |
125 | } |
126 | qsizetype leftChars = column; |
127 | while (i != text.size() && leftChars) { |
128 | QChar c = text.at(i); |
129 | if (c == u'\n' || c == u'\r') |
130 | break; |
131 | ++i; |
132 | if (!c.isLowSurrogate()) |
133 | --leftChars; |
134 | } |
135 | return i; |
136 | } |
137 | |
138 | /*! |
139 | \internal |
140 | \brief Convert a text position from an offset into (line, column). |
141 | |
142 | Row, Column and the offset are all 0-based. |
143 | For example, \c{textRowAndColumnFrom(s, 55)} returns the line and columns of the |
144 | character at \c {s[55]}. |
145 | |
146 | \sa QQmlLSUtils::textOffsetFrom |
147 | */ |
148 | QQmlLSUtilsTextPosition QQmlLSUtils::textRowAndColumnFrom(const QString &text, qsizetype offset) |
149 | { |
150 | int row = 0; |
151 | int column = 0; |
152 | qsizetype currentLineOffset = 0; |
153 | for (qsizetype i = 0; i < offset; i++) { |
154 | QChar c = text[i]; |
155 | if (c == u'\n') { |
156 | row++; |
157 | currentLineOffset = i + 1; |
158 | } else if (c == u'\r') { |
159 | if (i > 0 && text[i - 1] == u'\n') |
160 | currentLineOffset++; |
161 | } |
162 | } |
163 | column = offset - currentLineOffset; |
164 | |
165 | return { .line: row, .character: column }; |
166 | } |
167 | |
168 | /*! |
169 | \internal |
170 | \brief Find the DomItem representing the object situated in file at given line and |
171 | character/column. |
172 | |
173 | If line and character point between two objects, two objects might be returned. |
174 | If line and character point to whitespace, it might return an inner node of the QmlDom-Tree. |
175 | */ |
176 | QList<QQmlLSUtilsItemLocation> QQmlLSUtils::itemsFromTextLocation(DomItem file, int line, |
177 | int character) |
178 | { |
179 | QList<QQmlLSUtilsItemLocation> itemsFound; |
180 | std::shared_ptr<QmlFile> filePtr = file.ownerAs<QmlFile>(); |
181 | if (!filePtr) |
182 | return itemsFound; |
183 | FileLocations::Tree t = filePtr->fileLocationsTree(); |
184 | Q_ASSERT(t); |
185 | QString code = filePtr->code(); // do something more advanced wrt to changes wrt to this->code? |
186 | QList<QQmlLSUtilsItemLocation> toDo; |
187 | qsizetype targetPos = textOffsetFrom(text: code, row: line, column: character); |
188 | Q_ASSERT(targetPos >= 0); |
189 | auto containsTarget = [targetPos](QQmlJS::SourceLocation l) { |
190 | if constexpr (sizeof(qsizetype) <= sizeof(quint32)) { |
191 | return l.begin() <= quint32(targetPos) && quint32(targetPos) <= l.end(); |
192 | } else { |
193 | return l.begin() <= targetPos && targetPos <= l.end(); |
194 | } |
195 | }; |
196 | if (containsTarget(t->info().fullRegion)) { |
197 | QQmlLSUtilsItemLocation loc; |
198 | loc.domItem = file; |
199 | loc.fileLocation = t; |
200 | toDo.append(t: loc); |
201 | } |
202 | while (!toDo.isEmpty()) { |
203 | QQmlLSUtilsItemLocation iLoc = toDo.last(); |
204 | toDo.removeLast(); |
205 | |
206 | bool inParentButOutsideChildren = true; |
207 | |
208 | auto subEls = iLoc.fileLocation->subItems(); |
209 | for (auto it = subEls.begin(); it != subEls.end(); ++it) { |
210 | auto subLoc = std::static_pointer_cast<AttachedInfoT<FileLocations>>(r: it.value()); |
211 | Q_ASSERT(subLoc); |
212 | |
213 | if (containsTarget(subLoc->info().fullRegion)) { |
214 | QQmlLSUtilsItemLocation subItem; |
215 | subItem.domItem = iLoc.domItem.path(p: it.key()); |
216 | if (!subItem.domItem) { |
217 | qCDebug(QQmlLSUtilsLog) |
218 | << "A DomItem child is missing or the FileLocationsTree structure does " |
219 | "not follow the DomItem Structure." ; |
220 | continue; |
221 | } |
222 | subItem.fileLocation = subLoc; |
223 | toDo.append(t: subItem); |
224 | inParentButOutsideChildren = false; |
225 | } |
226 | } |
227 | if (inParentButOutsideChildren) { |
228 | itemsFound.append(t: iLoc); |
229 | } |
230 | } |
231 | |
232 | // filtering step: |
233 | if (itemsFound.size() > 1) { |
234 | // if there are multiple items, take the smallest one + its neighbors |
235 | // this allows to prefer inline components over main components, when both contain the |
236 | // current textposition, and to disregard internal structures like property maps, which |
237 | // "contain" everything from their first-appearing to last-appearing property (e.g. also |
238 | // other stuff in between those two properties). |
239 | auto smallest = std::min_element( |
240 | first: itemsFound.begin(), last: itemsFound.end(), |
241 | comp: [](const QQmlLSUtilsItemLocation &a, const QQmlLSUtilsItemLocation &b) { |
242 | return a.fileLocation->info().fullRegion.length |
243 | < b.fileLocation->info().fullRegion.length; |
244 | }); |
245 | QList<QQmlLSUtilsItemLocation> filteredItems; |
246 | filteredItems.append(t: *smallest); |
247 | |
248 | const QQmlJS::SourceLocation smallestLoc = smallest->fileLocation->info().fullRegion; |
249 | const quint32 smallestBegin = smallestLoc.begin(); |
250 | const quint32 smallestEnd = smallestLoc.end(); |
251 | |
252 | for (auto it = itemsFound.begin(); it != itemsFound.end(); it++) { |
253 | if (it == smallest) |
254 | continue; |
255 | |
256 | const QQmlJS::SourceLocation itLoc = it->fileLocation->info().fullRegion; |
257 | const quint32 itBegin = itLoc.begin(); |
258 | const quint32 itEnd = itLoc.end(); |
259 | if (itBegin == smallestEnd || smallestBegin == itEnd) { |
260 | filteredItems.append(t: *it); |
261 | } |
262 | } |
263 | itemsFound = filteredItems; |
264 | } |
265 | return itemsFound; |
266 | } |
267 | |
268 | DomItem QQmlLSUtils::baseObject(DomItem object) |
269 | { |
270 | if (!object.as<QmlObject>()) |
271 | return {}; |
272 | |
273 | auto prototypes = object.field(name: QQmlJS::Dom::Fields::prototypes); |
274 | switch (prototypes.indexes()) { |
275 | case 0: |
276 | return {}; |
277 | case 1: |
278 | break; |
279 | default: |
280 | qDebug() << "Multiple prototypes found for " << object.name() << ", taking the first one." ; |
281 | break; |
282 | } |
283 | QQmlJS::Dom::DomItem base = prototypes.index(0).proceedToScope(); |
284 | return base; |
285 | } |
286 | |
287 | /*! |
288 | \internal |
289 | \brief Extracts the QML object type of an \l DomItem. |
290 | |
291 | For a \c PropertyDefinition, return the type of the property. |
292 | For a \c Binding, return the bound item's type if an QmlObject is bound, and otherwise the type |
293 | of the property. |
294 | For a \c QmlObject, do nothing and return it. |
295 | For an \c Id, return the object to |
296 | which the id resolves. |
297 | For a \c Methodparameter, return the type of the parameter. = |
298 | Otherwise, return an empty item. |
299 | */ |
300 | DomItem QQmlLSUtils::findTypeDefinitionOf(DomItem object) |
301 | { |
302 | DomItem result; |
303 | |
304 | switch (object.internalKind()) { |
305 | case QQmlJS::Dom::DomType::QmlComponent: |
306 | result = object.field(name: Fields::objects).index(0); |
307 | break; |
308 | case QQmlJS::Dom::DomType::QmlObject: |
309 | result = baseObject(object); |
310 | break; |
311 | case QQmlJS::Dom::DomType::Binding: { |
312 | auto binding = object.as<Binding>(); |
313 | Q_ASSERT(binding); |
314 | |
315 | // try to grab the type from the bound object |
316 | if (binding->valueKind() == BindingValueKind::Object) { |
317 | result = baseObject(object: object.field(name: Fields::value)); |
318 | } else { |
319 | // use the type of the property it is bound on for scriptexpression etc. |
320 | DomItem propertyDefinition; |
321 | const QString bindingName = binding->name(); |
322 | object.containingObject().visitLookup( |
323 | symbolName: bindingName, |
324 | visitor: [&propertyDefinition](DomItem &item) { |
325 | if (item.internalKind() == QQmlJS::Dom::DomType::PropertyDefinition) { |
326 | propertyDefinition = item; |
327 | return false; |
328 | } |
329 | return true; |
330 | }, |
331 | type: LookupType::PropertyDef); |
332 | result = propertyDefinition.field(name: Fields::type).proceedToScope(); |
333 | } |
334 | break; |
335 | } |
336 | case QQmlJS::Dom::DomType::Id: |
337 | result = object.field(name: Fields::referredObject).proceedToScope(); |
338 | break; |
339 | case QQmlJS::Dom::DomType::PropertyDefinition: |
340 | case QQmlJS::Dom::DomType::MethodParameter: |
341 | case QQmlJS::Dom::DomType::MethodInfo: |
342 | result = object.field(name: Fields::type).proceedToScope(); |
343 | break; |
344 | case QQmlJS::Dom::DomType::ScriptIdentifierExpression: { |
345 | if (object.directParent().internalKind() == DomType::ScriptType) { |
346 | DomItem type = |
347 | object.filterUp(filter: [](DomType k, DomItem &) { return k == DomType::ScriptType; }, |
348 | options: FilterUpOptions::ReturnOuter); |
349 | |
350 | const QString name = type.field(name: Fields::typeName).value().toString(); |
351 | result = object.path(p: Paths::lookupTypePath(name)); |
352 | break; |
353 | } |
354 | auto scope = |
355 | QQmlLSUtils::resolveExpressionType(item: object, QQmlLSUtilsResolveOptions::Everything); |
356 | if (!scope) |
357 | return {}; |
358 | |
359 | return QQmlLSUtils::sourceLocationToDomItem(file: object.containingFile(), |
360 | location: scope->sourceLocation()); |
361 | } |
362 | case QQmlJS::Dom::DomType::Empty: |
363 | break; |
364 | default: |
365 | qDebug() << "QQmlLSUtils::findTypeDefinitionOf: Found unimplemented Type" |
366 | << object.internalKindStr(); |
367 | result = {}; |
368 | } |
369 | |
370 | return result; |
371 | } |
372 | |
373 | static DomItem findJSIdentifierDefinition(DomItem item, const QString &name) |
374 | { |
375 | DomItem definitionOfItem; |
376 | item.visitUp(visitor: [&name, &definitionOfItem](DomItem &i) { |
377 | if (std::optional<QQmlJSScope::Ptr> scope = i.semanticScope(); scope) { |
378 | qCDebug(QQmlLSUtilsLog) << "Searching for definition in" << i.internalKindStr(); |
379 | if (auto jsIdentifier = scope.value()->JSIdentifier(id: name)) { |
380 | qCDebug(QQmlLSUtilsLog) << "Found scope" << scope.value()->baseTypeName(); |
381 | definitionOfItem = i; |
382 | return false; |
383 | } |
384 | } |
385 | // early exit: no JS definitions/usages outside the ScriptExpression DOM element. |
386 | if (i.internalKind() == DomType::ScriptExpression) |
387 | return false; |
388 | return true; |
389 | }); |
390 | |
391 | return definitionOfItem; |
392 | } |
393 | |
394 | static void findUsagesOfPropertiesAndIds(DomItem item, const QString &name, |
395 | QList<QQmlLSUtilsLocation> &result) |
396 | { |
397 | QQmlJSScope::ConstPtr targetType; |
398 | targetType = QQmlLSUtils::resolveExpressionType(item, QQmlLSUtilsResolveOptions::JustOwner); |
399 | |
400 | auto findUsages = [&targetType, &result, &name](Path, DomItem ¤t, bool) -> bool { |
401 | bool resolveType = false; |
402 | bool continueForChildren = true; |
403 | DomItem toBeResolved = current; |
404 | |
405 | if (auto scope = current.semanticScope()) { |
406 | // is the current property shadowed by some JS identifier? ignore current + its children |
407 | if (scope.value()->JSIdentifier(id: name)) { |
408 | return false; |
409 | } |
410 | } |
411 | |
412 | switch (current.internalKind()) { |
413 | case DomType::PropertyDefinition: { |
414 | const QString propertyName = current.field(name: Fields::name).value().toString(); |
415 | if (name == propertyName) { |
416 | resolveType = true; |
417 | } else { |
418 | return true; |
419 | } |
420 | break; |
421 | } |
422 | case DomType::ScriptIdentifierExpression: { |
423 | const QString propertyName = current.field(name: Fields::identifier).value().toString(); |
424 | if (name != propertyName) |
425 | return true; |
426 | |
427 | resolveType = true; |
428 | break; |
429 | } |
430 | default: |
431 | break; |
432 | }; |
433 | |
434 | if (resolveType) { |
435 | auto currentType = QQmlLSUtils::resolveExpressionType( |
436 | item: toBeResolved, QQmlLSUtilsResolveOptions::JustOwner); |
437 | qCDebug(QQmlLSUtilsLog) << "Will resolve type of" << toBeResolved.internalKindStr(); |
438 | if (currentType == targetType) { |
439 | auto tree = FileLocations::treeOf(current); |
440 | QQmlLSUtilsLocation location{ .filename: current.canonicalFilePath(), |
441 | .location: tree->info().fullRegion }; |
442 | result.append(t: location); |
443 | } |
444 | } |
445 | return continueForChildren; |
446 | }; |
447 | |
448 | item.containingFile() |
449 | .field(name: Fields::components) |
450 | .visitTree(basePath: Path(), visitor: emptyChildrenVisitor, options: VisitOption::Recurse | VisitOption::VisitSelf, |
451 | openingVisitor: findUsages); |
452 | } |
453 | |
454 | static void findUsagesHelper(DomItem item, const QString &name, QList<QQmlLSUtilsLocation> &result) |
455 | { |
456 | qCDebug(QQmlLSUtilsLog) << "Looking for JS identifier with name" << name; |
457 | DomItem definitionOfItem = findJSIdentifierDefinition(item, name); |
458 | |
459 | // if there is no definition found: check if name was a property or an id instead |
460 | if (!definitionOfItem) { |
461 | qCDebug(QQmlLSUtilsLog) << "No defining JS-Scope found!" ; |
462 | findUsagesOfPropertiesAndIds(item, name, result); |
463 | return; |
464 | } |
465 | |
466 | definitionOfItem.visitTree( |
467 | basePath: Path(), visitor: emptyChildrenVisitor, options: VisitOption::VisitAdopted | VisitOption::Recurse, |
468 | openingVisitor: [&name, &result](Path, DomItem &item, bool) -> bool { |
469 | qCDebug(QQmlLSUtilsLog) << "Visiting a " << item.internalKindStr(); |
470 | if (item.internalKind() == DomType::ScriptIdentifierExpression |
471 | && item.field(name: Fields::identifier).value().toString() == name) { |
472 | // add this usage |
473 | auto fileLocation = FileLocations::treeOf(item); |
474 | if (!fileLocation) { |
475 | qCWarning(QQmlLSUtilsLog) << "Failed finding filelocation of found usage" ; |
476 | return true; |
477 | } |
478 | const QQmlJS::SourceLocation location = fileLocation->info().fullRegion; |
479 | const QString fileName = item.canonicalFilePath(); |
480 | result.append(t: { .filename: fileName, .location: location }); |
481 | return true; |
482 | } else if (std::optional<QQmlJSScope::Ptr> scope = item.semanticScope(); |
483 | scope && scope.value()->JSIdentifier(id: name)) { |
484 | // current JS identifier has been redefined, do not visit children |
485 | return false; |
486 | } |
487 | return true; |
488 | }); |
489 | } |
490 | |
491 | QList<QQmlLSUtilsLocation> QQmlLSUtils::findUsagesOf(DomItem item) |
492 | { |
493 | QList<QQmlLSUtilsLocation> result; |
494 | |
495 | switch (item.internalKind()) { |
496 | case DomType::ScriptIdentifierExpression: { |
497 | const QString name = item.field(name: Fields::identifier).value().toString(); |
498 | findUsagesHelper(item, name, result); |
499 | break; |
500 | } |
501 | case DomType::ScriptVariableDeclarationEntry: { |
502 | const QString name = item.field(name: Fields::identifier).value().toString(); |
503 | findUsagesHelper(item, name, result); |
504 | break; |
505 | } |
506 | case DomType::PropertyDefinition: { |
507 | const QString name = item.field(name: Fields::name).value().toString(); |
508 | findUsagesHelper(item, name, result); |
509 | break; |
510 | } |
511 | default: |
512 | qCDebug(QQmlLSUtilsLog) << item.internalKindStr() |
513 | << "was not implemented for QQmlLSUtils::findUsagesOf" ; |
514 | return result; |
515 | } |
516 | |
517 | std::sort(first: result.begin(), last: result.end()); |
518 | |
519 | if (QQmlLSUtilsLog().isDebugEnabled()) { |
520 | qCDebug(QQmlLSUtilsLog) << "Found following usages:" ; |
521 | for (auto r : result) { |
522 | qCDebug(QQmlLSUtilsLog) |
523 | << r.filename << " @ " << r.location.startLine << ":" << r.location.startColumn |
524 | << " with length " << r.location.length; |
525 | } |
526 | } |
527 | |
528 | return result; |
529 | } |
530 | |
531 | static QQmlJSScope::ConstPtr findPropertyIn(const QQmlJSScope::Ptr &referrerScope, |
532 | const QString &propertyName, |
533 | QQmlLSUtilsResolveOptions options) |
534 | { |
535 | for (QQmlJSScope::Ptr current = referrerScope; current; current = current->parentScope()) { |
536 | if (auto property = current->property(name: propertyName); property.isValid()) { |
537 | switch (options) { |
538 | case JustOwner: |
539 | return current; |
540 | case Everything: |
541 | return property.type(); |
542 | } |
543 | } |
544 | } |
545 | return {}; |
546 | } |
547 | |
548 | /*! |
549 | \internal |
550 | Resolves the type of the given DomItem, when possible (e.g., when there are enough type |
551 | annotations). |
552 | */ |
553 | QQmlJSScope::ConstPtr QQmlLSUtils::resolveExpressionType(QQmlJS::Dom::DomItem item, |
554 | QQmlLSUtilsResolveOptions options) |
555 | { |
556 | switch (item.internalKind()) { |
557 | case DomType::ScriptIdentifierExpression: { |
558 | auto referrerScope = item.nearestSemanticScope(); |
559 | if (!referrerScope) |
560 | return {}; |
561 | |
562 | const QString name = item.field(name: Fields::identifier).value().toString(); |
563 | |
564 | if (isFieldMemberAccess(item)) { |
565 | DomItem parent = item.directParent(); |
566 | auto owner = QQmlLSUtils::resolveExpressionType(item: parent.field(name: Fields::left), |
567 | options: QQmlLSUtilsResolveOptions::Everything); |
568 | if (owner) { |
569 | if (auto property = owner->property(name); property.isValid()) { |
570 | switch (options) { |
571 | case JustOwner: |
572 | return owner; |
573 | case Everything: |
574 | return property.type(); |
575 | } |
576 | } |
577 | } else { |
578 | return {}; |
579 | } |
580 | } else { |
581 | DomItem definitionOfItem = findJSIdentifierDefinition(item, name); |
582 | if (definitionOfItem) { |
583 | Q_ASSERT_X(definitionOfItem.semanticScope().has_value() |
584 | && definitionOfItem.semanticScope() |
585 | .value() |
586 | ->JSIdentifier(name) |
587 | .has_value(), |
588 | "QQmlLSUtils::findDefinitionOf" , |
589 | "JS definition does not actually define the JS identifer. " |
590 | "It should be empty." ); |
591 | auto scope = definitionOfItem.semanticScope().value()->JSIdentifier(id: name)->scope.toStrongRef();; |
592 | return scope; |
593 | } |
594 | |
595 | // check if its an (unqualified) property |
596 | if (QQmlJSScope::ConstPtr scope = findPropertyIn(referrerScope: *referrerScope, propertyName: name, options)) { |
597 | return scope; |
598 | } |
599 | } |
600 | |
601 | // check if its an id |
602 | auto resolver = item.containingFile().ownerAs<QmlFile>()->typeResolver(); |
603 | if (!resolver) |
604 | return {}; |
605 | QQmlJSScope::ConstPtr fromId = resolver.value()->scopeForId(id: name, referrer: referrerScope.value()); |
606 | if (fromId) |
607 | return fromId; |
608 | |
609 | return {}; |
610 | } |
611 | case DomType::PropertyDefinition: { |
612 | auto propertyDefinition = item.as<PropertyDefinition>(); |
613 | if (propertyDefinition && propertyDefinition->scope) { |
614 | switch (options) { |
615 | case JustOwner: |
616 | return propertyDefinition->scope.value(); |
617 | case Everything: |
618 | return propertyDefinition->scope.value()->property(name: propertyDefinition->name).type(); |
619 | } |
620 | } |
621 | |
622 | return {}; |
623 | } |
624 | case DomType::QmlObject: { |
625 | auto object = item.as<QmlObject>(); |
626 | if (object && object->semanticScope()) |
627 | return object->semanticScope().value(); |
628 | |
629 | return {}; |
630 | } |
631 | case DomType::ScriptBinaryExpression: { |
632 | if (isFieldMemberExpression(item)) { |
633 | DomItem owner = item.field(name: Fields::left); |
634 | QString propertyName = |
635 | item.field(name: Fields::right).field(name: Fields::identifier).value().toString(); |
636 | auto ownerType = QQmlLSUtils::resolveExpressionType( |
637 | item: owner, options: QQmlLSUtilsResolveOptions::Everything); |
638 | if (!ownerType) |
639 | return ownerType; |
640 | switch (options) { |
641 | case JustOwner: |
642 | return ownerType; |
643 | case Everything: |
644 | return ownerType->property(name: propertyName).type(); |
645 | } |
646 | } |
647 | return {}; |
648 | } |
649 | default: { |
650 | qCDebug(QQmlLSUtilsLog) << "Type" << item.internalKindStr() |
651 | << "is unimplemented in QQmlLSUtils::resolveExpressionType" ; |
652 | return {}; |
653 | } |
654 | } |
655 | Q_UNREACHABLE(); |
656 | } |
657 | |
658 | DomItem QQmlLSUtils::sourceLocationToDomItem(DomItem file, const QQmlJS::SourceLocation &location) |
659 | { |
660 | // QQmlJS::SourceLocation starts counting at 1 but the utils and the LSP start at 0. |
661 | auto items = QQmlLSUtils::itemsFromTextLocation(file, line: location.startLine - 1, |
662 | character: location.startColumn - 1); |
663 | switch (items.size()) { |
664 | case 0: |
665 | return {}; |
666 | case 1: |
667 | return items.front().domItem; |
668 | case 2: { |
669 | // special case: because location points to the beginning of the type definition, |
670 | // itemsFromTextLocation might also return the type on its left, in case it is directly |
671 | // adjacent to it. In this case always take the right (=with the higher column-number) |
672 | // item. |
673 | auto &first = items.front(); |
674 | auto &second = items.back(); |
675 | Q_ASSERT_X(first.fileLocation->info().fullRegion.startLine |
676 | == second.fileLocation->info().fullRegion.startLine, |
677 | "QQmlLSUtils::findTypeDefinitionOf(DomItem)" , |
678 | "QQmlLSUtils::itemsFromTextLocation returned non-adjacent items." ); |
679 | if (first.fileLocation->info().fullRegion.startColumn |
680 | > second.fileLocation->info().fullRegion.startColumn) |
681 | return first.domItem; |
682 | else |
683 | return second.domItem; |
684 | break; |
685 | } |
686 | default: |
687 | qDebug() << "Found multiple candidates for type of scriptidentifierexpression" ; |
688 | break; |
689 | } |
690 | return {}; |
691 | } |
692 | |
693 | std::optional<QQmlLSUtilsLocation> |
694 | findPropertyDefinitionOf(DomItem file, QQmlJS::SourceLocation propertyDefinitionLocation, |
695 | const QString &name) |
696 | { |
697 | DomItem propertyOwner = QQmlLSUtils::sourceLocationToDomItem(file, location: propertyDefinitionLocation); |
698 | DomItem propertyDefinition = propertyOwner.field(name: Fields::propertyDefs).key(name).index(0); |
699 | if (!propertyDefinition) |
700 | return {}; |
701 | |
702 | QQmlLSUtilsLocation result; |
703 | result.location = FileLocations::treeOf(propertyDefinition)->info().fullRegion; |
704 | result.filename = propertyDefinition.canonicalFilePath(); |
705 | return result; |
706 | } |
707 | |
708 | std::optional<QQmlLSUtilsLocation> QQmlLSUtils::findDefinitionOf(DomItem item) |
709 | { |
710 | |
711 | switch (item.internalKind()) { |
712 | case QQmlJS::Dom::DomType::ScriptIdentifierExpression: { |
713 | // first check if its a JS Identifier |
714 | |
715 | const QString name = item.value().toString(); |
716 | if (isFieldMemberAccess(item)) { |
717 | if (auto ownerScope = QQmlLSUtils::resolveExpressionType( |
718 | item, options: QQmlLSUtilsResolveOptions::JustOwner)) { |
719 | const DomItem ownerFile = item.goToFile(filePath: ownerScope->filePath()); |
720 | const QQmlJS::SourceLocation ownerLocation = ownerScope->sourceLocation(); |
721 | if (auto propertyDefinition = |
722 | findPropertyDefinitionOf(file: ownerFile, propertyDefinitionLocation: ownerLocation, name)) { |
723 | return propertyDefinition; |
724 | } |
725 | } |
726 | return {}; |
727 | } |
728 | |
729 | // check: is it a JS identifier? |
730 | if (DomItem definitionOfItem = findJSIdentifierDefinition(item, name)) { |
731 | Q_ASSERT_X(definitionOfItem.semanticScope().has_value() |
732 | && definitionOfItem.semanticScope() |
733 | .value() |
734 | ->JSIdentifier(name) |
735 | .has_value(), |
736 | "QQmlLSUtils::findDefinitionOf" , |
737 | "JS definition does not actually define the JS identifer. " |
738 | "It should be empty." ); |
739 | QQmlJS::SourceLocation location = |
740 | definitionOfItem.semanticScope().value()->JSIdentifier(id: name).value().location; |
741 | |
742 | QQmlLSUtilsLocation result = { .filename: definitionOfItem.canonicalFilePath(), .location: location }; |
743 | return result; |
744 | } |
745 | |
746 | // not a JS identifier, check for ids and properties |
747 | auto referrerScope = item.nearestSemanticScope(); |
748 | if (!referrerScope) |
749 | return {}; |
750 | |
751 | if (QQmlJSScope::ConstPtr scope = |
752 | findPropertyIn(referrerScope: *referrerScope, propertyName: name, options: QQmlLSUtilsResolveOptions::JustOwner)) { |
753 | const QString canonicalPath = scope->filePath(); |
754 | qDebug() << canonicalPath; |
755 | DomItem file = item.goToFile(filePath: canonicalPath); |
756 | return findPropertyDefinitionOf(file, propertyDefinitionLocation: scope->sourceLocation(), name); |
757 | } |
758 | |
759 | // check if its an id |
760 | auto resolver = item.containingFile().ownerAs<QmlFile>()->typeResolver(); |
761 | if (!resolver) |
762 | return {}; |
763 | QQmlJSScope::ConstPtr fromId = resolver.value()->scopeForId(id: name, referrer: referrerScope.value()); |
764 | if (fromId) { |
765 | QQmlLSUtilsLocation result; |
766 | result.location = fromId->sourceLocation(); |
767 | result.filename = item.canonicalFilePath(); |
768 | return result; |
769 | } |
770 | return {}; |
771 | } |
772 | default: |
773 | qDebug() << "QQmlLSUtils::findDefinitionOf: Found unimplemented Type " |
774 | << item.internalKindStr(); |
775 | return {}; |
776 | } |
777 | |
778 | Q_UNREACHABLE_RETURN(std::nullopt); |
779 | } |
780 | |
781 | QT_END_NAMESPACE |
782 | |