Revision control
Copy as Markdown
// 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