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