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 return typeResolver->typeForId(scope: referrer, name: id);
87 },
88 property, owner, visitor);
89}
90
91QQmlJSUtils::ResolvedAlias QQmlJSUtils::resolveAlias(const QQmlJSScopesById &idScopes,
92 const QQmlJSMetaProperty &property,
93 const QQmlJSScope::ConstPtr &owner,
94 const AliasResolutionVisitor &visitor)
95{
96 return ::resolveAlias(
97 scopeForId: [&](const QString &id, const QQmlJSScope::ConstPtr &referrer) {
98 return idScopes.scope(id, referrer);
99 },
100 property, owner, visitor);
101}
102
103std::optional<QQmlJSFixSuggestion> QQmlJSUtils::didYouMean(const QString &userInput,
104 QStringList candidates,
105 QQmlJS::SourceLocation location)
106{
107 QString shortestDistanceWord;
108 int shortestDistance = userInput.size();
109
110 // Most of the time the candidates are keys() from QHash, which means that
111 // running this function in the seemingly same setup might yield different
112 // best cadidate (e.g. imagine a typo 'thing' with candidates 'thingA' vs
113 // 'thingB'). This is especially flaky in e.g. test environment where the
114 // results may differ (even when the global hash seed is fixed!) when
115 // running one test vs the whole test suite (recall platform-dependent
116 // QSKIPs). There could be user-visible side effects as well, so just sort
117 // the candidates to guarantee consistent results
118 std::sort(first: candidates.begin(), last: candidates.end());
119
120 for (const QString &candidate : candidates) {
121 /*
122 * Calculate the distance between the userInput and candidate using Damerau–Levenshtein
123 * Roughly based on
124 * https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows.
125 */
126 QVarLengthArray<int> v0(candidate.size() + 1);
127 QVarLengthArray<int> v1(candidate.size() + 1);
128
129 std::iota(first: v0.begin(), last: v0.end(), value: 0);
130
131 for (qsizetype i = 0; i < userInput.size(); i++) {
132 v1[0] = i + 1;
133 for (qsizetype j = 0; j < candidate.size(); j++) {
134 int deletionCost = v0[j + 1] + 1;
135 int insertionCost = v1[j] + 1;
136 int substitutionCost = userInput[i] == candidate[j] ? v0[j] : v0[j] + 1;
137 v1[j + 1] = std::min(l: { deletionCost, insertionCost, substitutionCost });
138 }
139 std::swap(a&: v0, b&: v1);
140 }
141
142 int distance = v0[candidate.size()];
143 if (distance < shortestDistance) {
144 shortestDistanceWord = candidate;
145 shortestDistance = distance;
146 }
147 }
148
149 if (shortestDistance
150 < std::min(a: std::max(a: userInput.size() / 2, b: qsizetype(3)), b: userInput.size())) {
151 return QQmlJSFixSuggestion {
152 u"Did you mean \"%1\"?"_s.arg(a: shortestDistanceWord),
153 location,
154 shortestDistanceWord
155 };
156 } else {
157 return {};
158 }
159}
160
161/*! \internal
162
163 Returns a corresponding source directory path for \a buildDirectoryPath
164 Returns empty string on error
165*/
166std::variant<QString, QQmlJS::DiagnosticMessage>
167QQmlJSUtils::sourceDirectoryPath(const QQmlJSImporter *importer, const QString &buildDirectoryPath)
168{
169 const auto makeError = [](const QString &msg) {
170 return QQmlJS::DiagnosticMessage { .message: msg, .type: QtWarningMsg, .loc: QQmlJS::SourceLocation() };
171 };
172
173 if (!importer->metaDataMapper())
174 return makeError(u"QQmlJSImporter::metaDataMapper() is nullptr"_s);
175
176 // for now, meta data contains just a single entry
177 QQmlJSResourceFileMapper::Filter matchAll { .path: QString(), .suffixes: QStringList(),
178 .flags: QQmlJSResourceFileMapper::Directory
179 | QQmlJSResourceFileMapper::Recurse };
180 QQmlJSResourceFileMapper::Entry entry = importer->metaDataMapper()->entry(filter: matchAll);
181 if (!entry.isValid())
182 return makeError(u"Failed to find meta data entry in QQmlJSImporter::metaDataMapper()"_s);
183 if (!buildDirectoryPath.startsWith(s: entry.filePath)) // assume source directory path already
184 return makeError(u"The module output directory does not match the build directory path"_s);
185
186 QString qrcPath = buildDirectoryPath;
187 qrcPath.remove(i: 0, len: entry.filePath.size());
188 qrcPath.prepend(s: entry.resourcePath);
189 qrcPath.remove(i: 0, len: 1); // remove extra "/"
190
191 const QStringList sourceDirPaths = importer->resourceFileMapper()->filePaths(
192 filter: QQmlJSResourceFileMapper::resourceFileFilter(file: qrcPath));
193 if (sourceDirPaths.size() != 1) {
194 const QString matchedPaths =
195 sourceDirPaths.isEmpty() ? u"<none>"_s : sourceDirPaths.join(sep: u", ");
196 return makeError(
197 QStringLiteral("QRC path %1 (deduced from %2) has unexpected number of mappings "
198 "(%3). File paths that matched:\n%4")
199 .arg(args&: qrcPath, args: buildDirectoryPath, args: QString::number(sourceDirPaths.size()),
200 args: matchedPaths));
201 }
202 return sourceDirPaths[0];
203}
204
205/*! \internal
206
207 Utility method that checks if one of the registers is var, and the other can be
208 efficiently compared to it
209*/
210bool canStrictlyCompareWithVar(
211 const QQmlJSTypeResolver *typeResolver, const QQmlJSScope::ConstPtr &lhsType,
212 const QQmlJSScope::ConstPtr &rhsType)
213{
214 Q_ASSERT(typeResolver);
215
216 const QQmlJSScope::ConstPtr varType = typeResolver->varType();
217 const bool leftIsVar = (lhsType == varType);
218 const bool righttIsVar = (rhsType == varType);
219 return leftIsVar != righttIsVar;
220}
221
222/*! \internal
223
224 Utility method that checks if one of the registers is qobject, and the other can be
225 efficiently compared to it
226*/
227bool canCompareWithQObject(
228 const QQmlJSTypeResolver *typeResolver, const QQmlJSScope::ConstPtr &lhsType,
229 const QQmlJSScope::ConstPtr &rhsType)
230{
231 Q_ASSERT(typeResolver);
232 return (lhsType->isReferenceType()
233 && (rhsType->isReferenceType() || rhsType == typeResolver->nullType()))
234 || (rhsType->isReferenceType()
235 && (lhsType->isReferenceType() || lhsType == typeResolver->nullType()));
236}
237
238/*! \internal
239
240 Utility method that checks if both sides are QUrl type. In future, that might be extended to
241 support comparison with other types i.e QUrl vs string
242*/
243bool canCompareWithQUrl(
244 const QQmlJSTypeResolver *typeResolver, const QQmlJSScope::ConstPtr &lhsType,
245 const QQmlJSScope::ConstPtr &rhsType)
246{
247 Q_ASSERT(typeResolver);
248 return lhsType == typeResolver->urlType() && rhsType == typeResolver->urlType();
249}
250
251QStringList QQmlJSUtils::resourceFilesFromBuildFolders(const QStringList &buildFolders)
252{
253 QStringList result;
254 for (const QString &path : buildFolders) {
255 QDirIterator it(path, QStringList{ u"*.qrc"_s }, QDir::Files | QDir::Hidden,
256 QDirIterator::Subdirectories);
257 while (it.hasNext()) {
258 result.append(t: it.next());
259 }
260 }
261 return result;
262}
263
264enum FilterType {
265 LocalFileFilter,
266 ResourceFileFilter
267};
268
269/*!
270\internal
271Obtain a QML module qrc entry from its qmldir entry.
272
273Contains a heuristic for QML modules without nested-qml-module-with-prefer-feature
274that tries to find a parent directory that contains a qmldir entry in the qrc.
275*/
276static QQmlJSResourceFileMapper::Entry
277qmlModuleEntryFromBuildPath(const QQmlJSResourceFileMapper *mapper,
278 const QString &pathInBuildFolder, FilterType type)
279{
280 const QString cleanPath = QDir::cleanPath(path: pathInBuildFolder);
281 QStringView directoryPath = cleanPath;
282
283 while (!directoryPath.isEmpty()) {
284 const qsizetype lastSlashIndex = directoryPath.lastIndexOf(c: u'/');
285 if (lastSlashIndex == -1)
286 return {};
287
288 directoryPath.truncate(n: lastSlashIndex);
289 const QString qmldirPath = u"%1/qmldir"_s.arg(a: directoryPath);
290 const QQmlJSResourceFileMapper::Filter qmldirFilter = type == LocalFileFilter
291 ? QQmlJSResourceFileMapper::localFileFilter(file: qmldirPath)
292 : QQmlJSResourceFileMapper::resourceFileFilter(file: qmldirPath);
293
294 QQmlJSResourceFileMapper::Entry result = mapper->entry(filter: qmldirFilter);
295 if (result.isValid()) {
296 result.resourcePath.chop(n: std::char_traits<char>::length(s: "/qmldir"));
297 result.filePath.chop(n: std::char_traits<char>::length(s: "/qmldir"));
298 return result;
299 }
300 }
301 return {};
302}
303
304/*!
305\internal
306Obtains the source folder path from a build folder QML file path via the passed \c mapper.
307
308This works on proper QML modules when using the nested-qml-module-with-prefer-feature
309from 6.8 and uses a heuristic when the qmldir with the prefer entry is missing.
310*/
311QString QQmlJSUtils::qmlSourcePathFromBuildPath(const QQmlJSResourceFileMapper *mapper,
312 const QString &pathInBuildFolder)
313{
314 if (!mapper)
315 return pathInBuildFolder;
316
317 const auto qmlModuleEntry =
318 qmlModuleEntryFromBuildPath(mapper, pathInBuildFolder, type: LocalFileFilter);
319 if (!qmlModuleEntry.isValid())
320 return pathInBuildFolder;
321 const QString qrcPath = qmlModuleEntry.resourcePath
322 + QStringView(pathInBuildFolder).sliced(pos: qmlModuleEntry.filePath.size());
323
324 const auto entry = mapper->entry(filter: QQmlJSResourceFileMapper::resourceFileFilter(file: qrcPath));
325 return entry.isValid()? entry.filePath : pathInBuildFolder;
326}
327
328/*!
329\internal
330Obtains the source folder path from a build folder QML file path via the passed \c mapper, see also
331\l QQmlJSUtils::qmlSourcePathFromBuildPath.
332*/
333QString QQmlJSUtils::qmlBuildPathFromSourcePath(const QQmlJSResourceFileMapper *mapper,
334 const QString &pathInSourceFolder)
335{
336 if (!mapper)
337 return pathInSourceFolder;
338
339 const QString qrcPath =
340 mapper->entry(filter: QQmlJSResourceFileMapper::localFileFilter(file: pathInSourceFolder))
341 .resourcePath;
342
343 if (qrcPath.isEmpty())
344 return pathInSourceFolder;
345
346 const auto moduleBuildEntry =
347 qmlModuleEntryFromBuildPath(mapper, pathInBuildFolder: qrcPath, type: ResourceFileFilter);
348
349 if (!moduleBuildEntry.isValid())
350 return pathInSourceFolder;
351
352 const auto qrcFolderPath = qrcPath.first(n: qrcPath.lastIndexOf(c: u'/')); // drop the filename
353
354 return moduleBuildEntry.filePath + qrcFolderPath.sliced(pos: moduleBuildEntry.resourcePath.size())
355 + pathInSourceFolder.sliced(pos: pathInSourceFolder.lastIndexOf(c: u'/'));
356}
357
358QT_END_NAMESPACE
359

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