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
279Finds the qrc folder that contains the "root" qmldir file of a QML Module.
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
284\note This assumes that the build folder structure follows the qrc folder structure.
285*/
287qmlModuleEntryFromBuildPath(const QQmlJSResourceFileMapper *mapper,
288 const QString &pathInBuildFolder, FilterType type)
289{
290 const QString cleanPath = QDir::cleanPath(pathInBuildFolder);
291 QStringView directoryPath = cleanPath;
292
293 while (!directoryPath.isEmpty()) {
294 const qsizetype lastSlashIndex = directoryPath.lastIndexOf(u'/');
295 if (lastSlashIndex == -1)
296 return {};
297
298 directoryPath.truncate(lastSlashIndex);
299 const QString qmldirPath = u"%1/qmldir"_s.arg(directoryPath);
300 const QQmlJSResourceFileMapper::Filter qmldirFilter = type == LocalFileFilter
301 ? QQmlJSResourceFileMapper::localFileFilter(qmldirPath)
302 : QQmlJSResourceFileMapper::resourceFileFilter(qmldirPath);
303
304 QQmlJSResourceFileMapper::Entry result = mapper->entry(qmldirFilter);
305 if (result.isValid()) {
306 result.resourcePath.chop(std::char_traits<char>::length("/qmldir"));
307 result.filePath.chop(std::char_traits<char>::length("/qmldir"));
308 return result;
309 }
310 }
311 return {};
312}
313
314/*!
315\internal
316Obtains the source path from a build path for a QML file via the passed \c mapper.
317
318We assume here that the QML Module file-structure in the build folder follows the qrc-structure.
319The build folder contains two qrc mappings:
320\list
321 \li A mapping from build folder qmldirs to their qrc paths
322 \li A mapping from source folder .qml files to their qrc paths
323\endlist
324The build folder path is traveled up until a parent folder was found that contains a qmldir file.
325The relative path from the parent folder with the qmldir file to the QML file's build path
326is resolved from the the qrc path of the qmldir obtained from the mapper to obtain the
327absolute qrc path.
328The QML file's source path is obtained by mapping the constructed qrc path back to a
329file system path.
330
331This works on proper QML modules when using the nested-qml-module-with-prefer-feature
332from 6.8 and uses a heuristic when the qmldir with the prefer entry is missing.
333*/
334QString qmlSourcePathFromBuildPath(const QQmlJSResourceFileMapper *mapper,
335 const QString &pathInBuildFolder)
336{
337 if (!mapper)
338 return pathInBuildFolder;
339
340 // Slice the path the mapper matched against: its entries are absolute and clean.
341 const QString buildPath = QDir::cleanPath(QFileInfo(pathInBuildFolder).absoluteFilePath());
342 const auto qmlModuleEntry = qmlModuleEntryFromBuildPath(mapper, buildPath, LocalFileFilter);
343 if (!qmlModuleEntry.isValid())
344 return pathInBuildFolder;
345 const QString qrcPath = qmlModuleEntry.resourcePath
346 + QStringView(buildPath).sliced(qmlModuleEntry.filePath.size());
347
348 const auto entry = mapper->entry(QQmlJSResourceFileMapper::resourceFileFilter(qrcPath));
349 return entry.isValid()? entry.filePath : pathInBuildFolder;
350}
351
352/*!
353\internal
354Obtains the source folder path from a build folder QML file path via the passed \c mapper, see also
355\l QQmlJSUtils::qmlSourcePathFromBuildPath.
356
357We assume here that the QML Module file-structure in the build folder follows the qrc-structure.
358The build folder contains two qrc mappings:
359\list
360 \li A mapping from build folder qmldirs to their qrc paths
361 \li A mapping from source folder .qml files to their qrc paths
362\endlist
363The source folder path is first mapped to the .qrc path via the mapper. The .qrc path
364is traveled up until a parent folder containing a qmldir file was found. The build path
365is constructed from the qmldir's build path and the relative qrc path of the QML file.
366*/
367QString qmlBuildPathFromSourcePath(const QQmlJSResourceFileMapper *mapper,
368 const QString &pathInSourceFolder)
369{
370 if (!mapper)
371 return pathInSourceFolder;
372
373 // A source file may appear under multiple resource paths across different qrc files.
374 // Iterate all entries to find the one that resolves to a module build path.
375 const auto allEntries =
376 mapper->filter(QQmlJSResourceFileMapper::localFileFilter(pathInSourceFolder));
377
378 for (const auto &mapEntry : allEntries) {
379 const QString qrcPath = mapEntry.resourcePath;
380 if (qrcPath.isEmpty())
381 continue;
382
383 const auto moduleBuildEntry =
384 qmlModuleEntryFromBuildPath(mapper, qrcPath, ResourceFileFilter);
385 if (!moduleBuildEntry.isValid())
386 continue;
387
388 return moduleBuildEntry.filePath + qrcPath.sliced(moduleBuildEntry.resourcePath.size());
389 }
390
391 return pathInSourceFolder;
392}
393
394/*!
395 \internal
396 Returns the name of \a scope based on \a type.
397*/
398QString getScopeName(const QQmlJSScope::ConstPtr &scope, QQmlJSScope::ScopeType type)
399{
400 Q_ASSERT(scope);
403 return scope->internalName();
404
405 if (!scope->isComposite())
406 return scope->internalName();
407
408 if (scope->isInlineComponent() && scope->inlineComponentName().has_value())
409 return scope->inlineComponentName().value();
410
411 if (scope->isFileRootComponent())
412 return QFileInfo(scope->filePath()).baseName();
413
414 return scope->baseTypeName();
415}
416
417QString fileSelectorFor(const QQmlJSScope::ConstPtr &scope)
418{
419 if (!scope)
420 return { };
421 const auto factory = scope.factory();
422 const QString filePath = factory ? factory->filePath() : scope->filePath();
423 QString dir = QFileInfo(filePath).dir().dirName();
424 if (dir.startsWith("+"_L1)) {
425 dir.slice(1);
426 return dir;
427 }
428 return { };
429}
430
431} // namespace QQmlJSUtils
432
433QT_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