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