File indexing completed on 2024-05-19 04:41:58

0001 /*
0002     SPDX-FileCopyrightText: 2013 Sven Brauch <svenbrauch@googlemail.com>
0003     SPDX-FileCopyrightText: 2014 Denis Steckelmacher <steckdenis@yahoo.fr>
0004 
0005     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0006 */
0007 
0008 #include "cache.h"
0009 #include "debug.h"
0010 
0011 #include <QString>
0012 #include <QProcess>
0013 #include <QDir>
0014 #include <QStandardPaths>
0015 #include <QCryptographicHash>
0016 #include <QCoreApplication>
0017 
0018 QmlJS::Cache::Cache()
0019 {
0020     // qmlplugindump from Qt4 and Qt5. They will be tried in order when dumping
0021     // a binary QML file.
0022     m_pluginDumpExecutables
0023         << PluginDumpExecutable(QStringLiteral("qmlplugindump"), QStringLiteral("1.0"))
0024         << PluginDumpExecutable(QStringLiteral("qmlplugindump-qt4"), QStringLiteral("1.0"))
0025         << PluginDumpExecutable(QStringLiteral("qmlplugindump-qt5"), QStringLiteral("2.0"))
0026         << PluginDumpExecutable(QStringLiteral("qml1plugindump-qt5"), QStringLiteral("1.0"));
0027 }
0028 
0029 QmlJS::Cache& QmlJS::Cache::instance()
0030 {
0031     static Cache *c = nullptr;
0032 
0033     if (!c) {
0034         c = new Cache();
0035     }
0036 
0037     return *c;
0038 }
0039 
0040 KDevelop::Path::List QmlJS::Cache::libraryPaths(const KDevelop::IndexedString& baseFile) const
0041 {
0042     QMutexLocker lock(&m_mutex);
0043     return libraryPaths_internal(baseFile);
0044 }
0045 
0046 KDevelop::Path::List QmlJS::Cache::libraryPaths_internal(const KDevelop::IndexedString& baseFile) const
0047 {
0048     Q_ASSERT(!m_mutex.try_lock());
0049 
0050     KDevelop::Path::List paths;
0051 
0052     const auto& libraryPaths = QCoreApplication::instance()->libraryPaths();
0053     for (auto& path : libraryPaths) {
0054         KDevelop::Path p(path);
0055 
0056         // Change /path/to/qt5/plugins to /path/to/qt5/{qml,imports}
0057         paths << p.cd(QStringLiteral("../qml"));
0058         paths << p.cd(QStringLiteral("../imports"));
0059     }
0060 
0061     paths << m_includeDirs[baseFile];
0062     return paths;
0063 }
0064 
0065 QString QmlJS::Cache::modulePath(const KDevelop::IndexedString& baseFile, const QString& uri, const QString& version)
0066 {
0067     QMutexLocker lock(&m_mutex);
0068     QString cacheKey = uri + version;
0069     QString path = m_modulePaths.value(cacheKey, QString());
0070 
0071     if (!path.isEmpty()) {
0072         return path;
0073     }
0074 
0075     // Find the path for which <path>/u/r/i exists
0076     QString fragment = QString(uri).replace(QLatin1Char('.'), QDir::separator());
0077     bool isVersion1 = version.startsWith(QLatin1String("1."));
0078     bool isQtQuick = (uri == QLatin1String("QtQuick"));
0079 
0080     const QStringList modulesWithoutVersionSuffix{
0081         QStringLiteral("QtQml"),
0082         QStringLiteral("QtMultimedia"),
0083         QStringLiteral("QtQuick.LocalStorage"),
0084         QStringLiteral("QtQuick.XmlListModel"),
0085     };
0086 
0087     if (!isVersion1 && !fragment.isEmpty() && !fragment.endsWith(QLatin1Char('/')) && !version.isEmpty()
0088         && !modulesWithoutVersionSuffix.contains(uri)) {
0089         // Modules having a version greater or equal to 2 are stored in a directory
0090         // name like QtQuick.2
0091         fragment += QLatin1Char('.') + version.section(QLatin1Char('.'), 0, 0);
0092     }
0093 
0094     const auto paths = libraryPaths_internal(baseFile);
0095     for (auto& p : paths) {
0096         QString pathString = p.cd(fragment).path();
0097 
0098         // HACK: QtQuick 1.0 is put in $LIB/qt5/imports/builtins.qmltypes. The "QtQuick"
0099         //       identifier appears nowhere.
0100         if (isQtQuick && isVersion1) {
0101             if (QFile::exists(p.cd(QStringLiteral("builtins.qmltypes")).path())) {
0102                 path = p.path();
0103                 break;
0104             }
0105         } else if (QFile::exists(pathString + QLatin1String("/plugins.qmltypes"))) {
0106             path = pathString;
0107             break;
0108         }
0109     }
0110 
0111     m_modulePaths.insert(cacheKey, path);
0112     return path;
0113 }
0114 
0115 QStringList QmlJS::Cache::getFileNames(const QFileInfoList& fileInfos)
0116 {
0117     QStringList result;
0118 
0119     for (const QFileInfo& fileInfo : fileInfos) {
0120         QString filePath = fileInfo.canonicalFilePath();
0121 
0122         // If the module directory contains a plugins.qmltypes files, use it
0123         // and skip everything else
0124         if (filePath.endsWith(QLatin1String("plugins.qmltypes"))) {
0125             return QStringList() << filePath;
0126         } else if (fileInfo.dir().exists(QStringLiteral("plugins.qmltypes"))) {
0127             return {fileInfo.dir().filePath(QStringLiteral("plugins.qmltypes"))};
0128         }
0129 
0130         // Non-so files don't need any treatment
0131         if (!filePath.endsWith(QLatin1String(".so"))) {
0132             result.append(filePath);
0133             continue;
0134         }
0135 
0136         // Use the cache to speed-up reparses
0137         {
0138             QMutexLocker lock(&m_mutex);
0139 
0140             const auto modulePathIt = m_modulePaths.constFind(filePath);
0141             if (modulePathIt != m_modulePaths.constEnd()) {
0142                 const QString& cachedFilePath = *modulePathIt;
0143 
0144                 if (!cachedFilePath.isEmpty()) {
0145                     result.append(cachedFilePath);
0146                 }
0147 
0148                 continue;
0149             }
0150         }
0151 
0152         // Locate an existing dump of the file
0153         QString dumpFile = QStringLiteral("kdevqmljssupport/%1.qml").arg(
0154             QString::fromLatin1(QCryptographicHash::hash(filePath.toUtf8(), QCryptographicHash::Md5).toHex())
0155         );
0156         QString dumpPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation,
0157             dumpFile
0158         );
0159 
0160         if (!dumpPath.isEmpty()) {
0161             QMutexLocker lock(&m_mutex);
0162 
0163             result.append(dumpPath);
0164             m_modulePaths.insert(filePath, dumpPath);
0165             continue;
0166         }
0167 
0168         // Create a dump of the file
0169         const QStringList args = {QStringLiteral("-noinstantiate"), QStringLiteral("-path"), filePath};
0170 
0171         for (const PluginDumpExecutable& executable : qAsConst(m_pluginDumpExecutables)) {
0172             QProcess qmlplugindump;
0173             qmlplugindump.setProcessChannelMode(QProcess::SeparateChannels);
0174             qmlplugindump.start(executable.executable, args, QIODevice::ReadOnly);
0175 
0176             qCDebug(KDEV_QMLJS_DUCHAIN) << "starting qmlplugindump with args:" << executable.executable << args << qmlplugindump.state() << fileInfo.absolutePath();
0177 
0178             if (!qmlplugindump.waitForFinished(3000)) {
0179                 if (qmlplugindump.state() == QProcess::Running) {
0180                     qCWarning(KDEV_QMLJS_DUCHAIN) << "qmlplugindump didn't finish in time -- killing";
0181                     qmlplugindump.kill();
0182                     qmlplugindump.waitForFinished(100);
0183                 } else {
0184                     qCDebug(KDEV_QMLJS_DUCHAIN) << "qmlplugindump attempt failed" << qmlplugindump.program() << qmlplugindump.arguments() << qmlplugindump.readAllStandardError();
0185                 }
0186                 continue;
0187             }
0188 
0189             if (qmlplugindump.exitCode() != 0) {
0190                 qCWarning(KDEV_QMLJS_DUCHAIN) << "qmlplugindump finished with exit code:" << qmlplugindump.exitCode();
0191                 continue;
0192             }
0193 
0194             // Open a file in which the dump can be written
0195             QFile dumpFile(
0196                 QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)
0197                 + dumpPath
0198             );
0199 
0200             if (dumpFile.open(QIODevice::WriteOnly)) {
0201                 qmlplugindump.readLine();   // Skip "import QtQuick.tooling 1.1"
0202 
0203                 dumpFile.write("// " + filePath.toUtf8() + '\n');
0204                 dumpFile.write("import QtQuick " + executable.quickVersion.toUtf8() + '\n');
0205                 dumpFile.write(qmlplugindump.readAllStandardOutput());
0206                 dumpFile.close();
0207 
0208                 result.append(dumpFile.fileName());
0209 
0210                 QMutexLocker lock(&m_mutex);
0211                 m_modulePaths.insert(filePath, dumpFile.fileName());
0212                 break;
0213             }
0214         }
0215     }
0216 
0217     return result;
0218 }
0219 
0220 void QmlJS::Cache::setFileCustomIncludes(const KDevelop::IndexedString& file, const KDevelop::Path::List& dirs)
0221 {
0222     QMutexLocker lock(&m_mutex);
0223 
0224     m_includeDirs[file] = dirs;
0225 }
0226 
0227 void QmlJS::Cache::addDependency(const KDevelop::IndexedString& file, const KDevelop::IndexedString& dependency)
0228 {
0229     QMutexLocker lock(&m_mutex);
0230 
0231     m_dependees[dependency].insert(file);
0232     m_dependencies[file].insert(dependency);
0233 }
0234 
0235 QList<KDevelop::IndexedString> QmlJS::Cache::filesThatDependOn(const KDevelop::IndexedString& file)
0236 {
0237     QMutexLocker lock(&m_mutex);
0238 
0239     return m_dependees[file].values();
0240 }
0241 
0242 QList<KDevelop::IndexedString> QmlJS::Cache::dependencies(const KDevelop::IndexedString& file)
0243 {
0244     QMutexLocker lock(&m_mutex);
0245 
0246     return m_dependencies[file].values();
0247 }
0248 
0249 bool QmlJS::Cache::isUpToDate(const KDevelop::IndexedString& file)
0250 {
0251     QMutexLocker lock(&m_mutex);
0252 
0253     return m_isUpToDate.value(file, false);
0254 }
0255 
0256 void QmlJS::Cache::setUpToDate(const KDevelop::IndexedString& file, bool upToDate)
0257 {
0258     QMutexLocker lock(&m_mutex);
0259 
0260     m_isUpToDate[file] = upToDate;
0261 }