| 1 | // Copyright (C) 2022 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 |
| 3 | |
| 4 | #include "qqmljsutils_p.h" |
| 5 | #include "qqmljstyperesolver_p.h" |
| 6 | #include "qqmljsscopesbyid_p.h" |
| 7 | |
| 8 | #include <QtCore/qvarlengtharray.h> |
| 9 | #include <QtCore/qdir.h> |
| 10 | #include <QtCore/qdiriterator.h> |
| 11 | |
| 12 | #include <algorithm> |
| 13 | |
| 14 | QT_BEGIN_NAMESPACE |
| 15 | |
| 16 | using namespace Qt::StringLiterals; |
| 17 | |
| 18 | /*! \internal |
| 19 | |
| 20 | Fully resolves alias \a property and returns the information about the |
| 21 | origin, which is not an alias. |
| 22 | */ |
| 23 | template<typename ScopeForId> |
| 24 | static QQmlJSUtils::ResolvedAlias |
| 25 | resolveAlias(ScopeForId scopeForId, const QQmlJSMetaProperty &property, |
| 26 | const QQmlJSScope::ConstPtr &owner, const QQmlJSUtils::AliasResolutionVisitor &visitor) |
| 27 | { |
| 28 | Q_ASSERT(property.isAlias()); |
| 29 | Q_ASSERT(owner); |
| 30 | |
| 31 | QQmlJSUtils::ResolvedAlias result {}; |
| 32 | result.owner = owner; |
| 33 | |
| 34 | // TODO: one could optimize the generated alias code for aliases pointing to aliases |
| 35 | // e.g., if idA.myAlias -> idB.myAlias2 -> idC.myProp, then one could directly generate |
| 36 | // idA.myProp as pointing to idC.myProp. // This gets complicated when idB.myAlias is in a different Component than where the |
| 37 | // idA.myAlias is defined: scopeForId currently only contains the ids of the current |
| 38 | // component and alias resolution on the ids of a different component fails then. |
| 39 | if (QQmlJSMetaProperty nextProperty = property; nextProperty.isAlias()) { |
| 40 | QQmlJSScope::ConstPtr resultOwner = result.owner; |
| 41 | result = QQmlJSUtils::ResolvedAlias {}; |
| 42 | |
| 43 | visitor.reset(); |
| 44 | |
| 45 | auto aliasExprBits = nextProperty.aliasExpression().split(sep: u'.'); |
| 46 | // do not crash on invalid aliasexprbits when accessing aliasExprBits[0] |
| 47 | if (aliasExprBits.size() < 1) |
| 48 | return {}; |
| 49 | |
| 50 | // resolve id first: |
| 51 | resultOwner = scopeForId(aliasExprBits[0], resultOwner); |
| 52 | if (!resultOwner) |
| 53 | return {}; |
| 54 | |
| 55 | visitor.processResolvedId(resultOwner); |
| 56 | |
| 57 | aliasExprBits.removeFirst(); // Note: for simplicity, remove the <id> |
| 58 | result.owner = resultOwner; |
| 59 | result.kind = QQmlJSUtils::AliasTarget_Object; |
| 60 | |
| 61 | for (const QString &bit : std::as_const(t&: aliasExprBits)) { |
| 62 | nextProperty = resultOwner->property(name: bit); |
| 63 | if (!nextProperty.isValid()) |
| 64 | return {}; |
| 65 | |
| 66 | visitor.processResolvedProperty(nextProperty, resultOwner); |
| 67 | |
| 68 | result.property = nextProperty; |
| 69 | result.owner = resultOwner; |
| 70 | result.kind = QQmlJSUtils::AliasTarget_Property; |
| 71 | |
| 72 | resultOwner = nextProperty.type(); |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | return result; |
| 77 | } |
| 78 | |
| 79 | QQmlJSUtils::ResolvedAlias QQmlJSUtils::resolveAlias(const QQmlJSTypeResolver *typeResolver, |
| 80 | const QQmlJSMetaProperty &property, |
| 81 | const QQmlJSScope::ConstPtr &owner, |
| 82 | const AliasResolutionVisitor &visitor) |
| 83 | { |
| 84 | return ::resolveAlias( |
| 85 | scopeForId: [&](const QString &id, const QQmlJSScope::ConstPtr &referrer) { |
| 86 | const QQmlJSRegisterContent content = typeResolver->scopedType(scope: referrer, name: id); |
| 87 | if (content.variant() == QQmlJSRegisterContent::ObjectById) |
| 88 | return content.type(); |
| 89 | return QQmlJSScope::ConstPtr(); |
| 90 | }, |
| 91 | property, owner, visitor); |
| 92 | } |
| 93 | |
| 94 | QQmlJSUtils::ResolvedAlias QQmlJSUtils::resolveAlias(const QQmlJSScopesById &idScopes, |
| 95 | const QQmlJSMetaProperty &property, |
| 96 | const QQmlJSScope::ConstPtr &owner, |
| 97 | const AliasResolutionVisitor &visitor) |
| 98 | { |
| 99 | return ::resolveAlias( |
| 100 | scopeForId: [&](const QString &id, const QQmlJSScope::ConstPtr &referrer) { |
| 101 | return idScopes.scope(id, referrer); |
| 102 | }, |
| 103 | property, owner, visitor); |
| 104 | } |
| 105 | |
| 106 | std::optional<QQmlJSFixSuggestion> QQmlJSUtils::didYouMean(const QString &userInput, |
| 107 | QStringList candidates, |
| 108 | QQmlJS::SourceLocation location) |
| 109 | { |
| 110 | QString shortestDistanceWord; |
| 111 | int shortestDistance = userInput.size(); |
| 112 | |
| 113 | // Most of the time the candidates are keys() from QHash, which means that |
| 114 | // running this function in the seemingly same setup might yield different |
| 115 | // best cadidate (e.g. imagine a typo 'thing' with candidates 'thingA' vs |
| 116 | // 'thingB'). This is especially flaky in e.g. test environment where the |
| 117 | // results may differ (even when the global hash seed is fixed!) when |
| 118 | // running one test vs the whole test suite (recall platform-dependent |
| 119 | // QSKIPs). There could be user-visible side effects as well, so just sort |
| 120 | // the candidates to guarantee consistent results |
| 121 | std::sort(first: candidates.begin(), last: candidates.end()); |
| 122 | |
| 123 | for (const QString &candidate : candidates) { |
| 124 | /* |
| 125 | * Calculate the distance between the userInput and candidate using Damerau–Levenshtein |
| 126 | * Roughly based on |
| 127 | * https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows. |
| 128 | */ |
| 129 | QList<int> v0(candidate.size() + 1); |
| 130 | QList<int> v1(candidate.size() + 1); |
| 131 | |
| 132 | std::iota(first: v0.begin(), last: v0.end(), value: 0); |
| 133 | |
| 134 | for (qsizetype i = 0; i < userInput.size(); i++) { |
| 135 | v1[0] = i + 1; |
| 136 | for (qsizetype j = 0; j < candidate.size(); j++) { |
| 137 | int deletionCost = v0[j + 1] + 1; |
| 138 | int insertionCost = v1[j] + 1; |
| 139 | int substitutionCost = userInput[i] == candidate[j] ? v0[j] : v0[j] + 1; |
| 140 | v1[j + 1] = std::min(l: { deletionCost, insertionCost, substitutionCost }); |
| 141 | } |
| 142 | std::swap(a&: v0, b&: v1); |
| 143 | } |
| 144 | |
| 145 | int distance = v0[candidate.size()]; |
| 146 | if (distance < shortestDistance) { |
| 147 | shortestDistanceWord = candidate; |
| 148 | shortestDistance = distance; |
| 149 | } |
| 150 | } |
| 151 | |
| 152 | if (shortestDistance |
| 153 | < std::min(a: std::max(a: userInput.size() / 2, b: qsizetype(3)), b: userInput.size())) { |
| 154 | return QQmlJSFixSuggestion { |
| 155 | u"Did you mean \"%1\"?"_s .arg(a: shortestDistanceWord), |
| 156 | location, |
| 157 | shortestDistanceWord |
| 158 | }; |
| 159 | } else { |
| 160 | return {}; |
| 161 | } |
| 162 | } |
| 163 | |
| 164 | /*! \internal |
| 165 | |
| 166 | Returns a corresponding source directory path for \a buildDirectoryPath |
| 167 | Returns empty string on error |
| 168 | */ |
| 169 | std::variant<QString, QQmlJS::DiagnosticMessage> |
| 170 | QQmlJSUtils::sourceDirectoryPath(const QQmlJSImporter *importer, const QString &buildDirectoryPath) |
| 171 | { |
| 172 | const auto makeError = [](const QString &msg) { |
| 173 | return QQmlJS::DiagnosticMessage { .message: msg, .type: QtWarningMsg, .loc: QQmlJS::SourceLocation() }; |
| 174 | }; |
| 175 | |
| 176 | if (!importer->metaDataMapper()) |
| 177 | return makeError(u"QQmlJSImporter::metaDataMapper() is nullptr"_s ); |
| 178 | |
| 179 | // for now, meta data contains just a single entry |
| 180 | QQmlJSResourceFileMapper::Filter matchAll { .path: QString(), .suffixes: QStringList(), |
| 181 | .flags: QQmlJSResourceFileMapper::Directory |
| 182 | | QQmlJSResourceFileMapper::Recurse }; |
| 183 | QQmlJSResourceFileMapper::Entry entry = importer->metaDataMapper()->entry(filter: matchAll); |
| 184 | if (!entry.isValid()) |
| 185 | return makeError(u"Failed to find meta data entry in QQmlJSImporter::metaDataMapper()"_s ); |
| 186 | if (!buildDirectoryPath.startsWith(s: entry.filePath)) // assume source directory path already |
| 187 | return makeError(u"The module output directory does not match the build directory path"_s ); |
| 188 | |
| 189 | QString qrcPath = buildDirectoryPath; |
| 190 | qrcPath.remove(i: 0, len: entry.filePath.size()); |
| 191 | qrcPath.prepend(s: entry.resourcePath); |
| 192 | qrcPath.remove(i: 0, len: 1); // remove extra "/" |
| 193 | |
| 194 | const QStringList sourceDirPaths = importer->resourceFileMapper()->filePaths( |
| 195 | filter: QQmlJSResourceFileMapper::resourceFileFilter(file: qrcPath)); |
| 196 | if (sourceDirPaths.size() != 1) { |
| 197 | const QString matchedPaths = |
| 198 | sourceDirPaths.isEmpty() ? u"<none>"_s : sourceDirPaths.join(sep: u", " ); |
| 199 | return makeError( |
| 200 | QStringLiteral("QRC path %1 (deduced from %2) has unexpected number of mappings " |
| 201 | "(%3). File paths that matched:\n%4" ) |
| 202 | .arg(args&: qrcPath, args: buildDirectoryPath, args: QString::number(sourceDirPaths.size()), |
| 203 | args: matchedPaths)); |
| 204 | } |
| 205 | return sourceDirPaths[0]; |
| 206 | } |
| 207 | |
| 208 | /*! \internal |
| 209 | |
| 210 | Utility method that checks if one of the registers is var, and the other can be |
| 211 | efficiently compared to it |
| 212 | */ |
| 213 | bool canStrictlyCompareWithVar( |
| 214 | const QQmlJSTypeResolver *typeResolver, const QQmlJSScope::ConstPtr &lhsType, |
| 215 | const QQmlJSScope::ConstPtr &rhsType) |
| 216 | { |
| 217 | Q_ASSERT(typeResolver); |
| 218 | |
| 219 | const QQmlJSScope::ConstPtr varType = typeResolver->varType(); |
| 220 | const bool leftIsVar = typeResolver->equals(a: lhsType, b: varType); |
| 221 | const bool righttIsVar = typeResolver->equals(a: rhsType, b: varType); |
| 222 | return leftIsVar != righttIsVar; |
| 223 | } |
| 224 | |
| 225 | /*! \internal |
| 226 | |
| 227 | Utility method that checks if one of the registers is qobject, and the other can be |
| 228 | efficiently compared to it |
| 229 | */ |
| 230 | bool canCompareWithQObject( |
| 231 | const QQmlJSTypeResolver *typeResolver, const QQmlJSScope::ConstPtr &lhsType, |
| 232 | const QQmlJSScope::ConstPtr &rhsType) |
| 233 | { |
| 234 | Q_ASSERT(typeResolver); |
| 235 | return (lhsType->isReferenceType() |
| 236 | && (rhsType->isReferenceType() |
| 237 | || typeResolver->equals(a: rhsType, b: typeResolver->nullType()))) |
| 238 | || (rhsType->isReferenceType() |
| 239 | && (lhsType->isReferenceType() |
| 240 | || typeResolver->equals(a: lhsType, b: typeResolver->nullType()))); |
| 241 | } |
| 242 | |
| 243 | /*! \internal |
| 244 | |
| 245 | Utility method that checks if both sides are QUrl type. In future, that might be extended to |
| 246 | support comparison with other types i.e QUrl vs string |
| 247 | */ |
| 248 | bool canCompareWithQUrl( |
| 249 | const QQmlJSTypeResolver *typeResolver, const QQmlJSScope::ConstPtr &lhsType, |
| 250 | const QQmlJSScope::ConstPtr &rhsType) |
| 251 | { |
| 252 | Q_ASSERT(typeResolver); |
| 253 | return typeResolver->equals(a: lhsType, b: typeResolver->urlType()) |
| 254 | && typeResolver->equals(a: rhsType, b: typeResolver->urlType()); |
| 255 | } |
| 256 | |
| 257 | QStringList QQmlJSUtils::resourceFilesFromBuildFolders(const QStringList &buildFolders) |
| 258 | { |
| 259 | QStringList result; |
| 260 | for (const QString &path : buildFolders) { |
| 261 | QDirIterator it(path, QStringList{ u"*.qrc"_s }, QDir::Files | QDir::Hidden, |
| 262 | QDirIterator::Subdirectories); |
| 263 | while (it.hasNext()) { |
| 264 | result.append(t: it.next()); |
| 265 | } |
| 266 | } |
| 267 | return result; |
| 268 | } |
| 269 | |
| 270 | enum FilterType { |
| 271 | LocalFileFilter, |
| 272 | ResourceFileFilter |
| 273 | }; |
| 274 | |
| 275 | /*! |
| 276 | \internal |
| 277 | Obtain a QML module qrc entry from its qmldir entry. |
| 278 | |
| 279 | Contains a heuristic for QML modules without nested-qml-module-with-prefer-feature |
| 280 | that tries to find a parent directory that contains a qmldir entry in the qrc. |
| 281 | */ |
| 282 | static QQmlJSResourceFileMapper::Entry |
| 283 | qmlModuleEntryFromBuildPath(const QQmlJSResourceFileMapper *mapper, |
| 284 | const QString &pathInBuildFolder, FilterType type) |
| 285 | { |
| 286 | const QString cleanPath = QDir::cleanPath(path: pathInBuildFolder); |
| 287 | QStringView directoryPath = cleanPath; |
| 288 | |
| 289 | while (!directoryPath.isEmpty()) { |
| 290 | const qsizetype lastSlashIndex = directoryPath.lastIndexOf(c: u'/'); |
| 291 | if (lastSlashIndex == -1) |
| 292 | return {}; |
| 293 | |
| 294 | directoryPath.truncate(n: lastSlashIndex); |
| 295 | const QString qmldirPath = u"%1/qmldir"_s .arg(a: directoryPath); |
| 296 | const QQmlJSResourceFileMapper::Filter qmldirFilter = type == LocalFileFilter |
| 297 | ? QQmlJSResourceFileMapper::localFileFilter(file: qmldirPath) |
| 298 | : QQmlJSResourceFileMapper::resourceFileFilter(file: qmldirPath); |
| 299 | |
| 300 | QQmlJSResourceFileMapper::Entry result = mapper->entry(filter: qmldirFilter); |
| 301 | if (result.isValid()) { |
| 302 | result.resourcePath.chop(n: std::char_traits<char>::length(s: "/qmldir" )); |
| 303 | result.filePath.chop(n: std::char_traits<char>::length(s: "/qmldir" )); |
| 304 | return result; |
| 305 | } |
| 306 | } |
| 307 | return {}; |
| 308 | } |
| 309 | |
| 310 | /*! |
| 311 | \internal |
| 312 | Obtains the source folder path from a build folder QML file path via the passed \c mapper. |
| 313 | |
| 314 | This works on proper QML modules when using the nested-qml-module-with-prefer-feature |
| 315 | from 6.8 and uses a heuristic when the qmldir with the prefer entry is missing. |
| 316 | */ |
| 317 | QString QQmlJSUtils::qmlSourcePathFromBuildPath(const QQmlJSResourceFileMapper *mapper, |
| 318 | const QString &pathInBuildFolder) |
| 319 | { |
| 320 | if (!mapper) |
| 321 | return pathInBuildFolder; |
| 322 | |
| 323 | const auto qmlModuleEntry = |
| 324 | qmlModuleEntryFromBuildPath(mapper, pathInBuildFolder, type: LocalFileFilter); |
| 325 | if (!qmlModuleEntry.isValid()) |
| 326 | return pathInBuildFolder; |
| 327 | const QString qrcPath = qmlModuleEntry.resourcePath |
| 328 | + QStringView(pathInBuildFolder).sliced(pos: qmlModuleEntry.filePath.size()); |
| 329 | |
| 330 | const auto entry = mapper->entry(filter: QQmlJSResourceFileMapper::resourceFileFilter(file: qrcPath)); |
| 331 | return entry.isValid()? entry.filePath : pathInBuildFolder; |
| 332 | } |
| 333 | |
| 334 | /*! |
| 335 | \internal |
| 336 | Obtains the source folder path from a build folder QML file path via the passed \c mapper, see also |
| 337 | \l QQmlJSUtils::qmlSourcePathFromBuildPath. |
| 338 | */ |
| 339 | QString QQmlJSUtils::qmlBuildPathFromSourcePath(const QQmlJSResourceFileMapper *mapper, |
| 340 | const QString &pathInSourceFolder) |
| 341 | { |
| 342 | if (!mapper) |
| 343 | return pathInSourceFolder; |
| 344 | |
| 345 | const QString qrcPath = |
| 346 | mapper->entry(filter: QQmlJSResourceFileMapper::localFileFilter(file: pathInSourceFolder)) |
| 347 | .resourcePath; |
| 348 | |
| 349 | if (qrcPath.isEmpty()) |
| 350 | return pathInSourceFolder; |
| 351 | |
| 352 | const auto moduleBuildEntry = |
| 353 | qmlModuleEntryFromBuildPath(mapper, pathInBuildFolder: qrcPath, type: ResourceFileFilter); |
| 354 | |
| 355 | if (!moduleBuildEntry.isValid()) |
| 356 | return pathInSourceFolder; |
| 357 | |
| 358 | const auto qrcFolderPath = qrcPath.first(n: qrcPath.lastIndexOf(c: u'/')); // drop the filename |
| 359 | |
| 360 | return moduleBuildEntry.filePath + qrcFolderPath.sliced(pos: moduleBuildEntry.resourcePath.size()) |
| 361 | + pathInSourceFolder.sliced(pos: pathInSourceFolder.lastIndexOf(c: u'/')); |
| 362 | } |
| 363 | |
| 364 | QT_END_NAMESPACE |
| 365 | |