diff options
Diffstat (limited to 'src/qmlcompiler/qqmljsutils.cpp')
-rw-r--r-- | src/qmlcompiler/qqmljsutils.cpp | 254 |
1 files changed, 254 insertions, 0 deletions
diff --git a/src/qmlcompiler/qqmljsutils.cpp b/src/qmlcompiler/qqmljsutils.cpp new file mode 100644 index 0000000000..c6eb09b313 --- /dev/null +++ b/src/qmlcompiler/qqmljsutils.cpp @@ -0,0 +1,254 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "qqmljsutils_p.h" +#include "qqmljstyperesolver_p.h" +#include "qqmljsscopesbyid_p.h" + +#include <algorithm> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +/*! \internal + + Fully resolves alias \a property and returns the information about the + origin, which is not an alias. +*/ +template<typename ScopeForId> +static QQmlJSUtils::ResolvedAlias +resolveAlias(ScopeForId scopeForId, const QQmlJSMetaProperty &property, + const QQmlJSScope::ConstPtr &owner, const QQmlJSUtils::AliasResolutionVisitor &visitor) +{ + Q_ASSERT(property.isAlias()); + Q_ASSERT(owner); + + QQmlJSUtils::ResolvedAlias result {}; + result.owner = owner; + + // TODO: one could optimize the generated alias code for aliases pointing to aliases + // e.g., if idA.myAlias -> idB.myAlias2 -> idC.myProp, then one could directly generate + // idA.myProp as pointing to idC.myProp. + // This gets complicated when idB.myAlias is in a different Component than where the + // idA.myAlias is defined: scopeForId currently only contains the ids of the current + // component and alias resolution on the ids of a different component fails then. + if (QQmlJSMetaProperty nextProperty = property; nextProperty.isAlias()) { + QQmlJSScope::ConstPtr resultOwner = result.owner; + result = QQmlJSUtils::ResolvedAlias {}; + + visitor.reset(); + + auto aliasExprBits = nextProperty.aliasExpression().split(u'.'); + // do not crash on invalid aliasexprbits when accessing aliasExprBits[0] + if (aliasExprBits.size() < 1) + return {}; + + // resolve id first: + resultOwner = scopeForId(aliasExprBits[0], resultOwner); + if (!resultOwner) + return {}; + + visitor.processResolvedId(resultOwner); + + aliasExprBits.removeFirst(); // Note: for simplicity, remove the <id> + result.owner = resultOwner; + result.kind = QQmlJSUtils::AliasTarget_Object; + + for (const QString &bit : std::as_const(aliasExprBits)) { + nextProperty = resultOwner->property(bit); + if (!nextProperty.isValid()) + return {}; + + visitor.processResolvedProperty(nextProperty, resultOwner); + + result.property = nextProperty; + result.owner = resultOwner; + result.kind = QQmlJSUtils::AliasTarget_Property; + + resultOwner = nextProperty.type(); + } + } + + return result; +} + +QQmlJSUtils::ResolvedAlias QQmlJSUtils::resolveAlias(const QQmlJSTypeResolver *typeResolver, + const QQmlJSMetaProperty &property, + const QQmlJSScope::ConstPtr &owner, + const AliasResolutionVisitor &visitor) +{ + return ::resolveAlias( + [&](const QString &id, const QQmlJSScope::ConstPtr &referrer) { + const QQmlJSRegisterContent content = typeResolver->scopedType(referrer, id); + if (content.variant() == QQmlJSRegisterContent::ObjectById) + return content.type(); + return QQmlJSScope::ConstPtr(); + }, + property, owner, visitor); +} + +QQmlJSUtils::ResolvedAlias QQmlJSUtils::resolveAlias(const QQmlJSScopesById &idScopes, + const QQmlJSMetaProperty &property, + const QQmlJSScope::ConstPtr &owner, + const AliasResolutionVisitor &visitor) +{ + return ::resolveAlias( + [&](const QString &id, const QQmlJSScope::ConstPtr &referrer) { + return idScopes.scope(id, referrer); + }, + property, owner, visitor); +} + +std::optional<QQmlJSFixSuggestion> QQmlJSUtils::didYouMean(const QString &userInput, + QStringList candidates, + QQmlJS::SourceLocation location) +{ + QString shortestDistanceWord; + int shortestDistance = userInput.size(); + + // Most of the time the candidates are keys() from QHash, which means that + // running this function in the seemingly same setup might yield different + // best cadidate (e.g. imagine a typo 'thing' with candidates 'thingA' vs + // 'thingB'). This is especially flaky in e.g. test environment where the + // results may differ (even when the global hash seed is fixed!) when + // running one test vs the whole test suite (recall platform-dependent + // QSKIPs). There could be user-visible side effects as well, so just sort + // the candidates to guarantee consistent results + std::sort(candidates.begin(), candidates.end()); + + for (const QString &candidate : candidates) { + /* + * Calculate the distance between the userInput and candidate using Damerau–Levenshtein + * Roughly based on + * https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows. + */ + QList<int> v0(candidate.size() + 1); + QList<int> v1(candidate.size() + 1); + + std::iota(v0.begin(), v0.end(), 0); + + for (qsizetype i = 0; i < userInput.size(); i++) { + v1[0] = i + 1; + for (qsizetype j = 0; j < candidate.size(); j++) { + int deletionCost = v0[j + 1] + 1; + int insertionCost = v1[j] + 1; + int substitutionCost = userInput[i] == candidate[j] ? v0[j] : v0[j] + 1; + v1[j + 1] = std::min({ deletionCost, insertionCost, substitutionCost }); + } + std::swap(v0, v1); + } + + int distance = v0[candidate.size()]; + if (distance < shortestDistance) { + shortestDistanceWord = candidate; + shortestDistance = distance; + } + } + + if (shortestDistance + < std::min(std::max(userInput.size() / 2, qsizetype(3)), userInput.size())) { + return QQmlJSFixSuggestion { + u"Did you mean \"%1\"?"_s.arg(shortestDistanceWord), + location, + shortestDistanceWord + }; + } else { + return {}; + } +} + +/*! \internal + + Returns a corresponding source directory path for \a buildDirectoryPath + Returns empty string on error +*/ +std::variant<QString, QQmlJS::DiagnosticMessage> +QQmlJSUtils::sourceDirectoryPath(const QQmlJSImporter *importer, const QString &buildDirectoryPath) +{ + const auto makeError = [](const QString &msg) { + return QQmlJS::DiagnosticMessage { msg, QtWarningMsg, QQmlJS::SourceLocation() }; + }; + + if (!importer->metaDataMapper()) + return makeError(u"QQmlJSImporter::metaDataMapper() is nullptr"_s); + + // for now, meta data contains just a single entry + QQmlJSResourceFileMapper::Filter matchAll { QString(), QStringList(), + QQmlJSResourceFileMapper::Directory + | QQmlJSResourceFileMapper::Recurse }; + QQmlJSResourceFileMapper::Entry entry = importer->metaDataMapper()->entry(matchAll); + if (!entry.isValid()) + return makeError(u"Failed to find meta data entry in QQmlJSImporter::metaDataMapper()"_s); + if (!buildDirectoryPath.startsWith(entry.filePath)) // assume source directory path already + return makeError(u"The module output directory does not match the build directory path"_s); + + QString qrcPath = buildDirectoryPath; + qrcPath.remove(0, entry.filePath.size()); + qrcPath.prepend(entry.resourcePath); + qrcPath.remove(0, 1); // remove extra "/" + + const QStringList sourceDirPaths = importer->resourceFileMapper()->filePaths( + QQmlJSResourceFileMapper::resourceFileFilter(qrcPath)); + if (sourceDirPaths.size() != 1) { + const QString matchedPaths = + sourceDirPaths.isEmpty() ? u"<none>"_s : sourceDirPaths.join(u", "); + return makeError( + QStringLiteral("QRC path %1 (deduced from %2) has unexpected number of mappings " + "(%3). File paths that matched:\n%4") + .arg(qrcPath, buildDirectoryPath, QString::number(sourceDirPaths.size()), + matchedPaths)); + } + return sourceDirPaths[0]; +} + +/*! \internal + + Utility method that checks if one of the registers is var, and the other can be + efficiently compared to it +*/ +bool canStrictlyCompareWithVar( + const QQmlJSTypeResolver *typeResolver, const QQmlJSScope::ConstPtr &lhsType, + const QQmlJSScope::ConstPtr &rhsType) +{ + Q_ASSERT(typeResolver); + + const QQmlJSScope::ConstPtr varType = typeResolver->varType(); + const bool leftIsVar = typeResolver->equals(lhsType, varType); + const bool righttIsVar = typeResolver->equals(rhsType, varType); + return leftIsVar != righttIsVar; +} + +/*! \internal + + Utility method that checks if one of the registers is qobject, and the other can be + efficiently compared to it +*/ +bool canCompareWithQObject( + const QQmlJSTypeResolver *typeResolver, const QQmlJSScope::ConstPtr &lhsType, + const QQmlJSScope::ConstPtr &rhsType) +{ + Q_ASSERT(typeResolver); + return (lhsType->isReferenceType() + && (rhsType->isReferenceType() + || typeResolver->equals(rhsType, typeResolver->nullType()))) + || (rhsType->isReferenceType() + && (lhsType->isReferenceType() + || typeResolver->equals(lhsType, typeResolver->nullType()))); +} + +/*! \internal + + Utility method that checks if both sides are QUrl type. In future, that might be extended to + support comparison with other types i.e QUrl vs string +*/ +bool canCompareWithQUrl( + const QQmlJSTypeResolver *typeResolver, const QQmlJSScope::ConstPtr &lhsType, + const QQmlJSScope::ConstPtr &rhsType) +{ + Q_ASSERT(typeResolver); + return typeResolver->equals(lhsType, typeResolver->urlType()) + && typeResolver->equals(rhsType, typeResolver->urlType()); +} + +QT_END_NAMESPACE |