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