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
257QStringList QQmlJSUtils::resourceFilesFromBuildFolders(const QStringList &buildFolders)
258{
259 QStringList result;
260 for (const QString &path : buildFolders) {
261 QDirIterator it(path, QStringList{ u"*.qrc"_s }, QDir::Files | QDir::Hidden,
262 QDirIterator::Subdirectories);
263 while (it.hasNext()) {
264 result.append(t: it.next());
265 }
266 }
267 return result;
268}
269
270enum FilterType {
271 LocalFileFilter,
272 ResourceFileFilter
273};
274
275/*!
276\internal
277Obtain a QML module qrc entry from its qmldir entry.
278
279Contains a heuristic for QML modules without nested-qml-module-with-prefer-feature
280that tries to find a parent directory that contains a qmldir entry in the qrc.
281*/
282static QQmlJSResourceFileMapper::Entry
283qmlModuleEntryFromBuildPath(const QQmlJSResourceFileMapper *mapper,
284 const QString &pathInBuildFolder, FilterType type)
285{
286 const QString cleanPath = QDir::cleanPath(path: pathInBuildFolder);
287 QStringView directoryPath = cleanPath;
288
289 while (!directoryPath.isEmpty()) {
290 const qsizetype lastSlashIndex = directoryPath.lastIndexOf(c: u'/');
291 if (lastSlashIndex == -1)
292 return {};
293
294 directoryPath.truncate(n: lastSlashIndex);
295 const QString qmldirPath = u"%1/qmldir"_s.arg(a: directoryPath);
296 const QQmlJSResourceFileMapper::Filter qmldirFilter = type == LocalFileFilter
297 ? QQmlJSResourceFileMapper::localFileFilter(file: qmldirPath)
298 : QQmlJSResourceFileMapper::resourceFileFilter(file: qmldirPath);
299
300 QQmlJSResourceFileMapper::Entry result = mapper->entry(filter: qmldirFilter);
301 if (result.isValid()) {
302 result.resourcePath.chop(n: std::char_traits<char>::length(s: "/qmldir"));
303 result.filePath.chop(n: std::char_traits<char>::length(s: "/qmldir"));
304 return result;
305 }
306 }
307 return {};
308}
309
310/*!
311\internal
312Obtains the source folder path from a build folder QML file path via the passed \c mapper.
313
314This works on proper QML modules when using the nested-qml-module-with-prefer-feature
315from 6.8 and uses a heuristic when the qmldir with the prefer entry is missing.
316*/
317QString QQmlJSUtils::qmlSourcePathFromBuildPath(const QQmlJSResourceFileMapper *mapper,
318 const QString &pathInBuildFolder)
319{
320 if (!mapper)
321 return pathInBuildFolder;
322
323 const auto qmlModuleEntry =
324 qmlModuleEntryFromBuildPath(mapper, pathInBuildFolder, type: LocalFileFilter);
325 if (!qmlModuleEntry.isValid())
326 return pathInBuildFolder;
327 const QString qrcPath = qmlModuleEntry.resourcePath
328 + QStringView(pathInBuildFolder).sliced(pos: qmlModuleEntry.filePath.size());
329
330 const auto entry = mapper->entry(filter: QQmlJSResourceFileMapper::resourceFileFilter(file: qrcPath));
331 return entry.isValid()? entry.filePath : pathInBuildFolder;
332}
333
334/*!
335\internal
336Obtains the source folder path from a build folder QML file path via the passed \c mapper, see also
337\l QQmlJSUtils::qmlSourcePathFromBuildPath.
338*/
339QString QQmlJSUtils::qmlBuildPathFromSourcePath(const QQmlJSResourceFileMapper *mapper,
340 const QString &pathInSourceFolder)
341{
342 if (!mapper)
343 return pathInSourceFolder;
344
345 const QString qrcPath =
346 mapper->entry(filter: QQmlJSResourceFileMapper::localFileFilter(file: pathInSourceFolder))
347 .resourcePath;
348
349 if (qrcPath.isEmpty())
350 return pathInSourceFolder;
351
352 const auto moduleBuildEntry =
353 qmlModuleEntryFromBuildPath(mapper, pathInBuildFolder: qrcPath, type: ResourceFileFilter);
354
355 if (!moduleBuildEntry.isValid())
356 return pathInSourceFolder;
357
358 const auto qrcFolderPath = qrcPath.first(n: qrcPath.lastIndexOf(c: u'/')); // drop the filename
359
360 return moduleBuildEntry.filePath + qrcFolderPath.sliced(pos: moduleBuildEntry.resourcePath.size())
361 + pathInSourceFolder.sliced(pos: pathInSourceFolder.lastIndexOf(c: u'/'));
362}
363
364QT_END_NAMESPACE
365

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