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