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 | |
10 | QT_BEGIN_NAMESPACE |
11 | |
12 | using 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 | */ |
19 | template<typename ScopeForId> |
20 | static QQmlJSUtils::ResolvedAlias |
21 | resolveAlias(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 | |
76 | QQmlJSUtils::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 | |
88 | QQmlJSUtils::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 | |
100 | std::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 | */ |
163 | std::variant<QString, QQmlJS::DiagnosticMessage> |
164 | QQmlJSUtils::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 | */ |
207 | bool 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 | */ |
232 | bool 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 | */ |
252 | bool 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 | |
263 | QT_END_NAMESPACE |
264 | |