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 | |
14 | QT_BEGIN_NAMESPACE |
15 | |
16 | using 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 | */ |
23 | template<typename ScopeForId> |
24 | static QQmlJSUtils::ResolvedAlias |
25 | resolveAlias(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 | |
79 | QQmlJSUtils::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 | |
94 | QQmlJSUtils::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 | |
106 | std::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 | */ |
169 | std::variant<QString, QQmlJS::DiagnosticMessage> |
170 | QQmlJSUtils::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 | */ |
213 | bool 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 | */ |
230 | bool 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 | */ |
248 | bool 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 | |
257 | static 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 | |
271 | QStringList 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 | |
286 | enum FilterType { |
287 | LocalFileFilter, |
288 | ResourceFileFilter |
289 | }; |
290 | |
291 | /*! |
292 | \internal |
293 | Obtain a QML module qrc entry from its qmldir entry. |
294 | |
295 | Contains a heuristic for QML modules without nested-qml-module-with-prefer-feature |
296 | that tries to find a parent directory that contains a qmldir entry in the qrc. |
297 | */ |
298 | static QQmlJSResourceFileMapper::Entry |
299 | qmlModuleEntryFromBuildPath(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 |
328 | Obtains the source folder path from a build folder QML file path via the passed \c mapper. |
329 | |
330 | This works on proper QML modules when using the nested-qml-module-with-prefer-feature |
331 | from 6.8 and uses a heuristic when the qmldir with the prefer entry is missing. |
332 | */ |
333 | QString 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 |
352 | Obtains the source folder path from a build folder QML file path via the passed \c mapper, see also |
353 | \l QQmlJSUtils::qmlSourcePathFromBuildPath. |
354 | */ |
355 | QString 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 | |
380 | QT_END_NAMESPACE |
381 |
Definitions
Learn Advanced QML with KDAB
Find out more