Revision control

++ // Copyright (C) 2022 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
++ // Qt-Security score:significant
++
++ #include "qqmljsutils_p.h"
++ #include "qqmljstyperesolver_p.h"
++ #include "qqmljsscopesbyid_p.h"
++
++ #include <QtCore/qvarlengtharray.h>
++ #include <QtCore/qdir.h>
++ #include <QtCore/qdiriterator.h>
++
++ #include <algorithm>
++
++ QT_BEGIN_NAMESPACE
++
++ using namespace Qt::StringLiterals;
++
++ namespace QQmlJSUtils {
++
++ /*! \internal
++
++ Fully resolves alias \a property and returns the information about the
++ origin, which is not an alias.
++ */
++ template <typename ScopeForId>
++ static ResolvedAlias resolveAlias(ScopeForId scopeForId, const QQmlJSMetaProperty &property,
++ const QQmlJSScope::ConstPtr &owner,
++ const AliasResolutionVisitor &visitor)
++ {
++ Q_ASSERT(property.isAlias());
++ Q_ASSERT(owner);
++
++ 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 = 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 = 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 = AliasTarget_Property;
++
++ resultOwner = nextProperty.type();
++ }
++ }
++
++ return result;
++ }
++
++ ResolvedAlias resolveAlias(const QQmlJSTypeResolver *typeResolver,
++ const QQmlJSMetaProperty &property, const QQmlJSScope::ConstPtr &owner,
++ const AliasResolutionVisitor &visitor)
++ {
++ return resolveAlias(
++ [&](const QString &id, const QQmlJSScope::ConstPtr &referrer) {
++ return typeResolver->typeForId(referrer, id);
++ },
++ property, owner, visitor);
++ }
++
++ ResolvedAlias 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> didYouMean(const QString &userInput, QStringList candidates,
++ const QString &filename,
++ const 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
++ */
++ QVarLengthArray<int> v0(candidate.size() + 1);
++ QVarLengthArray<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,
++ { QQmlJSDocumentEdit{ filename, location, shortestDistanceWord } }
++ };
++ } else {
++ return {};
++ }
++ }
++
++ /*! \internal
++
++ Returns a corresponding source directory path for \a buildDirectoryPath
++ Returns empty string on error
++ */
++ std::variant<QString, QQmlJS::DiagnosticMessage>
++ 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 = (lhsType == varType);
++ const bool righttIsVar = (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() || rhsType == typeResolver->nullType()))
++ || (rhsType->isReferenceType()
++ && (lhsType->isReferenceType() || 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 lhsType == typeResolver->urlType() && rhsType == typeResolver->urlType();
++ }
++
++ /*!
++ \internal
++
++ Utility method that searches qrc files in given folders. Do not use this when the order or
++ selection of the returned qrc files matters.
++ */
++ QStringList resourceFilesFromBuildFolders(const QStringList &buildFolders)
++ {
++ QStringList result;
++ for (const QString &path : buildFolders) {
++ for (auto it : QDirListing{ path, QStringList{ u"*.qrc"_s },
++ QDirListing::IteratorFlag::Recursive
++ | QDirListing::IteratorFlag::FilesOnly
++ | QDirListing::IteratorFlag::IncludeHidden }) {
++ result.append(it.filePath());
++ }
++ }
++ return result;
++ }
++
++ enum FilterType {
++ LocalFileFilter,
++ ResourceFileFilter
++ };
++
++ /*!
++ \internal
++ Finds the qrc folder that contains the "root" qmldir file of a QML Module.
++
++ Contains a heuristic for QML modules without nested-qml-module-with-prefer-feature
++ that tries to find a parent directory that contains a qmldir entry in the qrc.
++
++ \note This assumes that the build folder structure follows the qrc folder structure.
++ */
++ static QQmlJSResourceFileMapper::Entry
++ qmlModuleEntryFromBuildPath(const QQmlJSResourceFileMapper *mapper,
++ const QString &pathInBuildFolder, FilterType type)
++ {
++ const QString cleanPath = QDir::cleanPath(pathInBuildFolder);
++ QStringView directoryPath = cleanPath;
++
++ while (!directoryPath.isEmpty()) {
++ const qsizetype lastSlashIndex = directoryPath.lastIndexOf(u'/');
++ if (lastSlashIndex == -1)
++ return {};
++
++ directoryPath.truncate(lastSlashIndex);
++ const QString qmldirPath = u"%1/qmldir"_s.arg(directoryPath);
++ const QQmlJSResourceFileMapper::Filter qmldirFilter = type == LocalFileFilter
++ ? QQmlJSResourceFileMapper::localFileFilter(qmldirPath)
++ : QQmlJSResourceFileMapper::resourceFileFilter(qmldirPath);
++
++ QQmlJSResourceFileMapper::Entry result = mapper->entry(qmldirFilter);
++ if (result.isValid()) {
++ result.resourcePath.chop(std::char_traits<char>::length("/qmldir"));
++ result.filePath.chop(std::char_traits<char>::length("/qmldir"));
++ return result;
++ }
++ }
++ return {};
++ }
++
++ /*!
++ \internal
++ Obtains the source path from a build path for a QML file via the passed \c mapper.
++
++ We assume here that the QML Module file-structure in the build folder follows the qrc-structure.
++ The build folder contains two qrc mappings:
++ \list
++ \li A mapping from build folder qmldirs to their qrc paths
++ \li A mapping from source folder .qml files to their qrc paths
++ \endlist
++ The build folder path is traveled up until a parent folder was found that contains a qmldir file.
++ The relative path from the parent folder with the qmldir file to the QML file's build path
++ is resolved from the the qrc path of the qmldir obtained from the mapper to obtain the
++ absolute qrc path.
++ The QML file's source path is obtained by mapping the constructed qrc path back to a
++ file system path.
++
++ This works on proper QML modules when using the nested-qml-module-with-prefer-feature
++ from 6.8 and uses a heuristic when the qmldir with the prefer entry is missing.
++ */
++ QString qmlSourcePathFromBuildPath(const QQmlJSResourceFileMapper *mapper,
++ const QString &pathInBuildFolder)
++ {
++ if (!mapper)
++ return pathInBuildFolder;
++
++ // Slice the path the mapper matched against: its entries are absolute and clean.
++ const QString buildPath = QDir::cleanPath(QFileInfo(pathInBuildFolder).absoluteFilePath());
++ const auto qmlModuleEntry = qmlModuleEntryFromBuildPath(mapper, buildPath, LocalFileFilter);
++ if (!qmlModuleEntry.isValid())
++ return pathInBuildFolder;
++ const QString qrcPath = qmlModuleEntry.resourcePath
++ + QStringView(buildPath).sliced(qmlModuleEntry.filePath.size());
++
++ const auto entry = mapper->entry(QQmlJSResourceFileMapper::resourceFileFilter(qrcPath));
++ return entry.isValid()? entry.filePath : pathInBuildFolder;
++ }
++
++ /*!
++ \internal
++ Obtains the source folder path from a build folder QML file path via the passed \c mapper, see also
++ \l QQmlJSUtils::qmlSourcePathFromBuildPath.
++
++ We assume here that the QML Module file-structure in the build folder follows the qrc-structure.
++ The build folder contains two qrc mappings:
++ \list
++ \li A mapping from build folder qmldirs to their qrc paths
++ \li A mapping from source folder .qml files to their qrc paths
++ \endlist
++ The source folder path is first mapped to the .qrc path via the mapper. The .qrc path
++ is traveled up until a parent folder containing a qmldir file was found. The build path
++ is constructed from the qmldir's build path and the relative qrc path of the QML file.
++ */
++ QString qmlBuildPathFromSourcePath(const QQmlJSResourceFileMapper *mapper,
++ const QString &pathInSourceFolder)
++ {
++ if (!mapper)
++ return pathInSourceFolder;
++
++ // A source file may appear under multiple resource paths across different qrc files.
++ // Iterate all entries to find the one that resolves to a module build path.
++ const auto allEntries =
++ mapper->filter(QQmlJSResourceFileMapper::localFileFilter(pathInSourceFolder));
++
++ for (const auto &mapEntry : allEntries) {
++ const QString qrcPath = mapEntry.resourcePath;
++ if (qrcPath.isEmpty())
++ continue;
++
++ const auto moduleBuildEntry =
++ qmlModuleEntryFromBuildPath(mapper, qrcPath, ResourceFileFilter);
++ if (!moduleBuildEntry.isValid())
++ continue;
++
++ return moduleBuildEntry.filePath + qrcPath.sliced(moduleBuildEntry.resourcePath.size());
++ }
++
++ return pathInSourceFolder;
++ }
++
++ /*!
++ \internal
++ Returns the name of \a scope based on \a type.
++ */
++ QString getScopeName(const QQmlJSScope::ConstPtr &scope, QQmlJSScope::ScopeType type)
++ {
++ Q_ASSERT(scope);
++ if (type == QQmlSA::ScopeType::GroupedPropertyScope
++ || type == QQmlSA::ScopeType::AttachedPropertyScope)
++ return scope->internalName();
++
++ if (!scope->isComposite())
++ return scope->internalName();
++
++ if (scope->isInlineComponent() && scope->inlineComponentName().has_value())
++ return scope->inlineComponentName().value();
++
++ if (scope->isFileRootComponent())
++ return QFileInfo(scope->filePath()).baseName();
++
++ return scope->baseTypeName();
++ }
++
++ QString fileSelectorFor(const QQmlJSScope::ConstPtr &scope)
++ {
++ if (!scope)
++ return { };
++ const auto factory = scope.factory();
++ const QString filePath = factory ? factory->filePath() : scope->filePath();
++ QString dir = QFileInfo(filePath).dir().dirName();
++ if (dir.startsWith("+"_L1)) {
++ dir.slice(1);
++ return dir;
++ }
++ return { };
++ }
++
++ } // namespace QQmlJSUtils
++
++ QT_END_NAMESPACE