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

Provided by KDAB

Privacy Policy
Learn Advanced QML with KDAB
Find out more

source code of qtdeclarative/src/qmlcompiler/qqmljsutils.cpp