1 | // Copyright (C) 2020 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 "qqmljsimporter_p.h" |
5 | #include "qqmljstypedescriptionreader_p.h" |
6 | #include "qqmljstypereader_p.h" |
7 | #include "qqmljsimportvisitor_p.h" |
8 | #include "qqmljslogger_p.h" |
9 | #include "qqmljsutils_p.h" |
10 | |
11 | #include <QtQml/private/qqmlimportresolver_p.h> |
12 | |
13 | #include <QtCore/qfileinfo.h> |
14 | #include <QtCore/qdiriterator.h> |
15 | |
16 | QT_BEGIN_NAMESPACE |
17 | |
18 | using namespace Qt::StringLiterals; |
19 | |
20 | static const QLatin1String SlashQmldir = QLatin1String("/qmldir"); |
21 | static const QLatin1String PluginsDotQmltypes = QLatin1String("plugins.qmltypes"); |
22 | static const QLatin1String JsrootDotQmltypes = QLatin1String("jsroot.qmltypes"); |
23 | |
24 | |
25 | QQmlJS::Import::Import(QString prefix, QString name, QTypeRevision version, bool isFile, |
26 | bool isDependency) : |
27 | m_prefix(std::move(prefix)), |
28 | m_name(std::move(name)), |
29 | m_version(version), |
30 | m_isFile(isFile), |
31 | m_isDependency(isDependency) |
32 | { |
33 | } |
34 | |
35 | bool QQmlJS::Import::isValid() const |
36 | { |
37 | return !m_name.isEmpty(); |
38 | } |
39 | |
40 | static const QString prefixedName(const QString &prefix, const QString &name) |
41 | { |
42 | Q_ASSERT(!prefix.endsWith(u'.')); |
43 | return prefix.isEmpty() ? name : (prefix + QLatin1Char('.') + name); |
44 | } |
45 | |
46 | QQmlDirParser QQmlJSImporter::createQmldirParserForFile(const QString &filename, Import *import) |
47 | { |
48 | Q_ASSERT(import); |
49 | QFile f(filename); |
50 | QQmlDirParser parser; |
51 | if (f.open(flags: QFile::ReadOnly)) { |
52 | parser.parse(source: QString::fromUtf8(ba: f.readAll())); |
53 | } else { |
54 | import->warnings.append(t: { |
55 | QStringLiteral("Could not open qmldir file: ") + filename, |
56 | .type: QtWarningMsg, |
57 | .loc: QQmlJS::SourceLocation() |
58 | }); |
59 | } |
60 | |
61 | return parser; |
62 | } |
63 | |
64 | void QQmlJSImporter::readQmltypes(const QString &filename, Import *result) |
65 | { |
66 | const QFileInfo fileInfo(filename); |
67 | if (!fileInfo.exists()) { |
68 | result->warnings.append(t: { |
69 | QStringLiteral("QML types file does not exist: ") + filename, |
70 | .type: QtWarningMsg, |
71 | .loc: QQmlJS::SourceLocation() |
72 | }); |
73 | return; |
74 | } |
75 | |
76 | if (fileInfo.isDir()) { |
77 | result->warnings.append(t: { |
78 | QStringLiteral("QML types file cannot be a directory: ") + filename, |
79 | .type: QtWarningMsg, |
80 | .loc: QQmlJS::SourceLocation() |
81 | }); |
82 | return; |
83 | } |
84 | |
85 | QFile file(filename); |
86 | if (!file.open(flags: QFile::ReadOnly)) { |
87 | result->warnings.append(t: { |
88 | QStringLiteral("QML types file cannot be opened: ") + filename, |
89 | .type: QtWarningMsg, |
90 | .loc: QQmlJS::SourceLocation() |
91 | }); |
92 | return; |
93 | } |
94 | |
95 | QQmlJSTypeDescriptionReader reader { filename, QString::fromUtf8(ba: file.readAll()) }; |
96 | QStringList dependencyStrings; |
97 | auto succ = reader(&result->objects, &dependencyStrings); |
98 | if (!succ) |
99 | result->warnings.append(t: { .message: reader.errorMessage(), .type: QtCriticalMsg, .loc: QQmlJS::SourceLocation() }); |
100 | |
101 | const QString warningMessage = reader.warningMessage(); |
102 | if (!warningMessage.isEmpty()) |
103 | result->warnings.append(t: { .message: warningMessage, .type: QtWarningMsg, .loc: QQmlJS::SourceLocation() }); |
104 | |
105 | if (dependencyStrings.isEmpty()) |
106 | return; |
107 | |
108 | result->warnings.append(t: { |
109 | QStringLiteral("Found deprecated dependency specifications in %1." |
110 | "Specify dependencies in qmldir and use qmltyperegistrar " |
111 | "to generate qmltypes files without dependencies.") |
112 | .arg(a: filename), |
113 | .type: QtWarningMsg, |
114 | .loc: QQmlJS::SourceLocation() |
115 | }); |
116 | |
117 | for (const QString &dependency : std::as_const(t&: dependencyStrings)) { |
118 | const auto blank = dependency.indexOf(ch: u' '); |
119 | if (blank < 0) { |
120 | result->dependencies.append( |
121 | t: QQmlDirParser::Import(dependency, {}, QQmlDirParser::Import::Default)); |
122 | continue; |
123 | } |
124 | |
125 | const QString module = dependency.left(n: blank); |
126 | const QString versionString = dependency.mid(position: blank + 1).trimmed(); |
127 | if (versionString == QStringLiteral("auto")) { |
128 | result->dependencies.append( |
129 | t: QQmlDirParser::Import(module, {}, QQmlDirParser::Import::Auto)); |
130 | continue; |
131 | } |
132 | |
133 | const auto dot = versionString.indexOf(ch: u'.'); |
134 | |
135 | const QTypeRevision version = dot < 0 |
136 | ? QTypeRevision::fromMajorVersion(majorVersion: versionString.toUShort()) |
137 | : QTypeRevision::fromVersion(majorVersion: versionString.left(n: dot).toUShort(), |
138 | minorVersion: versionString.mid(position: dot + 1).toUShort()); |
139 | |
140 | result->dependencies.append( |
141 | t: QQmlDirParser::Import(module, version, QQmlDirParser::Import::Default)); |
142 | } |
143 | } |
144 | |
145 | static QString internalName(const QQmlJSScope::ConstPtr &scope) |
146 | { |
147 | if (const auto *factory = scope.factory()) |
148 | return factory->internalName(); |
149 | return scope->internalName(); |
150 | } |
151 | |
152 | static bool isComposite(const QQmlJSScope::ConstPtr &scope) |
153 | { |
154 | // The only thing the factory can do is load a composite type. |
155 | return scope.factory() || scope->isComposite(); |
156 | } |
157 | |
158 | static QStringList aliases(const QQmlJSScope::ConstPtr &scope) |
159 | { |
160 | return isComposite(scope) |
161 | ? QStringList() |
162 | : scope->aliases(); |
163 | } |
164 | |
165 | QQmlJSImporter::QQmlJSImporter(const QStringList &importPaths, QQmlJSResourceFileMapper *mapper, |
166 | QQmlJSImporterFlags flags) |
167 | : m_importPaths(importPaths), |
168 | m_mapper(mapper), |
169 | m_flags(flags), |
170 | m_importVisitor([](QQmlJS::AST::Node *rootNode, QQmlJSImporter *self, |
171 | const ImportVisitorPrerequisites &p) { |
172 | auto visitor = std::unique_ptr<QQmlJS::AST::BaseVisitor>(new QQmlJSImportVisitor( |
173 | p.m_target, self, p.m_logger, p.m_implicitImportDirectory, p.m_qmldirFiles)); |
174 | QQmlJS::AST::Node::accept(node: rootNode, visitor: visitor.get()); |
175 | }) |
176 | { |
177 | } |
178 | |
179 | static QString resolvePreferredPath( |
180 | const QString &qmldirPath, const QString &prefer, QQmlJSResourceFileMapper *mapper) |
181 | { |
182 | if (prefer.isEmpty()) |
183 | return qmldirPath; |
184 | |
185 | if (!prefer.endsWith(c: u'/')) { |
186 | qWarning() << "Ignoring invalid prefer path"<< prefer << "(has to end with slash)"; |
187 | return qmldirPath; |
188 | } |
189 | |
190 | if (prefer.startsWith(c: u':')) { |
191 | // Resource path: Resolve via resource file mapper if possible. |
192 | if (!mapper) |
193 | return qmldirPath; |
194 | |
195 | Q_ASSERT(prefer.endsWith(u'/')); |
196 | const auto entry = mapper->entry( |
197 | filter: QQmlJSResourceFileMapper::resourceFileFilter(file: prefer.mid(position: 1) + SlashQmldir.mid(pos: 1))); |
198 | |
199 | // This can be empty if the .qrc files does not belong to this module. |
200 | // In that case we trust the given qmldir file. |
201 | return entry.filePath.endsWith(s: SlashQmldir) |
202 | ? entry.filePath |
203 | : qmldirPath; |
204 | } |
205 | |
206 | // Host file system path. This should be rare. We don't generate it. |
207 | const QFileInfo f(prefer + SlashQmldir); |
208 | const QString canonical = f.canonicalFilePath(); |
209 | if (canonical.isEmpty()) { |
210 | qWarning() << "No qmldir at"<< prefer; |
211 | return qmldirPath; |
212 | } |
213 | return canonical; |
214 | } |
215 | |
216 | QQmlJSImporter::Import QQmlJSImporter::readQmldir(const QString &modulePath) |
217 | { |
218 | Import result; |
219 | const QString moduleQmldirPath = modulePath + SlashQmldir; |
220 | auto reader = createQmldirParserForFile(filename: moduleQmldirPath, import: &result); |
221 | |
222 | const QString resolvedQmldirPath |
223 | = resolvePreferredPath(qmldirPath: moduleQmldirPath, prefer: reader.preferredPath(), mapper: m_mapper); |
224 | if (resolvedQmldirPath != moduleQmldirPath) |
225 | reader = createQmldirParserForFile(filename: resolvedQmldirPath, import: &result); |
226 | |
227 | // Leave the trailing slash |
228 | Q_ASSERT(resolvedQmldirPath.endsWith(SlashQmldir)); |
229 | QStringView resolvedPath = QStringView(resolvedQmldirPath).chopped(n: SlashQmldir.size() - 1); |
230 | |
231 | result.name = reader.typeNamespace(); |
232 | |
233 | result.isStaticModule = reader.isStaticModule(); |
234 | result.isSystemModule = reader.isSystemModule(); |
235 | result.imports.append(other: reader.imports()); |
236 | result.dependencies.append(other: reader.dependencies()); |
237 | |
238 | if (result.isSystemModule) { |
239 | // Import jsroot first, so that the builtins override it. |
240 | const QString jsrootPath = resolvedPath + JsrootDotQmltypes; |
241 | if (QFile::exists(fileName: jsrootPath)) { |
242 | readQmltypes(filename: jsrootPath, result: &result); |
243 | } else { |
244 | result.warnings.append(t: { |
245 | QStringLiteral("System module at %1 does not contain jsroot.qmltypes") |
246 | .arg(a: resolvedPath), |
247 | .type: QtWarningMsg, |
248 | .loc: QQmlJS::SourceLocation() |
249 | }); |
250 | } |
251 | } |
252 | |
253 | const auto typeInfos = reader.typeInfos(); |
254 | for (const auto &typeInfo : typeInfos) { |
255 | const QString typeInfoPath = QFileInfo(typeInfo).isRelative() |
256 | ? resolvedPath + typeInfo |
257 | : typeInfo; |
258 | readQmltypes(filename: typeInfoPath, result: &result); |
259 | } |
260 | |
261 | if (typeInfos.isEmpty() && !reader.plugins().isEmpty()) { |
262 | const QString defaultTypeInfoPath = resolvedPath + PluginsDotQmltypes; |
263 | if (QFile::exists(fileName: defaultTypeInfoPath)) { |
264 | result.warnings.append(t: { |
265 | QStringLiteral("typeinfo not declared in qmldir file: ") |
266 | + defaultTypeInfoPath, |
267 | .type: QtWarningMsg, |
268 | .loc: QQmlJS::SourceLocation() |
269 | }); |
270 | readQmltypes(filename: defaultTypeInfoPath, result: &result); |
271 | } |
272 | } |
273 | |
274 | QHash<QString, QQmlJSExportedScope> qmlComponents; |
275 | const auto components = reader.components(); |
276 | for (auto it = components.begin(), end = components.end(); it != end; ++it) { |
277 | const QString filePath = resolvedPath + it->fileName; |
278 | if (!QFile::exists(fileName: filePath)) { |
279 | result.warnings.append(t: { |
280 | .message: it->fileName + QStringLiteral(" is listed as component in ") |
281 | + resolvedQmldirPath |
282 | + QStringLiteral(" but does not exist.\n"), |
283 | .type: QtWarningMsg, |
284 | .loc: QQmlJS::SourceLocation() |
285 | }); |
286 | continue; |
287 | } |
288 | |
289 | auto mo = qmlComponents.find(key: it->fileName); |
290 | if (mo == qmlComponents.end()) { |
291 | QQmlJSScope::Ptr imported = localFile2ScopeTree(filePath); |
292 | if (auto *factory = imported.factory()) { |
293 | if (it->singleton) { |
294 | factory->setIsSingleton(true); |
295 | } |
296 | } |
297 | mo = qmlComponents.insert(key: it->fileName, value: {.scope: imported, .exports: QList<QQmlJSScope::Export>() }); |
298 | } |
299 | |
300 | mo->exports.append(t: QQmlJSScope::Export( |
301 | reader.typeNamespace(), it.key(), it->version, QTypeRevision())); |
302 | } |
303 | for (auto it = qmlComponents.begin(), end = qmlComponents.end(); it != end; ++it) |
304 | result.objects.append(t: it.value()); |
305 | |
306 | const auto scripts = reader.scripts(); |
307 | for (const auto &script : scripts) { |
308 | const QString filePath = resolvedPath + script.fileName; |
309 | auto mo = result.scripts.find(key: script.fileName); |
310 | if (mo == result.scripts.end()) |
311 | mo = result.scripts.insert(key: script.fileName, value: { .scope: localFile2ScopeTree(filePath), .exports: {} }); |
312 | |
313 | mo->exports.append(t: QQmlJSScope::Export( |
314 | reader.typeNamespace(), script.nameSpace, |
315 | script.version, QTypeRevision())); |
316 | } |
317 | return result; |
318 | } |
319 | |
320 | QQmlJSImporter::Import QQmlJSImporter::readDirectory(const QString &directory) |
321 | { |
322 | Import import; |
323 | if (directory.startsWith(c: u':')) { |
324 | if (m_mapper) { |
325 | const auto resources = m_mapper->filter( |
326 | filter: QQmlJSResourceFileMapper::resourceQmlDirectoryFilter(directory: directory.mid(position: 1))); |
327 | for (const auto &entry : resources) { |
328 | const QString name = QFileInfo(entry.resourcePath).baseName(); |
329 | if (name.front().isUpper()) { |
330 | import.objects.append(t: { |
331 | .scope: localFile2ScopeTree(filePath: entry.filePath), |
332 | .exports: { QQmlJSScope::Export(QString(), name, QTypeRevision(), QTypeRevision()) } |
333 | }); |
334 | } |
335 | } |
336 | } else { |
337 | qWarning() << "Cannot read files from resource directory"<< directory |
338 | << "because no resource file mapper was provided"; |
339 | } |
340 | |
341 | return import; |
342 | } |
343 | |
344 | QDirIterator it { |
345 | directory, |
346 | QStringList() << QLatin1String("*.qml"), |
347 | QDir::NoFilter |
348 | }; |
349 | while (it.hasNext()) { |
350 | QString name = it.nextFileInfo().completeBaseName(); |
351 | |
352 | // Non-uppercase names cannot be imported anyway. |
353 | if (!name.front().isUpper()) |
354 | continue; |
355 | |
356 | // .ui.qml is fine |
357 | if (name.endsWith(s: u".ui")) |
358 | name = name.chopped(n: 3); |
359 | |
360 | // Names with dots in them cannot be imported either. |
361 | if (name.contains(c: u'.')) |
362 | continue; |
363 | |
364 | import.objects.append(t: { |
365 | .scope: localFile2ScopeTree(filePath: it.filePath()), |
366 | .exports: { QQmlJSScope::Export(QString(), name, QTypeRevision(), QTypeRevision()) } |
367 | }); |
368 | } |
369 | return import; |
370 | } |
371 | |
372 | void QQmlJSImporter::importDependencies( |
373 | const QQmlJSImporter::Import &import, QQmlJSImporter::AvailableTypes *types, |
374 | const QString &prefix, QTypeRevision version, bool isDependency) |
375 | { |
376 | // Import the dependencies with an invalid prefix. The prefix will never be matched by actual |
377 | // QML code but the C++ types will be visible. |
378 | for (auto const &dependency : std::as_const(t: import.dependencies)) |
379 | importHelper(module: dependency.module, types, prefix: QString(), version: dependency.version, isDependency: true); |
380 | |
381 | bool hasOptionalImports = false; |
382 | for (auto const &import : std::as_const(t: import.imports)) { |
383 | if (import.flags & QQmlDirParser::Import::Optional) { |
384 | hasOptionalImports = true; |
385 | if (!useOptionalImports()) { |
386 | continue; |
387 | } |
388 | |
389 | if (!(import.flags & QQmlDirParser::Import::OptionalDefault)) |
390 | continue; |
391 | } |
392 | |
393 | importHelper(module: import.module, types, prefix: isDependency ? QString() : prefix, |
394 | version: (import.flags & QQmlDirParser::Import::Auto) ? version : import.version, |
395 | isDependency); |
396 | } |
397 | |
398 | if (hasOptionalImports && !useOptionalImports()) { |
399 | types->warnings.append(t: { |
400 | .message: u"%1 uses optional imports which are not supported. Some types might not be found."_s |
401 | .arg(a: import.name), |
402 | .type: QtCriticalMsg, .loc: QQmlJS::SourceLocation() |
403 | }); |
404 | } |
405 | } |
406 | |
407 | static bool isVersionAllowed(const QQmlJSScope::Export &exportEntry, |
408 | const QQmlJS::Import &importDescription) |
409 | { |
410 | const QTypeRevision importVersion = importDescription.version(); |
411 | const QTypeRevision exportVersion = exportEntry.version(); |
412 | if (!importVersion.hasMajorVersion()) |
413 | return true; |
414 | if (importVersion.majorVersion() != exportVersion.majorVersion()) |
415 | return false; |
416 | return !importVersion.hasMinorVersion() |
417 | || exportVersion.minorVersion() <= importVersion.minorVersion(); |
418 | } |
419 | |
420 | void QQmlJSImporter::processImport( |
421 | const QQmlJS::Import &importDescription, const QQmlJSImporter::Import &import, |
422 | QQmlJSImporter::AvailableTypes *types) |
423 | { |
424 | // In the list of QML types we prefix unresolvable QML names with $anonymous$, and C++ |
425 | // names with $internal$. This is to avoid clashes between them. |
426 | // In the list of C++ types we insert types that don't have a C++ name as their |
427 | // QML name prefixed with $anonymous$. |
428 | const QString anonPrefix = QStringLiteral("$anonymous$"); |
429 | const QString internalPrefix = QStringLiteral("$internal$"); |
430 | const QString modulePrefix = QStringLiteral("$module$"); |
431 | QHash<QString, QList<QQmlJSScope::Export>> seenExports; |
432 | |
433 | const auto insertAliases = [&](const QQmlJSScope::ConstPtr &scope, QTypeRevision revision) { |
434 | const QStringList cppAliases = aliases(scope); |
435 | for (const QString &alias : cppAliases) |
436 | types->cppNames.setType(name: alias, type: { .scope: scope, .revision: revision }); |
437 | }; |
438 | |
439 | const auto insertExports = [&](const QQmlJSExportedScope &val, const QString &cppName) { |
440 | QQmlJSScope::Export bestExport; |
441 | |
442 | // Resolve conflicting qmlNames within an import |
443 | for (const auto &valExport : val.exports) { |
444 | const QString qmlName = prefixedName(prefix: importDescription.prefix(), name: valExport.type()); |
445 | if (!isVersionAllowed(exportEntry: valExport, importDescription)) |
446 | continue; |
447 | |
448 | // Even if the QML name is overridden by some other type, we still want |
449 | // to insert the C++ type, with the highest revision available. |
450 | if (!bestExport.isValid() || valExport.version() > bestExport.version()) |
451 | bestExport = valExport; |
452 | |
453 | const auto it = types->qmlNames.types().find(key: qmlName); |
454 | if (it != types->qmlNames.types().end()) { |
455 | |
456 | // The same set of exports can declare the same name multiple times for different |
457 | // versions. That's the common thing and we would just continue here when we hit |
458 | // it again after having inserted successfully once. |
459 | // However, it can also declare *different* names. Then we need to do the whole |
460 | // thing again. |
461 | if (it->scope == val.scope && it->revision == valExport.version()) |
462 | continue; |
463 | |
464 | const auto existingExports = seenExports.value(key: qmlName); |
465 | enum { LowerVersion, SameVersion, HigherVersion } seenVersion = LowerVersion; |
466 | for (const QQmlJSScope::Export &entry : existingExports) { |
467 | if (!isVersionAllowed(exportEntry: entry, importDescription)) |
468 | continue; |
469 | |
470 | if (valExport.version() < entry.version()) { |
471 | seenVersion = HigherVersion; |
472 | break; |
473 | } |
474 | |
475 | if (seenVersion == LowerVersion && valExport.version() == entry.version()) |
476 | seenVersion = SameVersion; |
477 | } |
478 | |
479 | switch (seenVersion) { |
480 | case LowerVersion: |
481 | break; |
482 | case SameVersion: { |
483 | types->warnings.append(t: { |
484 | QStringLiteral("Ambiguous type detected. " |
485 | "%1 %2.%3 is defined multiple times.") |
486 | .arg(a: qmlName) |
487 | .arg(a: valExport.version().majorVersion()) |
488 | .arg(a: valExport.version().minorVersion()), |
489 | .type: QtCriticalMsg, |
490 | .loc: QQmlJS::SourceLocation() |
491 | }); |
492 | |
493 | // Invalidate the type. We don't know which one to use. |
494 | types->qmlNames.clearType(name: qmlName); |
495 | continue; |
496 | } |
497 | case HigherVersion: |
498 | continue; |
499 | } |
500 | } |
501 | |
502 | types->qmlNames.setType(name: qmlName, type: { .scope: val.scope, .revision: valExport.version() }); |
503 | seenExports[qmlName].append(t: valExport); |
504 | } |
505 | |
506 | const QTypeRevision bestRevision = bestExport.isValid() |
507 | ? bestExport.revision() |
508 | : QTypeRevision::zero(); |
509 | types->cppNames.setType(name: cppName, type: { .scope: val.scope, .revision: bestRevision }); |
510 | |
511 | insertAliases(val.scope, bestRevision); |
512 | |
513 | const QTypeRevision bestVersion = bestExport.isValid() |
514 | ? bestExport.version() |
515 | : QTypeRevision::zero(); |
516 | types->qmlNames.setType(name: prefixedName(prefix: internalPrefix, name: cppName), type: { .scope: val.scope, .revision: bestVersion }); |
517 | }; |
518 | |
519 | // Empty type means "this is the prefix" |
520 | if (!importDescription.prefix().isEmpty()) |
521 | types->qmlNames.setType(name: importDescription.prefix(), type: {}); |
522 | |
523 | if (!importDescription.isDependency()) { |
524 | // Add a marker to show that this module has been imported |
525 | types->qmlNames.setType(name: prefixedName(prefix: modulePrefix, name: importDescription.name()), type: {}); |
526 | |
527 | if (import.isStaticModule) |
528 | types->staticModules << import.name; |
529 | |
530 | if (import.isSystemModule) |
531 | types->hasSystemModule = true; |
532 | |
533 | types->warnings.append(l: import.warnings); |
534 | } |
535 | |
536 | for (auto it = import.scripts.begin(); it != import.scripts.end(); ++it) { |
537 | // You cannot have a script without an export |
538 | Q_ASSERT(!it->exports.isEmpty()); |
539 | insertExports(*it, prefixedName(prefix: anonPrefix, name: internalName(scope: it->scope))); |
540 | } |
541 | |
542 | // add objects |
543 | for (const auto &val : import.objects) { |
544 | const QString cppName = isComposite(scope: val.scope) |
545 | ? prefixedName(prefix: anonPrefix, name: internalName(scope: val.scope)) |
546 | : internalName(scope: val.scope); |
547 | |
548 | if (val.exports.isEmpty()) { |
549 | // Insert an unresolvable dummy name |
550 | types->qmlNames.setType( |
551 | name: prefixedName(prefix: internalPrefix, name: cppName), type: { .scope: val.scope, .revision: QTypeRevision() }); |
552 | types->cppNames.setType(name: cppName, type: { .scope: val.scope, .revision: QTypeRevision() }); |
553 | insertAliases(val.scope, QTypeRevision()); |
554 | } else { |
555 | insertExports(val, cppName); |
556 | } |
557 | } |
558 | |
559 | /* We need to create a temporary AvailableTypes instance here to make builtins available as |
560 | QQmlJSScope::resolveTypes relies on them being available. They cannot be part of the regular |
561 | types as they would keep overwriting existing types when loaded from cache. |
562 | This is only a problem with builtin types as only builtin types can be overridden by any |
563 | sibling import. Consider the following qmldir: |
564 | |
565 | module Things |
566 | import QtQml 2.0 |
567 | import QtQuick.LocalStorage auto |
568 | |
569 | The module "Things" sees QtQml's definition of Qt, not the builtins', even though |
570 | QtQuick.LocalStorage does not depend on QtQml and is imported afterwards. Conversely: |
571 | |
572 | module Stuff |
573 | import ModuleOverridingQObject |
574 | import QtQuick |
575 | |
576 | The module "Stuff" sees QtQml's definition of QObject (via QtQuick), even if |
577 | ModuleOverridingQObject has overridden it. |
578 | */ |
579 | |
580 | QQmlJSImporter::AvailableTypes tempTypes(builtinImportHelper().cppNames); |
581 | tempTypes.cppNames.addTypes(types: types->cppNames); |
582 | |
583 | // At present, there are corner cases that couldn't be resolved in a single |
584 | // pass of resolveTypes() (e.g. QQmlEasingEnums::Type). However, such cases |
585 | // only happen when enumerations are involved, thus the strategy is to |
586 | // resolve enumerations (which can potentially create new child scopes) |
587 | // before resolving the type fully |
588 | const QQmlJSScope::ConstPtr arrayType = tempTypes.cppNames.type(name: u"Array"_s).scope; |
589 | for (auto it = import.objects.begin(); it != import.objects.end(); ++it) { |
590 | if (!it->scope.factory()) { |
591 | QQmlJSScope::resolveEnums(self: it->scope, contextualTypes: tempTypes.cppNames); |
592 | QQmlJSScope::resolveList(self: it->scope, arrayType); |
593 | } |
594 | } |
595 | |
596 | for (const auto &val : std::as_const(t: import.objects)) { |
597 | // Otherwise we have already done it in localFile2ScopeTree() |
598 | if (!val.scope.factory() && val.scope->baseType().isNull()) { |
599 | |
600 | // Composite types use QML names, and we should have resolved those already. |
601 | // ... except that old qmltypes files might specify composite types with C++ names. |
602 | // Warn about those. |
603 | if (val.scope->isComposite()) { |
604 | types->warnings.append(t: { |
605 | QStringLiteral("Found incomplete composite type %1. Do not use qmlplugindump.") |
606 | .arg(a: val.scope->internalName()), |
607 | .type: QtWarningMsg, |
608 | .loc: QQmlJS::SourceLocation() |
609 | }); |
610 | } |
611 | |
612 | QQmlJSScope::resolveNonEnumTypes(self: val.scope, contextualTypes: tempTypes.cppNames); |
613 | } |
614 | } |
615 | } |
616 | |
617 | /*! |
618 | * \internal |
619 | * Imports builtins, but only the subset hardcoded into the parser. |
620 | */ |
621 | QQmlJSImporter::ImportedTypes QQmlJSImporter::importHardCodedBuiltins() |
622 | { |
623 | const auto builtins = builtinImportHelper(); |
624 | |
625 | QQmlJS::ContextualTypes result( |
626 | QQmlJS::ContextualTypes::QML, {}, {}, builtins.cppNames.arrayType()); |
627 | for (const QString hardcoded : { |
628 | "void"_L1, "int"_L1, "bool"_L1, "double"_L1, "real"_L1, "string"_L1, "url"_L1, |
629 | "date"_L1, "regexp"_L1, "rect"_L1, "point"_L1, "size"_L1, "variant"_L1, "var"_L1, |
630 | }) { |
631 | |
632 | const auto type = builtins.qmlNames.type(name: hardcoded); |
633 | Q_ASSERT(type.scope); |
634 | result.setType(name: hardcoded, type); |
635 | } |
636 | |
637 | return ImportedTypes(std::move(result), {}); |
638 | } |
639 | |
640 | |
641 | QQmlJSImporter::AvailableTypes QQmlJSImporter::builtinImportHelper() |
642 | { |
643 | if (m_builtins) |
644 | return *m_builtins; |
645 | |
646 | AvailableTypes builtins(QQmlJS::ContextualTypes(QQmlJS::ContextualTypes::INTERNAL, {}, {}, {})); |
647 | |
648 | importHelper(module: u"QML"_s, types: &builtins, prefix: QString(), version: QTypeRevision::fromVersion(majorVersion: 1, minorVersion: 0)); |
649 | |
650 | QQmlJSScope::ConstPtr arrayType = builtins.cppNames.type(name: u"Array"_s).scope; |
651 | Q_ASSERT(arrayType); |
652 | |
653 | m_builtins = AvailableTypes(QQmlJS::ContextualTypes( |
654 | QQmlJS::ContextualTypes::INTERNAL, builtins.cppNames.types(), builtins.cppNames.names(), |
655 | arrayType)); |
656 | m_builtins->qmlNames = QQmlJS::ContextualTypes( |
657 | QQmlJS::ContextualTypes::QML, builtins.qmlNames.types(), builtins.qmlNames.names(), |
658 | arrayType); |
659 | m_builtins->staticModules = std::move(builtins.staticModules); |
660 | m_builtins->warnings = std::move(builtins.warnings); |
661 | m_builtins->hasSystemModule = builtins.hasSystemModule; |
662 | |
663 | return *m_builtins; |
664 | } |
665 | |
666 | /*! |
667 | * Imports types from the specified \a qmltypesFiles. |
668 | */ |
669 | QList<QQmlJS::DiagnosticMessage> QQmlJSImporter::importQmldirs(const QStringList &qmldirFiles) |
670 | { |
671 | QList<QQmlJS::DiagnosticMessage> warnings; |
672 | for (const auto &file : qmldirFiles) { |
673 | Import result; |
674 | QString qmldirName; |
675 | if (file.endsWith(s: SlashQmldir)) { |
676 | result = readQmldir(modulePath: file.chopped(n: SlashQmldir.size())); |
677 | setQualifiedNamesOn(result); |
678 | qmldirName = file; |
679 | } else { |
680 | warnings.append(t: { |
681 | QStringLiteral("Argument %1 to -i option is not a qmldir file. Assuming qmltypes.") |
682 | .arg(a: file), |
683 | .type: QtWarningMsg, |
684 | .loc: QQmlJS::SourceLocation() |
685 | }); |
686 | |
687 | readQmltypes(filename: file, result: &result); |
688 | |
689 | // Append _FAKE_QMLDIR to our made up qmldir name so that if it ever gets used somewhere |
690 | // else except for cache lookups, it will blow up due to a missing file instead of |
691 | // producing weird results. |
692 | qmldirName = file + QStringLiteral("_FAKE_QMLDIR"); |
693 | } |
694 | |
695 | warnings.append(l: result.warnings); |
696 | m_seenQmldirFiles.insert(key: qmldirName, value: result); |
697 | |
698 | for (const auto &object : std::as_const(t&: result.objects)) { |
699 | for (const auto &ex : object.exports) { |
700 | m_seenImports.insert(key: {ex.package(), ex.version()}, value: qmldirName); |
701 | // We also have to handle the case that no version is provided |
702 | m_seenImports.insert(key: {ex.package(), QTypeRevision()}, value: qmldirName); |
703 | } |
704 | } |
705 | } |
706 | |
707 | return warnings; |
708 | } |
709 | |
710 | QQmlJSImporter::ImportedTypes QQmlJSImporter::importModule(const QString &module, |
711 | const QString &prefix, |
712 | QTypeRevision version, |
713 | QStringList *staticModuleList) |
714 | { |
715 | const AvailableTypes builtins = builtinImportHelper(); |
716 | AvailableTypes result(builtins.cppNames); |
717 | if (!importHelper(module, types: &result, prefix, version)) { |
718 | result.warnings.append(t: { |
719 | QStringLiteral("Failed to import %1. Are your import paths set up properly?") |
720 | .arg(a: module), |
721 | .type: QtWarningMsg, |
722 | .loc: QQmlJS::SourceLocation() |
723 | }); |
724 | } |
725 | |
726 | if (staticModuleList) |
727 | *staticModuleList << result.staticModules; |
728 | |
729 | return ImportedTypes(std::move(result.qmlNames), std::move(result.warnings)); |
730 | } |
731 | |
732 | QQmlJSImporter::ImportedTypes QQmlJSImporter::builtinInternalNames() |
733 | { |
734 | auto builtins = builtinImportHelper(); |
735 | return ImportedTypes(std::move(builtins.cppNames), std::move(builtins.warnings)); |
736 | } |
737 | |
738 | bool QQmlJSImporter::importHelper(const QString &module, AvailableTypes *types, |
739 | const QString &prefix, QTypeRevision version, bool isDependency, |
740 | bool isFile) |
741 | { |
742 | // QtQuick/Controls and QtQuick.Controls are the same module |
743 | const QString moduleCacheName = QString(module).replace(before: u'/', after: u'.'); |
744 | |
745 | if (isDependency) |
746 | Q_ASSERT(prefix.isEmpty()); |
747 | |
748 | const QQmlJS::Import cacheKey(prefix, moduleCacheName, version, isFile, isDependency); |
749 | |
750 | auto getTypesFromCache = [&]() -> bool { |
751 | if (!m_cachedImportTypes.contains(key: cacheKey)) |
752 | return false; |
753 | |
754 | const auto &cacheEntry = m_cachedImportTypes[cacheKey]; |
755 | |
756 | types->cppNames.addTypes(types: cacheEntry->cppNames); |
757 | types->staticModules << cacheEntry->staticModules; |
758 | types->hasSystemModule |= cacheEntry->hasSystemModule; |
759 | |
760 | // No need to import qml names for dependencies |
761 | if (!isDependency) { |
762 | types->warnings.append(l: cacheEntry->warnings); |
763 | types->qmlNames.addTypes(types: cacheEntry->qmlNames); |
764 | } |
765 | |
766 | return true; |
767 | }; |
768 | |
769 | if (getTypesFromCache()) |
770 | return true; |
771 | |
772 | auto cacheTypes = QSharedPointer<QQmlJSImporter::AvailableTypes>( |
773 | new QQmlJSImporter::AvailableTypes(QQmlJS::ContextualTypes( |
774 | QQmlJS::ContextualTypes::INTERNAL, {}, {}, types->cppNames.arrayType()))); |
775 | m_cachedImportTypes[cacheKey] = cacheTypes; |
776 | |
777 | const QPair<QString, QTypeRevision> importId { module, version }; |
778 | const auto it = m_seenImports.constFind(key: importId); |
779 | |
780 | if (it != m_seenImports.constEnd()) { |
781 | if (it->isEmpty()) |
782 | return false; |
783 | |
784 | Q_ASSERT(m_seenQmldirFiles.contains(*it)); |
785 | const QQmlJSImporter::Import import = m_seenQmldirFiles.value(key: *it); |
786 | |
787 | importDependencies(import, types: cacheTypes.get(), prefix, version, isDependency); |
788 | processImport(importDescription: cacheKey, import, types: cacheTypes.get()); |
789 | |
790 | const bool typesFromCache = getTypesFromCache(); |
791 | Q_ASSERT(typesFromCache); |
792 | return typesFromCache; |
793 | } |
794 | |
795 | QStringList modulePaths; |
796 | if (isFile) { |
797 | const auto import = readDirectory(directory: module); |
798 | m_seenQmldirFiles.insert(key: module, value: import); |
799 | m_seenImports.insert(key: importId, value: module); |
800 | importDependencies(import, types: cacheTypes.get(), prefix, version, isDependency); |
801 | processImport(importDescription: cacheKey, import, types: cacheTypes.get()); |
802 | |
803 | // Try to load a qmldir below, on top of the directory import. |
804 | modulePaths.append(t: module); |
805 | } else if (module == "QML"_L1) { |
806 | // Always load builtins from the resource file system. We don't want any nasty surprises |
807 | // that could surface when loading them from an actual import path. We rely on the builtins |
808 | // to contain a number of types later on. It's better to always use our own copy. |
809 | modulePaths = { ":/qt-project.org/imports/QML"_L1}; |
810 | } else { |
811 | modulePaths = qQmlResolveImportPaths(uri: module, basePaths: m_importPaths, version); |
812 | } |
813 | |
814 | for (auto const &modulePath : modulePaths) { |
815 | QString qmldirPath = modulePath + SlashQmldir; |
816 | if (modulePath.startsWith(c: u':')) { |
817 | if (module == "QML"_L1) { |
818 | // Do not try to map the builtins' resource path. |
819 | } else if (m_mapper) { |
820 | const QString resourcePath = modulePath.mid( |
821 | position: 1, n: modulePath.endsWith(c: u'/') ? modulePath.size() - 2 : -1) |
822 | + SlashQmldir; |
823 | const auto entry = m_mapper->entry( |
824 | filter: QQmlJSResourceFileMapper::resourceFileFilter(file: resourcePath)); |
825 | qmldirPath = entry.filePath; |
826 | } else { |
827 | // The builtins are loaded from the resource file system. Don't warn about them. |
828 | qWarning() << "Cannot read files from resource directory"<< modulePath |
829 | << "because no resource file mapper was provided"; |
830 | } |
831 | } |
832 | |
833 | const auto it = m_seenQmldirFiles.constFind(key: qmldirPath); |
834 | if (it != m_seenQmldirFiles.constEnd()) { |
835 | const QQmlJSImporter::Import import = *it; |
836 | m_seenImports.insert(key: importId, value: qmldirPath); |
837 | importDependencies(import, types: cacheTypes.get(), prefix, version, isDependency); |
838 | processImport(importDescription: cacheKey, import, types: cacheTypes.get()); |
839 | |
840 | const bool typesFromCache = getTypesFromCache(); |
841 | Q_ASSERT(typesFromCache); |
842 | return typesFromCache; |
843 | } |
844 | |
845 | const QFileInfo file(qmldirPath); |
846 | if (file.exists()) { |
847 | const auto import = readQmldir(modulePath: file.canonicalPath()); |
848 | setQualifiedNamesOn(import); |
849 | m_seenQmldirFiles.insert(key: qmldirPath, value: import); |
850 | m_seenImports.insert(key: importId, value: qmldirPath); |
851 | importDependencies(import, types: cacheTypes.get(), prefix, version, isDependency); |
852 | |
853 | // Potentially merges with the result of readDirectory() above. |
854 | processImport(importDescription: cacheKey, import, types: cacheTypes.get()); |
855 | |
856 | const bool typesFromCache = getTypesFromCache(); |
857 | Q_ASSERT(typesFromCache); |
858 | return typesFromCache; |
859 | } |
860 | } |
861 | |
862 | if (isFile) { |
863 | // We've loaded the directory above |
864 | const bool typesFromCache = getTypesFromCache(); |
865 | Q_ASSERT(typesFromCache); |
866 | return typesFromCache; |
867 | } |
868 | |
869 | m_seenImports.insert(key: importId, value: QString()); |
870 | return false; |
871 | } |
872 | |
873 | QQmlJSScope::Ptr QQmlJSImporter::localFile2ScopeTree(const QString &filePath) |
874 | { |
875 | const QString sourceFolderFile = preferQmlFilesFromSourceFolder() |
876 | ? QQmlJSUtils::qmlSourcePathFromBuildPath(mapper: m_mapper, pathInBuildFolder: filePath) |
877 | : filePath; |
878 | |
879 | const auto seen = m_importedFiles.find(key: sourceFolderFile); |
880 | if (seen != m_importedFiles.end()) |
881 | return *seen; |
882 | |
883 | return *m_importedFiles.insert( |
884 | key: sourceFolderFile, |
885 | value: { QQmlJSScope::create(), |
886 | QSharedPointer<QDeferredFactory<QQmlJSScope>>(new QDeferredFactory<QQmlJSScope>( |
887 | this, sourceFolderFile)) }); |
888 | } |
889 | |
890 | QQmlJSScope::Ptr QQmlJSImporter::importFile(const QString &file) |
891 | { |
892 | return localFile2ScopeTree(filePath: file); |
893 | } |
894 | |
895 | QQmlJSImporter::ImportedTypes QQmlJSImporter::importDirectory( |
896 | const QString &directory, const QString &prefix) |
897 | { |
898 | const AvailableTypes builtins = builtinImportHelper(); |
899 | QQmlJSImporter::AvailableTypes types(QQmlJS::ContextualTypes( |
900 | QQmlJS::ContextualTypes::INTERNAL, {}, {}, builtins.cppNames.arrayType())); |
901 | importHelper(module: directory, types: &types, prefix, version: QTypeRevision(), isDependency: false, isFile: true); |
902 | return ImportedTypes(std::move(types.qmlNames), std::move(types.warnings)); |
903 | } |
904 | |
905 | void QQmlJSImporter::setImportPaths(const QStringList &importPaths) |
906 | { |
907 | m_importPaths = importPaths; |
908 | |
909 | // We have to get rid off all cache elements directly referencing modules, since changing |
910 | // importPaths might change which module is found first |
911 | m_seenImports.clear(); |
912 | m_cachedImportTypes.clear(); |
913 | // Luckily this doesn't apply to m_seenQmldirFiles |
914 | } |
915 | |
916 | void QQmlJSImporter::clearCache() |
917 | { |
918 | m_seenImports.clear(); |
919 | m_cachedImportTypes.clear(); |
920 | m_seenQmldirFiles.clear(); |
921 | m_importedFiles.clear(); |
922 | m_builtins.reset(); |
923 | } |
924 | |
925 | QQmlJSScope::ConstPtr QQmlJSImporter::jsGlobalObject() |
926 | { |
927 | return builtinImportHelper().cppNames.type(name: u"GlobalObject"_s).scope; |
928 | } |
929 | |
930 | void QQmlJSImporter::setQualifiedNamesOn(const Import &import) |
931 | { |
932 | for (auto &object : import.objects) { |
933 | if (object.exports.isEmpty()) |
934 | continue; |
935 | if (auto *factory = object.scope.factory()) { |
936 | factory->setModuleName(import.name); |
937 | } else { |
938 | object.scope->setOwnModuleName(import.name); |
939 | } |
940 | } |
941 | } |
942 | |
943 | void QQmlJSImporter::runImportVisitor(QQmlJS::AST::Node *rootNode, |
944 | const ImportVisitorPrerequisites &p) |
945 | { |
946 | m_importVisitor(rootNode, this, p); |
947 | } |
948 | |
949 | QT_END_NAMESPACE |
950 |
Definitions
- SlashQmldir
- PluginsDotQmltypes
- JsrootDotQmltypes
- Import
- isValid
- prefixedName
- createQmldirParserForFile
- readQmltypes
- internalName
- isComposite
- aliases
- QQmlJSImporter
- resolvePreferredPath
- readQmldir
- readDirectory
- importDependencies
- isVersionAllowed
- processImport
- importHardCodedBuiltins
- builtinImportHelper
- importQmldirs
- importModule
- builtinInternalNames
- importHelper
- localFile2ScopeTree
- importFile
- importDirectory
- setImportPaths
- clearCache
- jsGlobalObject
- setQualifiedNamesOn
Learn Advanced QML with KDAB
Find out more