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
252/*!
253 \internal
254
255 Utility method that searches qrc files in given folders. Do not use this when the order or
256 selection of the returned qrc files matters.
257*/
258QStringList QQmlJSUtils::resourceFilesFromBuildFolders(const QStringList &buildFolders)
259{
260 QStringList result;
261 for (const QString &path : buildFolders) {
262 for (auto it : QDirListing{ path, QStringList{ u"*.qrc"_s },
263 QDirListing::IteratorFlag::Recursive
264 | QDirListing::IteratorFlag::FilesOnly
265 | QDirListing::IteratorFlag::IncludeHidden }) {
266 result.append(it.filePath());
267 }
268 }
269 return result;
270}
271
276
277/*!
278\internal
279Obtain a QML module qrc entry from its qmldir entry.
280
281Contains a heuristic for QML modules without nested-qml-module-with-prefer-feature
282that tries to find a parent directory that contains a qmldir entry in the qrc.
283*/
285qmlModuleEntryFromBuildPath(const QQmlJSResourceFileMapper *mapper,
286 const QString &pathInBuildFolder, FilterType type)
287{
288 const QString cleanPath = QDir::cleanPath(pathInBuildFolder);
289 QStringView directoryPath = cleanPath;
290
291 while (!directoryPath.isEmpty()) {
292 const qsizetype lastSlashIndex = directoryPath.lastIndexOf(u'/');
293 if (lastSlashIndex == -1)
294 return {};
295
296 directoryPath.truncate(lastSlashIndex);
297 const QString qmldirPath = u"%1/qmldir"_s.arg(directoryPath);
298 const QQmlJSResourceFileMapper::Filter qmldirFilter = type == LocalFileFilter
299 ? QQmlJSResourceFileMapper::localFileFilter(qmldirPath)
300 : QQmlJSResourceFileMapper::resourceFileFilter(qmldirPath);
301
302 QQmlJSResourceFileMapper::Entry result = mapper->entry(qmldirFilter);
303 if (result.isValid()) {
304 result.resourcePath.chop(std::char_traits<char>::length("/qmldir"));
305 result.filePath.chop(std::char_traits<char>::length("/qmldir"));
306 return result;
307 }
308 }
309 return {};
310}
311
312/*!
313\internal
314Obtains the source folder path from a build folder QML file path via the passed \c mapper.
315
316This works on proper QML modules when using the nested-qml-module-with-prefer-feature
317from 6.8 and uses a heuristic when the qmldir with the prefer entry is missing.
318*/
319QString QQmlJSUtils::qmlSourcePathFromBuildPath(const QQmlJSResourceFileMapper *mapper,
320 const QString &pathInBuildFolder)
321{
322 if (!mapper)
323 return pathInBuildFolder;
324
325 const auto qmlModuleEntry =
326 qmlModuleEntryFromBuildPath(mapper, pathInBuildFolder, LocalFileFilter);
327 if (!qmlModuleEntry.isValid())
328 return pathInBuildFolder;
329 const QString qrcPath = qmlModuleEntry.resourcePath
330 + QStringView(pathInBuildFolder).sliced(qmlModuleEntry.filePath.size());
331
332 const auto entry = mapper->entry(QQmlJSResourceFileMapper::resourceFileFilter(qrcPath));
333 return entry.isValid()? entry.filePath : pathInBuildFolder;
334}
335
336/*!
337\internal
338Obtains the source folder path from a build folder QML file path via the passed \c mapper, see also
339\l QQmlJSUtils::qmlSourcePathFromBuildPath.
340*/
341QString QQmlJSUtils::qmlBuildPathFromSourcePath(const QQmlJSResourceFileMapper *mapper,
342 const QString &pathInSourceFolder)
343{
344 if (!mapper)
345 return pathInSourceFolder;
346
347 const QString qrcPath =
348 mapper->entry(QQmlJSResourceFileMapper::localFileFilter(pathInSourceFolder))
349 .resourcePath;
350
351 if (qrcPath.isEmpty())
352 return pathInSourceFolder;
353
354 const auto moduleBuildEntry =
355 qmlModuleEntryFromBuildPath(mapper, qrcPath, ResourceFileFilter);
356
357 if (!moduleBuildEntry.isValid())
358 return pathInSourceFolder;
359
360 const auto qrcFolderPath = qrcPath.first(qrcPath.lastIndexOf(u'/')); // drop the filename
361
362 return moduleBuildEntry.filePath + qrcFolderPath.sliced(moduleBuildEntry.resourcePath.size())
363 + pathInSourceFolder.sliced(pathInSourceFolder.lastIndexOf(u'/'));
364}
365
366/*!
367 \internal
368 Returns the name of \a scope based on \a type.
369*/
370QString QQmlJSUtils::getScopeName(const QQmlJSScope::ConstPtr &scope, QQmlJSScope::ScopeType type)
371{
372 Q_ASSERT(scope);
373 if (type == QQmlSA::ScopeType::GroupedPropertyScope
374 || type == QQmlSA::ScopeType::AttachedPropertyScope)
375 return scope->internalName();
376
377 if (!scope->isComposite())
378 return scope->internalName();
379
380 if (scope->isInlineComponent() && scope->inlineComponentName().has_value())
381 return scope->inlineComponentName().value();
382
383 if (scope->isFileRootComponent())
384 return QFileInfo(scope->filePath()).baseName();
385
386 return scope->baseTypeName();
387}
388
389
390QT_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)