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 <algorithm>
9
10QT_BEGIN_NAMESPACE
11
12using namespace Qt::StringLiterals;
13
14/*! \internal
15
16 Fully resolves alias \a property and returns the information about the
17 origin, which is not an alias.
18*/
19template<typename ScopeForId>
20static QQmlJSUtils::ResolvedAlias
21resolveAlias(ScopeForId scopeForId, const QQmlJSMetaProperty &property,
22 const QQmlJSScope::ConstPtr &owner, const QQmlJSUtils::AliasResolutionVisitor &visitor)
23{
24 Q_ASSERT(property.isAlias());
25 Q_ASSERT(owner);
26
27 QQmlJSUtils::ResolvedAlias result {};
28 result.owner = owner;
29
30 // TODO: one could optimize the generated alias code for aliases pointing to aliases
31 // e.g., if idA.myAlias -> idB.myAlias2 -> idC.myProp, then one could directly generate
32 // idA.myProp as pointing to idC.myProp.
33 // This gets complicated when idB.myAlias is in a different Component than where the
34 // idA.myAlias is defined: scopeForId currently only contains the ids of the current
35 // component and alias resolution on the ids of a different component fails then.
36 if (QQmlJSMetaProperty nextProperty = property; nextProperty.isAlias()) {
37 QQmlJSScope::ConstPtr resultOwner = result.owner;
38 result = QQmlJSUtils::ResolvedAlias {};
39
40 visitor.reset();
41
42 auto aliasExprBits = nextProperty.aliasExpression().split(sep: u'.');
43 // do not crash on invalid aliasexprbits when accessing aliasExprBits[0]
44 if (aliasExprBits.size() < 1)
45 return {};
46
47 // resolve id first:
48 resultOwner = scopeForId(aliasExprBits[0], resultOwner);
49 if (!resultOwner)
50 return {};
51
52 visitor.processResolvedId(resultOwner);
53
54 aliasExprBits.removeFirst(); // Note: for simplicity, remove the <id>
55 result.owner = resultOwner;
56 result.kind = QQmlJSUtils::AliasTarget_Object;
57
58 for (const QString &bit : std::as_const(t&: aliasExprBits)) {
59 nextProperty = resultOwner->property(name: bit);
60 if (!nextProperty.isValid())
61 return {};
62
63 visitor.processResolvedProperty(nextProperty, resultOwner);
64
65 result.property = nextProperty;
66 result.owner = resultOwner;
67 result.kind = QQmlJSUtils::AliasTarget_Property;
68
69 resultOwner = nextProperty.type();
70 }
71 }
72
73 return result;
74}
75
76QQmlJSUtils::ResolvedAlias QQmlJSUtils::resolveAlias(const QQmlJSTypeResolver *typeResolver,
77 const QQmlJSMetaProperty &property,
78 const QQmlJSScope::ConstPtr &owner,
79 const AliasResolutionVisitor &visitor)
80{
81 return ::resolveAlias(
82 scopeForId: [&](const QString &id, const QQmlJSScope::ConstPtr &referrer) {
83 return typeResolver->scopeForId(id, referrer);
84 },
85 property, owner, visitor);
86}
87
88QQmlJSUtils::ResolvedAlias QQmlJSUtils::resolveAlias(const QQmlJSScopesById &idScopes,
89 const QQmlJSMetaProperty &property,
90 const QQmlJSScope::ConstPtr &owner,
91 const AliasResolutionVisitor &visitor)
92{
93 return ::resolveAlias(
94 scopeForId: [&](const QString &id, const QQmlJSScope::ConstPtr &referrer) {
95 return idScopes.scope(id, referrer);
96 },
97 property, owner, visitor);
98}
99
100std::optional<QQmlJSFixSuggestion> QQmlJSUtils::didYouMean(const QString &userInput,
101 QStringList candidates,
102 QQmlJS::SourceLocation location)
103{
104 QString shortestDistanceWord;
105 int shortestDistance = userInput.size();
106
107 // Most of the time the candidates are keys() from QHash, which means that
108 // running this function in the seemingly same setup might yield different
109 // best cadidate (e.g. imagine a typo 'thing' with candidates 'thingA' vs
110 // 'thingB'). This is especially flaky in e.g. test environment where the
111 // results may differ (even when the global hash seed is fixed!) when
112 // running one test vs the whole test suite (recall platform-dependent
113 // QSKIPs). There could be user-visible side effects as well, so just sort
114 // the candidates to guarantee consistent results
115 std::sort(first: candidates.begin(), last: candidates.end());
116
117 for (const QString &candidate : candidates) {
118 /*
119 * Calculate the distance between the userInput and candidate using Damerau–Levenshtein
120 * Roughly based on
121 * https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows.
122 */
123 QList<int> v0(candidate.size() + 1);
124 QList<int> v1(candidate.size() + 1);
125
126 std::iota(first: v0.begin(), last: v0.end(), value: 0);
127
128 for (qsizetype i = 0; i < userInput.size(); i++) {
129 v1[0] = i + 1;
130 for (qsizetype j = 0; j < candidate.size(); j++) {
131 int deletionCost = v0[j + 1] + 1;
132 int insertionCost = v1[j] + 1;
133 int substitutionCost = userInput[i] == candidate[j] ? v0[j] : v0[j] + 1;
134 v1[j + 1] = std::min(l: { deletionCost, insertionCost, substitutionCost });
135 }
136 std::swap(a&: v0, b&: v1);
137 }
138
139 int distance = v0[candidate.size()];
140 if (distance < shortestDistance) {
141 shortestDistanceWord = candidate;
142 shortestDistance = distance;
143 }
144 }
145
146 if (shortestDistance
147 < std::min(a: std::max(a: userInput.size() / 2, b: qsizetype(3)), b: userInput.size())) {
148 return QQmlJSFixSuggestion {
149 u"Did you mean \"%1\"?"_s.arg(a: shortestDistanceWord),
150 location,
151 shortestDistanceWord
152 };
153 } else {
154 return {};
155 }
156}
157
158/*! \internal
159
160 Returns a corresponding source directory path for \a buildDirectoryPath
161 Returns empty string on error
162*/
163std::variant<QString, QQmlJS::DiagnosticMessage>
164QQmlJSUtils::sourceDirectoryPath(const QQmlJSImporter *importer, const QString &buildDirectoryPath)
165{
166 const auto makeError = [](const QString &msg) {
167 return QQmlJS::DiagnosticMessage { .message: msg, .type: QtWarningMsg, .loc: QQmlJS::SourceLocation() };
168 };
169
170 if (!importer->metaDataMapper())
171 return makeError(u"QQmlJSImporter::metaDataMapper() is nullptr"_s);
172
173 // for now, meta data contains just a single entry
174 QQmlJSResourceFileMapper::Filter matchAll { .path: QString(), .suffixes: QStringList(),
175 .flags: QQmlJSResourceFileMapper::Directory
176 | QQmlJSResourceFileMapper::Recurse };
177 QQmlJSResourceFileMapper::Entry entry = importer->metaDataMapper()->entry(filter: matchAll);
178 if (!entry.isValid())
179 return makeError(u"Failed to find meta data entry in QQmlJSImporter::metaDataMapper()"_s);
180 if (!buildDirectoryPath.startsWith(s: entry.filePath)) // assume source directory path already
181 return makeError(u"The module output directory does not match the build directory path"_s);
182
183 QString qrcPath = buildDirectoryPath;
184 qrcPath.remove(i: 0, len: entry.filePath.size());
185 qrcPath.prepend(s: entry.resourcePath);
186 qrcPath.remove(i: 0, len: 1); // remove extra "/"
187
188 const QStringList sourceDirPaths = importer->resourceFileMapper()->filePaths(
189 filter: QQmlJSResourceFileMapper::resourceFileFilter(file: qrcPath));
190 if (sourceDirPaths.size() != 1) {
191 const QString matchedPaths =
192 sourceDirPaths.isEmpty() ? u"<none>"_s : sourceDirPaths.join(sep: u", ");
193 return makeError(
194 QStringLiteral("QRC path %1 (deduced from %2) has unexpected number of mappings "
195 "(%3). File paths that matched:\n%4")
196 .arg(args&: qrcPath, args: buildDirectoryPath, args: QString::number(sourceDirPaths.size()),
197 args: matchedPaths));
198 }
199 return sourceDirPaths[0];
200}
201
202/*! \internal
203
204 Utility method that checks if one of the registers is var, and the other can be
205 efficiently compared to it
206*/
207bool canStrictlyCompareWithVar(const QQmlJSTypeResolver *typeResolver,
208 const QQmlJSRegisterContent &lhsContent,
209 const QQmlJSRegisterContent &rhsContent)
210{
211 Q_ASSERT(typeResolver);
212 const auto varType = typeResolver->varType();
213 const auto nullType = typeResolver->nullType();
214 const auto voidType = typeResolver->voidType();
215
216 // Use containedType() because nullptr is not a stored type.
217 const auto lhsType = typeResolver->containedType(container: lhsContent);
218 const auto rhsType = typeResolver->containedType(container: rhsContent);
219
220 return (typeResolver->equals(a: lhsType, b: varType)
221 && (typeResolver->equals(a: rhsType, b: nullType) || typeResolver->equals(a: rhsType, b: voidType)))
222 || (typeResolver->equals(a: rhsType, b: varType)
223 && (typeResolver->equals(a: lhsType, b: nullType)
224 || typeResolver->equals(a: lhsType, b: voidType)));
225}
226
227/*! \internal
228
229 Utility method that checks if one of the registers is qobject, and the other can be
230 efficiently compared to it
231*/
232bool canCompareWithQObject(const QQmlJSTypeResolver *typeResolver,
233 const QQmlJSRegisterContent &lhsContent,
234 const QQmlJSRegisterContent &rhsContent)
235{
236 Q_ASSERT(typeResolver);
237 const auto lhsType = typeResolver->containedType(container: lhsContent);
238 const auto rhsType = typeResolver->containedType(container: rhsContent);
239 return (lhsType->isReferenceType()
240 && (rhsType->isReferenceType()
241 || typeResolver->equals(a: rhsType, b: typeResolver->nullType())))
242 || (rhsType->isReferenceType()
243 && (lhsType->isReferenceType()
244 || typeResolver->equals(a: lhsType, b: typeResolver->nullType())));
245}
246
247/*! \internal
248
249 Utility method that checks if both sides are QUrl type. In future, that might be extended to
250 support comparison with other types i.e QUrl vs string
251*/
252bool canCompareWithQUrl(const QQmlJSTypeResolver *typeResolver,
253 const QQmlJSRegisterContent &lhsContent,
254 const QQmlJSRegisterContent &rhsContent)
255{
256 Q_ASSERT(typeResolver);
257 const auto lhsType = typeResolver->containedType(container: lhsContent);
258 const auto rhsType = typeResolver->containedType(container: rhsContent);
259 return typeResolver->equals(a: lhsType, b: typeResolver->urlType())
260 && typeResolver->equals(a: rhsType, b: typeResolver->urlType());
261}
262
263QT_END_NAMESPACE
264

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