| 1 | // Copyright (C) 2020 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
| 3 | #include "qqmldomtop_p.h" |
| 4 | #include "qqmldomelements_p.h" |
| 5 | #include "qqmldom_utils_p.h" |
| 6 | |
| 7 | #include <QtCore/QDir> |
| 8 | #include <QtCore/QFileInfo> |
| 9 | #include <QtCore/QScopeGuard> |
| 10 | |
| 11 | #include <memory> |
| 12 | |
| 13 | QT_BEGIN_NAMESPACE |
| 14 | namespace QQmlJS { |
| 15 | namespace Dom { |
| 16 | |
| 17 | static ErrorGroups myVersioningErrors() |
| 18 | { |
| 19 | static ErrorGroups res = { .groups: { DomItem::domErrorGroup, NewErrorGroup("Exports" ), |
| 20 | NewErrorGroup("Version" ) } }; |
| 21 | return res; |
| 22 | } |
| 23 | |
| 24 | static ErrorGroups myExportErrors() |
| 25 | { |
| 26 | static ErrorGroups res = { .groups: { DomItem::domErrorGroup, NewErrorGroup("Exports" ) } }; |
| 27 | return res; |
| 28 | } |
| 29 | |
| 30 | bool ModuleScope::iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const |
| 31 | { |
| 32 | bool cont = true; |
| 33 | cont = cont && self.dvValueField(visitor, f: Fields::uri, value: uri); |
| 34 | cont = cont && self.dvWrapField(visitor, f: Fields::version, obj: version); |
| 35 | cont = cont && self.dvItemField(visitor, f: Fields::exports, it: [this, &self]() { |
| 36 | int minorVersion = version.minorVersion; |
| 37 | return self.subMapItem(map: Map( |
| 38 | self.pathFromOwner().field(name: Fields::exports), |
| 39 | [minorVersion](const DomItem &mapExp, const QString &name) -> DomItem { |
| 40 | DomItem mapExpOw = mapExp.owner(); |
| 41 | QList<DomItem> exports = |
| 42 | mapExp.ownerAs<ModuleIndex>()->exportsWithNameAndMinorVersion( |
| 43 | self: mapExpOw, name, minorVersion); |
| 44 | return mapExp.subListItem(list: List::fromQList<DomItem>( |
| 45 | pathFromOwner: mapExp.pathFromOwner().key(name), list: exports, |
| 46 | elWrapper: [](const DomItem &, const PathEls::PathComponent &, const DomItem &el) { |
| 47 | return el; |
| 48 | }, |
| 49 | options: ListOptions::Normal)); |
| 50 | }, |
| 51 | [](const DomItem &mapExp) { |
| 52 | DomItem mapExpOw = mapExp.owner(); |
| 53 | return mapExp.ownerAs<ModuleIndex>()->exportNames(self: mapExpOw); |
| 54 | }, |
| 55 | QLatin1String("List<Exports>" ))); |
| 56 | }); |
| 57 | cont = cont && self.dvItemField(visitor, f: Fields::symbols, it: [&self]() { |
| 58 | Path basePath = Path::Current(c: PathCurrent::Obj).field(name: Fields::exports); |
| 59 | return self.subMapItem(map: Map( |
| 60 | self.pathFromOwner().field(name: Fields::symbols), |
| 61 | [basePath](const DomItem &mapExp, const QString &name) -> DomItem { |
| 62 | QList<Path> symb({ basePath.key(name) }); |
| 63 | return mapExp.subReferencesItem(c: PathEls::Key(name), paths: symb); |
| 64 | }, |
| 65 | [](const DomItem &mapExp) { |
| 66 | DomItem mapExpOw = mapExp.owner(); |
| 67 | return mapExp.ownerAs<ModuleIndex>()->exportNames(self: mapExpOw); |
| 68 | }, |
| 69 | QLatin1String("List<References>" ))); |
| 70 | }); |
| 71 | cont = cont && self.dvItemField(visitor, f: Fields::autoExports, it: [this, &self]() { |
| 72 | return containingObject(self).field(name: Fields::autoExports); |
| 73 | }); |
| 74 | return cont; |
| 75 | } |
| 76 | |
| 77 | std::shared_ptr<OwningItem> ModuleIndex::doCopy(const DomItem &) const |
| 78 | { |
| 79 | return std::make_shared<ModuleIndex>(args: *this); |
| 80 | } |
| 81 | |
| 82 | ModuleIndex::ModuleIndex(const ModuleIndex &o) |
| 83 | : OwningItem(o), m_uri(o.uri()), m_majorVersion(o.majorVersion()) |
| 84 | { |
| 85 | QMap<int, ModuleScope *> scopes; |
| 86 | { |
| 87 | QMutexLocker l2(o.mutex()); |
| 88 | m_qmltypesFilesPaths += o.m_qmltypesFilesPaths; |
| 89 | m_qmldirPaths += o.m_qmldirPaths; |
| 90 | m_directoryPaths += o.m_directoryPaths; |
| 91 | scopes = o.m_moduleScope; |
| 92 | } |
| 93 | auto it = scopes.begin(); |
| 94 | auto end = scopes.end(); |
| 95 | while (it != end) { |
| 96 | ensureMinorVersion(minorVersion: (*it)->version.minorVersion); |
| 97 | ++it; |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | ModuleIndex::~ModuleIndex() |
| 102 | { |
| 103 | QMap<int, ModuleScope *> scopes; |
| 104 | { |
| 105 | QMutexLocker l(mutex()); |
| 106 | scopes = m_moduleScope; |
| 107 | m_moduleScope.clear(); |
| 108 | } |
| 109 | auto it = scopes.begin(); |
| 110 | auto end = scopes.end(); |
| 111 | while (it != end) { |
| 112 | delete *it; |
| 113 | ++it; |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | bool ModuleIndex::iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const |
| 118 | { |
| 119 | bool cont = self.dvValueField(visitor, f: Fields::uri, value: uri()); |
| 120 | cont = cont && self.dvValueField(visitor, f: Fields::majorVersion, value: majorVersion()); |
| 121 | cont = cont && self.dvItemField(visitor, f: Fields::moduleScope, it: [this, &self]() { |
| 122 | return self.subMapItem(map: Map( |
| 123 | pathFromOwner(self).field(name: Fields::moduleScope), |
| 124 | [](const DomItem &map, const QString &minorVersionStr) { |
| 125 | bool ok; |
| 126 | int minorVersion = minorVersionStr.toInt(ok: &ok); |
| 127 | if (minorVersionStr.isEmpty() |
| 128 | || minorVersionStr.compare(s: u"Latest" , cs: Qt::CaseInsensitive) == 0) |
| 129 | minorVersion = Version::Latest; |
| 130 | else if (!ok) |
| 131 | return DomItem(); |
| 132 | return map.copy(base: map.ownerAs<ModuleIndex>()->ensureMinorVersion(minorVersion)); |
| 133 | }, |
| 134 | [this](const DomItem &) { |
| 135 | QSet<QString> res; |
| 136 | for (int el : minorVersions()) |
| 137 | if (el >= 0) |
| 138 | res.insert(value: QString::number(el)); |
| 139 | if (!minorVersions().isEmpty()) |
| 140 | res.insert(value: QString()); |
| 141 | return res; |
| 142 | }, |
| 143 | QLatin1String("Map<List<Exports>>" ))); |
| 144 | }); |
| 145 | cont = cont && self.dvItemField(visitor, f: Fields::sources, it: [this, &self]() { |
| 146 | return self.subReferencesItem(c: PathEls::Field(Fields::sources), paths: sources()); |
| 147 | }); |
| 148 | cont = cont && self.dvValueLazyField(visitor, f: Fields::autoExports, valueF: [this, &self]() { |
| 149 | return autoExports(self); |
| 150 | }); |
| 151 | return cont; |
| 152 | } |
| 153 | |
| 154 | QSet<QString> ModuleIndex::exportNames(const DomItem &self) const |
| 155 | { |
| 156 | QSet<QString> res; |
| 157 | QList<Path> mySources = sources(); |
| 158 | for (int i = 0; i < mySources.size(); ++i) { |
| 159 | DomItem source = self.path(p: mySources.at(i)); |
| 160 | res += source.field(name: Fields::exports).keys(); |
| 161 | } |
| 162 | return res; |
| 163 | } |
| 164 | |
| 165 | QList<DomItem> ModuleIndex::autoExports(const DomItem &self) const |
| 166 | { |
| 167 | QList<DomItem> res; |
| 168 | Path selfPath = canonicalPath(self).field(name: Fields::autoExports); |
| 169 | RefCacheEntry cached = RefCacheEntry::forPath(el: self, canonicalPath: selfPath); |
| 170 | QList<Path> cachedPaths; |
| 171 | switch (cached.cached) { |
| 172 | case RefCacheEntry::Cached::None: |
| 173 | case RefCacheEntry::Cached::First: |
| 174 | break; |
| 175 | case RefCacheEntry::Cached::All: |
| 176 | cachedPaths += cached.canonicalPaths; |
| 177 | if (cachedPaths.isEmpty()) |
| 178 | return res; |
| 179 | } |
| 180 | DomItem env = self.environment(); |
| 181 | if (!cachedPaths.isEmpty()) { |
| 182 | bool outdated = false; |
| 183 | for (const Path &p : cachedPaths) { |
| 184 | DomItem newEl = env.path(p); |
| 185 | if (!newEl) { |
| 186 | outdated = true; |
| 187 | qWarning() << "referenceCache outdated, reference at " << selfPath |
| 188 | << " leads to invalid path " << p; |
| 189 | break; |
| 190 | } else { |
| 191 | res.append(t: newEl); |
| 192 | } |
| 193 | } |
| 194 | if (outdated) { |
| 195 | res.clear(); |
| 196 | } else { |
| 197 | return res; |
| 198 | } |
| 199 | } |
| 200 | QList<Path> mySources = sources(); |
| 201 | QSet<QString> knownAutoImportUris; |
| 202 | QList<ModuleAutoExport> knownExports; |
| 203 | for (const Path &p : mySources) { |
| 204 | DomItem autoExports = self.path(p).field(name: Fields::autoExports); |
| 205 | for (const DomItem &i : autoExports.values()) { |
| 206 | if (const ModuleAutoExport *iPtr = i.as<ModuleAutoExport>()) { |
| 207 | if (!knownAutoImportUris.contains(value: iPtr->import.uri.toString()) |
| 208 | || !knownExports.contains(t: *iPtr)) { |
| 209 | knownAutoImportUris.insert(value: iPtr->import.uri.toString()); |
| 210 | knownExports.append(t: *iPtr); |
| 211 | res.append(t: i); |
| 212 | cachedPaths.append(t: i.canonicalPath()); |
| 213 | } |
| 214 | } |
| 215 | } |
| 216 | } |
| 217 | RefCacheEntry::addForPath(el: self, canonicalPath: selfPath, |
| 218 | entry: RefCacheEntry { .cached: RefCacheEntry::Cached::All, .canonicalPaths: cachedPaths }); |
| 219 | return res; |
| 220 | } |
| 221 | |
| 222 | QList<DomItem> ModuleIndex::exportsWithNameAndMinorVersion(const DomItem &self, const QString &name, |
| 223 | int minorVersion) const |
| 224 | { |
| 225 | Path myPath = Paths::moduleScopePath(uri: uri(), version: Version(majorVersion(), minorVersion)) |
| 226 | .field(name: Fields::exports) |
| 227 | .key(name); |
| 228 | QList<Path> mySources = sources(); |
| 229 | QList<DomItem> res; |
| 230 | QList<DomItem> undef; |
| 231 | if (minorVersion < 0) |
| 232 | minorVersion = std::numeric_limits<int>::max(); |
| 233 | int vNow = Version::Undefined; |
| 234 | for (int i = 0; i < mySources.size(); ++i) { |
| 235 | DomItem source = self.path(p: mySources.at(i)); |
| 236 | DomItem exports = source.field(name: Fields::exports).key(name); |
| 237 | int nExports = exports.indexes(); |
| 238 | if (nExports == 0) |
| 239 | continue; |
| 240 | for (int j = 0; j < nExports; ++j) { |
| 241 | DomItem exportItem = exports.index(j); |
| 242 | if (!exportItem) |
| 243 | continue; |
| 244 | Version const *versionPtr = exportItem.field(name: Fields::version).as<Version>(); |
| 245 | if (versionPtr == nullptr || !versionPtr->isValid()) { |
| 246 | undef.append(t: exportItem); |
| 247 | } else { |
| 248 | if (majorVersion() < 0) |
| 249 | self.addError(msg: std::move(myVersioningErrors() |
| 250 | .error(message: tr(sourceText: "Module %1 (unversioned) has versioned entries " |
| 251 | "for '%2' from %3" ) |
| 252 | .arg(args: uri(), args: name, |
| 253 | args: source.canonicalPath().toString())) |
| 254 | .withPath(myPath))); |
| 255 | if ((versionPtr->majorVersion == majorVersion() |
| 256 | || versionPtr->majorVersion == Version::Undefined) |
| 257 | && versionPtr->minorVersion >= vNow |
| 258 | && versionPtr->minorVersion <= minorVersion) { |
| 259 | if (versionPtr->minorVersion > vNow) |
| 260 | res.clear(); |
| 261 | res.append(t: exportItem); |
| 262 | vNow = versionPtr->minorVersion; |
| 263 | } |
| 264 | } |
| 265 | } |
| 266 | } |
| 267 | if (!undef.isEmpty()) { |
| 268 | if (!res.isEmpty()) { |
| 269 | self.addError(msg: std::move(myVersioningErrors() |
| 270 | .error(message: tr(sourceText: "Module %1 (major version %2) has versioned and " |
| 271 | "unversioned entries for '%3'" ) |
| 272 | .arg(args: uri(), args: QString::number(majorVersion()), args: name)) |
| 273 | .withPath(myPath))); |
| 274 | return res + undef; |
| 275 | } else { |
| 276 | return undef; |
| 277 | } |
| 278 | } |
| 279 | return res; |
| 280 | } |
| 281 | |
| 282 | QList<Path> ModuleIndex::sources() const |
| 283 | { |
| 284 | QList<Path> res; |
| 285 | QMutexLocker l(mutex()); |
| 286 | res += m_qmltypesFilesPaths; |
| 287 | if (!m_qmldirPaths.isEmpty()) |
| 288 | res += m_qmldirPaths.first(); |
| 289 | else if (!m_directoryPaths.isEmpty()) |
| 290 | res += m_directoryPaths.first(); |
| 291 | return res; |
| 292 | } |
| 293 | |
| 294 | ModuleScope *ModuleIndex::ensureMinorVersion(int minorVersion) |
| 295 | { |
| 296 | if (minorVersion < 0) |
| 297 | minorVersion = Version::Latest; |
| 298 | { |
| 299 | QMutexLocker l(mutex()); |
| 300 | auto it = m_moduleScope.constFind(key: minorVersion); |
| 301 | if (it != m_moduleScope.cend()) |
| 302 | return *it; |
| 303 | } |
| 304 | ModuleScope *res = nullptr; |
| 305 | ModuleScope *newScope = new ModuleScope(m_uri, Version(majorVersion(), minorVersion)); |
| 306 | auto cleanup = qScopeGuard(f: [&newScope] { delete newScope; }); |
| 307 | { |
| 308 | QMutexLocker l(mutex()); |
| 309 | auto it = m_moduleScope.constFind(key: minorVersion); |
| 310 | if (it != m_moduleScope.cend()) { |
| 311 | res = *it; |
| 312 | } else { |
| 313 | res = newScope; |
| 314 | newScope = nullptr; |
| 315 | m_moduleScope.insert(key: minorVersion, value: res); |
| 316 | } |
| 317 | } |
| 318 | return res; |
| 319 | } |
| 320 | |
| 321 | void ModuleIndex::mergeWith(const std::shared_ptr<ModuleIndex> &o) |
| 322 | { |
| 323 | if (o) { |
| 324 | QList<Path> qmltypesPaths; |
| 325 | QMap<int, ModuleScope *> scopes; |
| 326 | { |
| 327 | QMutexLocker l2(o->mutex()); |
| 328 | qmltypesPaths = o->m_qmltypesFilesPaths; |
| 329 | scopes = o->m_moduleScope; |
| 330 | } |
| 331 | { |
| 332 | QMutexLocker l(mutex()); |
| 333 | for (const Path &qttPath : qmltypesPaths) { |
| 334 | if (!m_qmltypesFilesPaths.contains(t: (qttPath))) |
| 335 | m_qmltypesFilesPaths.append(t: qttPath); |
| 336 | } |
| 337 | } |
| 338 | auto it = scopes.begin(); |
| 339 | auto end = scopes.end(); |
| 340 | while (it != end) { |
| 341 | ensureMinorVersion(minorVersion: (*it)->version.minorVersion); |
| 342 | ++it; |
| 343 | } |
| 344 | } |
| 345 | } |
| 346 | |
| 347 | QList<Path> ModuleIndex::qmldirsToLoad(const DomItem &self) |
| 348 | { |
| 349 | // this always checks the filesystem to the qmldir file to load |
| 350 | DomItem env = self.environment(); |
| 351 | std::shared_ptr<DomEnvironment> envPtr = env.ownerAs<DomEnvironment>(); |
| 352 | QStringList subPathComponents = uri().split(sep: u'.'); |
| 353 | QString subPath = subPathComponents.join(sep: u'/'); |
| 354 | QString logicalPath; |
| 355 | QString subPathV = subPath + QChar::fromLatin1(c: '.') + QString::number(majorVersion()) |
| 356 | + QLatin1String("/qmldir" ); |
| 357 | QString dirPath; |
| 358 | if (majorVersion() >= 0) { |
| 359 | qCDebug(QQmlJSDomImporting) |
| 360 | << "ModuleIndex::qmldirsToLoad: Searching versioned module" << subPath |
| 361 | << majorVersion() << "in" << envPtr->loadPaths().join(sep: u", " ); |
| 362 | for (const QString &path : envPtr->loadPaths()) { |
| 363 | QDir dir(path); |
| 364 | QFileInfo fInfo(dir.filePath(fileName: subPathV)); |
| 365 | if (fInfo.isFile()) { |
| 366 | qCDebug(QQmlJSDomImporting) |
| 367 | << "Found versioned module in " << fInfo.canonicalFilePath(); |
| 368 | logicalPath = subPathV; |
| 369 | dirPath = fInfo.canonicalFilePath(); |
| 370 | break; |
| 371 | } |
| 372 | } |
| 373 | } |
| 374 | if (dirPath.isEmpty()) { |
| 375 | qCDebug(QQmlJSDomImporting) << "ModuleIndex::qmldirsToLoad: Searching unversioned module" |
| 376 | << subPath << "in" << envPtr->loadPaths().join(sep: u", " ); |
| 377 | for (const QString &path : envPtr->loadPaths()) { |
| 378 | QDir dir(path); |
| 379 | QFileInfo fInfo(dir.filePath(fileName: subPath + QLatin1String("/qmldir" ))); |
| 380 | if (fInfo.isFile()) { |
| 381 | qCDebug(QQmlJSDomImporting) |
| 382 | << "Found unversioned module in " << fInfo.canonicalFilePath(); |
| 383 | logicalPath = subPath + QLatin1String("/qmldir" ); |
| 384 | dirPath = fInfo.canonicalFilePath(); |
| 385 | break; |
| 386 | } |
| 387 | } |
| 388 | } |
| 389 | if (!dirPath.isEmpty()) { |
| 390 | QMutexLocker l(mutex()); |
| 391 | m_qmldirPaths = QList<Path>({ Paths::qmldirFilePath(path: dirPath) }); |
| 392 | } else if (uri() != u"QML" ) { |
| 393 | const QString loadPaths = envPtr->loadPaths().join(sep: u", "_s ); |
| 394 | qCDebug(QQmlJSDomImporting) << "ModuleIndex::qmldirsToLoad: qmldir at" |
| 395 | << (uri() + u"/qmldir"_s ) << " was not found in " << loadPaths; |
| 396 | addErrorLocal( |
| 397 | msg: myExportErrors() |
| 398 | .warning(message: tr(sourceText: "Failed to find main qmldir file for %1 %2 in %3." ) |
| 399 | .arg(args: uri(), args: QString::number(majorVersion()), args: loadPaths)) |
| 400 | .handle()); |
| 401 | } |
| 402 | return qmldirPaths(); |
| 403 | } |
| 404 | |
| 405 | } // end namespace Dom |
| 406 | } // end namespace QQmlJS |
| 407 | QT_END_NAMESPACE |
| 408 | |