| 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 | |