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
19namespace QQmlJSUtils {
20
21/*! \internal
22
23 Fully resolves alias \a property and returns the information about the
24 origin, which is not an alias.
25*/
26template <typename ScopeForId>
27static ResolvedAlias resolveAlias(ScopeForId scopeForId, const QQmlJSMetaProperty &property,
28 const QQmlJSScope::ConstPtr &owner,
29 const AliasResolutionVisitor &visitor)
30{
31 Q_ASSERT(property.isAlias());
32 Q_ASSERT(owner);
33
34 ResolvedAlias result{ };
35 result.owner = owner;
36
37 // TODO: one could optimize the generated alias code for aliases pointing to aliases
38 // e.g., if idA.myAlias -> idB.myAlias2 -> idC.myProp, then one could directly generate
39 // idA.myProp as pointing to idC.myProp. // This gets complicated when idB.myAlias is in a different Component than where the
40 // idA.myAlias is defined: scopeForId currently only contains the ids of the current
41 // component and alias resolution on the ids of a different component fails then.
42 if (QQmlJSMetaProperty nextProperty = property; nextProperty.isAlias()) {
43 QQmlJSScope::ConstPtr resultOwner = result.owner;
44 result = ResolvedAlias{ };
45
46 visitor.reset();
47
48 auto aliasExprBits = nextProperty.aliasExpression().split(u'.');
49 // do not crash on invalid aliasexprbits when accessing aliasExprBits[0]
50 if (aliasExprBits.size() < 1)
51 return {};
52
53 // resolve id first:
54 resultOwner = scopeForId(aliasExprBits[0], resultOwner);
55 if (!resultOwner)
56 return {};
57
58 visitor.processResolvedId(resultOwner);
59
60 aliasExprBits.removeFirst(); // Note: for simplicity, remove the <id>
61 result.owner = resultOwner;
63
64 for (const QString &bit : std::as_const(aliasExprBits)) {
65 nextProperty = resultOwner->property(bit);
66 if (!nextProperty.isValid())
67 return {};
68
69 visitor.processResolvedProperty(nextProperty, resultOwner);
70
71 result.property = nextProperty;
72 result.owner = resultOwner;
73 result.kind = AliasTarget_Property;
74
75 resultOwner = nextProperty.type();
76 }
77 }
78
79 return result;
80}
81
82ResolvedAlias resolveAlias(const QQmlJSTypeResolver *typeResolver,
83 const QQmlJSMetaProperty &property, const QQmlJSScope::ConstPtr &owner,
84 const AliasResolutionVisitor &visitor)
85{
86 return resolveAlias(
87 [&](const QString &id, const QQmlJSScope::ConstPtr &referrer) {
88 return typeResolver->typeForId(referrer, id);
89 },
90 property, owner, visitor);
91}
92
93ResolvedAlias resolveAlias(const QQmlJSScopesById &idScopes, 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> didYouMean(const QString &userInput, QStringList candidates,
105 const QString &filename,
106 const 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 { QQmlJSDocumentEdit{ filename, location, 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>
168sourceDirectoryPath(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*/
211bool canStrictlyCompareWithVar(
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*/
228bool canCompareWithQObject(
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*/
244bool canCompareWithQUrl(
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 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 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 qmlBuildPathFromSourcePath(const QQmlJSResourceFileMapper *mapper,
342 const QString &pathInSourceFolder)
343{
344 if (!mapper)
345 return pathInSourceFolder;
346
347 // A source file may appear under multiple resource paths across different qrc files.
348 // Iterate all entries to find the one that resolves to a module build path.
349 const auto allEntries =
350 mapper->filter(QQmlJSResourceFileMapper::localFileFilter(pathInSourceFolder));
351
352 for (const auto &mapEntry : allEntries) {
353 const QString qrcPath = mapEntry.resourcePath;
354 if (qrcPath.isEmpty())
355 continue;
356
357 const auto moduleBuildEntry =
358 qmlModuleEntryFromBuildPath(mapper, qrcPath, ResourceFileFilter);
359 if (!moduleBuildEntry.isValid())
360 continue;
361
362 const auto qrcFolderPath = qrcPath.first(qrcPath.lastIndexOf(u'/'));
363 return moduleBuildEntry.filePath + qrcFolderPath.sliced(moduleBuildEntry.resourcePath.size())
364 + pathInSourceFolder.sliced(pathInSourceFolder.lastIndexOf(u'/'));
365 }
366
367 return pathInSourceFolder;
368}
369
370/*!
371 \internal
372 Returns the name of \a scope based on \a type.
373*/
374QString getScopeName(const QQmlJSScope::ConstPtr &scope, QQmlJSScope::ScopeType type)
375{
376 Q_ASSERT(scope);
379 return scope->internalName();
380
381 if (!scope->isComposite())
382 return scope->internalName();
383
384 if (scope->isInlineComponent() && scope->inlineComponentName().has_value())
385 return scope->inlineComponentName().value();
386
387 if (scope->isFileRootComponent())
388 return QFileInfo(scope->filePath()).baseName();
389
390 return scope->baseTypeName();
391}
392
393QString fileSelectorFor(const QQmlJSScope::ConstPtr &scope)
394{
395 if (!scope)
396 return { };
397 const auto factory = scope.factory();
398 const QString filePath = factory ? factory->filePath() : scope->filePath();
399 QString dir = QFileInfo(filePath).dir().dirName();
400 if (dir.startsWith("+"_L1)) {
401 dir.slice(1);
402 return dir;
403 }
404 return { };
405}
406
407} // namespace QQmlJSUtils
408
409QT_END_NAMESPACE
static ResolvedAlias resolveAlias(ScopeForId scopeForId, const QQmlJSMetaProperty &property, const QQmlJSScope::ConstPtr &owner, const AliasResolutionVisitor &visitor)
static QQmlJSResourceFileMapper::Entry qmlModuleEntryFromBuildPath(const QQmlJSResourceFileMapper *mapper, const QString &pathInBuildFolder, FilterType type)
\inmodule QtQmlCompiler
ResolvedAliasTarget kind