File indexing completed on 2024-04-28 04:37:17

0001 /*
0002     SPDX-FileCopyrightText: 2006 Adam Treat <treat@kde.org>
0003     SPDX-FileCopyrightText: 2007 Alexander Dymo <adymo@kdevelop.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "languagecontroller.h"
0009 
0010 #include <algorithm>
0011 #include <utility>
0012 #include <vector>
0013 
0014 #include <QHash>
0015 #include <QMimeDatabase>
0016 #include <QMultiHash>
0017 #include <QMutexLocker>
0018 #include <QRecursiveMutex>
0019 #include <QRegularExpression>
0020 #include <QThread>
0021 
0022 #include <interfaces/idocument.h>
0023 #include <interfaces/idocumentcontroller.h>
0024 #include <interfaces/iplugin.h>
0025 #include <interfaces/iplugincontroller.h>
0026 #include <language/assistant/staticassistantsmanager.h>
0027 #include <language/interfaces/ilanguagesupport.h>
0028 #include <language/backgroundparser/backgroundparser.h>
0029 #include <language/duchain/duchain.h>
0030 
0031 #include "problemmodelset.h"
0032 
0033 #include "core.h"
0034 #include "settings/languagepreferences.h"
0035 #include "completionsettings.h"
0036 #include "debug.h"
0037 
0038 namespace {
0039 QString KEY_SupportedMimeTypes() { return QStringLiteral("X-KDevelop-SupportedMimeTypes"); }
0040 QString KEY_ILanguageSupport() { return QStringLiteral("ILanguageSupport"); }
0041 
0042 bool containsWildcardCharacter(QString::const_iterator first, QString::const_iterator last)
0043 {
0044     const auto isWildcardCharacter = [](QChar character) {
0045         const auto u = character.unicode();
0046         return u == '*' || u == '?' || u == '[';
0047     };
0048     return std::any_of(first, last, isWildcardCharacter);
0049 }
0050 
0051 using KDevelop::ILanguageSupport;
0052 
0053 class MimeTypeCache
0054 {
0055 public:
0056     void addMimeType(const QString& mimeTypeName, ILanguageSupport* language);
0057     QList<ILanguageSupport*> languagesForFileName(const QString& fileName) const;
0058 
0059 private:
0060     void addGlobPattern(const QString& pattern, ILanguageSupport* language);
0061 
0062     using StringLanguagePair = std::pair<QString, ILanguageSupport*>;
0063 
0064     /// key() is the lower-cased last character of the suffix; value().first is e.g. ".cpp".
0065     /// The last character key performs better than the entire lower-cased (last) extension key. Perhaps
0066     /// because no lower-cased temporary fileName's extension string is created in languagesForFileName().
0067     QMultiHash<QChar, StringLanguagePair> m_suffixes;
0068     std::vector<StringLanguagePair> m_literalPatterns; ///< contains e.g. "CMakeLists.txt" as front().first
0069     /// fallback, hopefully empty in practice
0070     std::vector<std::pair<QRegularExpression, ILanguageSupport*>> m_regularExpressions;
0071 };
0072 
0073 void MimeTypeCache::addMimeType(const QString& mimeTypeName, ILanguageSupport* language)
0074 {
0075     const QMimeType mime = QMimeDatabase().mimeTypeForName(mimeTypeName);
0076     if (!mime.isValid()) {
0077         qCWarning(SHELL) << "could not create mime-type" << mimeTypeName;
0078         return;
0079     }
0080 
0081     const auto globPatterns = mime.globPatterns();
0082     for (const QString& pattern : globPatterns) {
0083         addGlobPattern(pattern, language);
0084     }
0085 }
0086 
0087 QList<ILanguageSupport*> MimeTypeCache::languagesForFileName(const QString& fileName) const
0088 {
0089     if (fileName.isEmpty()) {
0090         // The file name of a remote URL that ends with a slash is empty, but such a URL can still reference a file.
0091         // An empty file name cannot match a MIME type.
0092         return {};
0093     }
0094 
0095     QList<ILanguageSupport*> languages;
0096     // lastLanguageEquals() helps to improve performance by skipping checks for an already added language.
0097     const auto lastLanguageEquals = [&languages](const ILanguageSupport* lang) {
0098         return !languages.empty() && languages.constLast() == lang;
0099     };
0100 
0101     const auto lastChar = fileName.back().toLower();
0102     for (auto it = m_suffixes.constFind(lastChar); it != m_suffixes.cend() && it.key() == lastChar; ++it) {
0103         const auto lang = it.value().second;
0104         if (!lastLanguageEquals(lang) && fileName.endsWith(it.value().first, Qt::CaseInsensitive)) {
0105             languages.push_back(lang);
0106         }
0107     }
0108 
0109     for (const auto& p : m_literalPatterns) {
0110         if (fileName.compare(p.first, Qt::CaseInsensitive) == 0 && !lastLanguageEquals(p.second)) {
0111             languages.push_back(p.second);
0112         }
0113     }
0114 
0115     for (const auto& p : m_regularExpressions) {
0116         if (!lastLanguageEquals(p.second) && p.first.match(fileName).hasMatch()) {
0117             languages.push_back(p.second);
0118         }
0119     }
0120 
0121     return languages;
0122 }
0123 
0124 void MimeTypeCache::addGlobPattern(const QString& pattern, ILanguageSupport* language)
0125 {
0126     if (pattern.isEmpty()) {
0127         qCWarning(SHELL) << "Attempt to add an invalid empty glob pattern.";
0128         return; // An empty pattern won't match a filename.
0129     }
0130 
0131     if (pattern.front() == QLatin1Char{'*'}) {
0132         if (pattern.size() > 1 && !containsWildcardCharacter(pattern.cbegin() + 1, pattern.cend())) {
0133             const auto lastChar = pattern.back().toLower();
0134             StringLanguagePair suffix{pattern.mid(1).toLower(), language};
0135             if (!m_suffixes.contains(lastChar, suffix)) {
0136                 m_suffixes.insert(lastChar, std::move(suffix));
0137             }
0138             return;
0139         }
0140     } else if (!containsWildcardCharacter(pattern.cbegin(), pattern.cend())) {
0141         m_literalPatterns.emplace_back(pattern, language);
0142         return;
0143     }
0144 
0145     QRegularExpression regularExpression(QRegularExpression::wildcardToRegularExpression(pattern),
0146                                          QRegularExpression::CaseInsensitiveOption);
0147     m_regularExpressions.emplace_back(std::move(regularExpression), language);
0148 }
0149 
0150 } // namespace
0151 
0152 namespace KDevelop {
0153 
0154 
0155 using LanguageHash = QHash<QString, ILanguageSupport*>;
0156 using LanguageCache = QHash<QString, QList<ILanguageSupport*>>;
0157 
0158 class LanguageControllerPrivate
0159 {
0160 public:
0161     explicit LanguageControllerPrivate(LanguageController *controller)
0162         : backgroundParser(new BackgroundParser(controller))
0163         , staticAssistantsManager(nullptr)
0164         , m_cleanedUp(false)
0165         , problemModelSet(new ProblemModelSet(controller))
0166     {}
0167 
0168     mutable QRecursiveMutex dataMutex;
0169 
0170     LanguageHash languages; //Maps language-names to languages
0171     LanguageCache languageCache; //Maps mimetype-names to languages
0172     MimeTypeCache mimeTypeCache; //Maps mimetype-glob-patterns to languages
0173 
0174     BackgroundParser* const backgroundParser;
0175     StaticAssistantsManager* staticAssistantsManager;
0176     bool m_cleanedUp;
0177 
0178     void addLanguageSupport(ILanguageSupport* support, const QStringList& mimetypes);
0179     void addLanguageSupport(ILanguageSupport* support);
0180 
0181     ProblemModelSet* const problemModelSet;
0182 };
0183 
0184 void LanguageControllerPrivate::addLanguageSupport(ILanguageSupport* languageSupport,
0185                                                             const QStringList& mimetypes)
0186 {
0187     Q_ASSERT(!languages.contains(languageSupport->name()));
0188     languages.insert(languageSupport->name(), languageSupport);
0189 
0190     for (const QString& mimeTypeName : mimetypes) {
0191         qCDebug(SHELL) << "adding supported mimetype:" << mimeTypeName << "language:" << languageSupport->name();
0192         languageCache[mimeTypeName] << languageSupport;
0193         mimeTypeCache.addMimeType(mimeTypeName, languageSupport);
0194     }
0195 }
0196 
0197 void LanguageControllerPrivate::addLanguageSupport(KDevelop::ILanguageSupport* languageSupport)
0198 {
0199     if (languages.contains(languageSupport->name()))
0200         return;
0201 
0202     Q_ASSERT(dynamic_cast<IPlugin*>(languageSupport));
0203 
0204     KPluginMetaData info = Core::self()->pluginController()->pluginInfo(dynamic_cast<IPlugin*>(languageSupport));
0205     const auto mimetypes = info.value(KEY_SupportedMimeTypes(), QStringList());
0206     addLanguageSupport(languageSupport, mimetypes);
0207 }
0208 
0209 LanguageController::LanguageController(QObject *parent)
0210     : ILanguageController(parent)
0211     , d_ptr(new LanguageControllerPrivate(this))
0212 {
0213     setObjectName(QStringLiteral("LanguageController"));
0214 }
0215 
0216 LanguageController::~LanguageController() = default;
0217 
0218 void LanguageController::initialize()
0219 {
0220     Q_D(LanguageController);
0221 
0222     d->languages = {};
0223     d->languageCache = {};
0224     d->mimeTypeCache = {};
0225 
0226     d->backgroundParser->loadSettings();
0227 
0228     delete d->staticAssistantsManager;
0229     d->staticAssistantsManager = new StaticAssistantsManager(this);
0230 
0231     d->m_cleanedUp = false;
0232 
0233     // make sure the DUChain is setup before we try to access it from different threads at the same time
0234     DUChain::self();
0235 }
0236 
0237 void LanguageController::cleanup()
0238 {
0239     Q_D(LanguageController);
0240 
0241     QMutexLocker lock(&d->dataMutex);
0242     d->m_cleanedUp = true;
0243 }
0244 
0245 StaticAssistantsManager* LanguageController::staticAssistantsManager() const
0246 {
0247     Q_D(const LanguageController);
0248 
0249     return d->staticAssistantsManager;
0250 }
0251 
0252 ICompletionSettings *LanguageController::completionSettings() const
0253 {
0254     return &CompletionSettings::self();
0255 }
0256 
0257 ProblemModelSet* LanguageController::problemModelSet() const
0258 {
0259     Q_D(const LanguageController);
0260 
0261     return d->problemModelSet;
0262 }
0263 
0264 QList<ILanguageSupport*> LanguageController::loadedLanguages() const
0265 {
0266     Q_D(const LanguageController);
0267 
0268     QMutexLocker lock(&d->dataMutex);
0269     QList<ILanguageSupport*> ret;
0270 
0271     if(d->m_cleanedUp)
0272         return ret;
0273 
0274     ret.reserve(d->languages.size());
0275     for (ILanguageSupport* lang : qAsConst(d->languages)) {
0276         ret << lang;
0277     }
0278     return ret;
0279 }
0280 
0281 ILanguageSupport* LanguageController::language(const QString &name) const
0282 {
0283     Q_D(const LanguageController);
0284 
0285     QMutexLocker lock(&d->dataMutex);
0286 
0287     if(d->m_cleanedUp)
0288         return nullptr;
0289 
0290     const auto languageIt = d->languages.constFind(name);
0291     if (languageIt != d->languages.constEnd())
0292         return *languageIt;
0293 
0294     // temporary support for deprecated-in-5.1 "X-KDevelop-Language" as fallback
0295     // remove in later version
0296     const QString keys[2] = {
0297         QStringLiteral("X-KDevelop-Languages"),
0298         QStringLiteral("X-KDevelop-Language")
0299     };
0300     QList<IPlugin*> supports;
0301     for (const auto& key : keys) {
0302         QVariantMap constraints;
0303         constraints.insert(key, name);
0304         supports = Core::self()->pluginController()->allPluginsForExtension(KEY_ILanguageSupport(), constraints);
0305         if (key == keys[1]) {
0306             for (auto support : qAsConst(supports)) {
0307                 qCWarning(SHELL) << "Plugin" << Core::self()->pluginController()->pluginInfo(support).name() << " has deprecated (since 5.1) metadata key \"X-KDevelop-Language\", needs porting to: \"X-KDevelop-Languages\": ["<<name<<"]'";
0308             }
0309         }
0310         if (!supports.isEmpty()) {
0311             break;
0312         }
0313     }
0314 
0315     if(!supports.isEmpty()) {
0316         auto *languageSupport = supports[0]->extension<ILanguageSupport>();
0317         if(languageSupport) {
0318             const_cast<LanguageControllerPrivate*>(d)->addLanguageSupport(languageSupport);
0319             return languageSupport;
0320         }
0321     }
0322     return nullptr;
0323 }
0324 
0325 QList<ILanguageSupport*> LanguageController::languagesForUrl(const QUrl &url)
0326 {
0327     if (url.isEmpty()) {
0328         // The URL of an unsaved document is empty.
0329         // The code below cannot find a language for an empty URL.
0330         return {};
0331     }
0332 
0333     Q_D(LanguageController);
0334 
0335     QMutexLocker lock(&d->dataMutex);
0336 
0337     if(d->m_cleanedUp)
0338         return {};
0339 
0340     ///non-crashy part: Use the mime-types of known languages
0341     auto languages = d->mimeTypeCache.languagesForFileName(url.fileName());
0342 
0343     //Never use findByUrl from within a background thread, and never load a language support
0344     //from within the backgruond thread. Both is unsafe, and can lead to crashes
0345     if(!languages.isEmpty() || QThread::currentThread() != thread())
0346         return languages;
0347 
0348     QMimeType mimeType;
0349 
0350     if (url.isLocalFile()) {
0351         mimeType = QMimeDatabase().mimeTypeForFile(url.toLocalFile());
0352     } else {
0353         // remote file, only look at the extension
0354         mimeType = QMimeDatabase().mimeTypeForUrl(url);
0355     }
0356     if (mimeType.isDefault()) {
0357         // ask the document controller about a more concrete mimetype
0358         IDocument* doc = ICore::self()->documentController()->documentForUrl(url);
0359         if (doc) {
0360             mimeType = doc->mimeType();
0361         }
0362     }
0363 
0364     languages = languagesForMimetype(mimeType.name());
0365 
0366     return languages;
0367 }
0368 
0369 QList<ILanguageSupport*> LanguageController::languagesForMimetype(const QString& mimetype)
0370 {
0371     Q_D(LanguageController);
0372 
0373     QMutexLocker lock(&d->dataMutex);
0374 
0375     QList<ILanguageSupport*> languages;
0376     LanguageCache::ConstIterator it = d->languageCache.constFind(mimetype);
0377     if (it != d->languageCache.constEnd()) {
0378         languages = it.value();
0379     } else {
0380         QVariantMap constraints;
0381         constraints.insert(KEY_SupportedMimeTypes(), mimetype);
0382         const QList<IPlugin*> supports = Core::self()->pluginController()->allPluginsForExtension(KEY_ILanguageSupport(), constraints);
0383 
0384         if (supports.isEmpty()) {
0385             qCDebug(SHELL) << "no languages for mimetype:" << mimetype;
0386             d->languageCache.insert(mimetype, QList<ILanguageSupport*>());
0387         } else {
0388             for (IPlugin *support : supports) {
0389                 auto* languageSupport = support->extension<ILanguageSupport>();
0390                 qCDebug(SHELL) << "language-support:" << languageSupport;
0391                 if(languageSupport) {
0392                     d->addLanguageSupport(languageSupport);
0393                     languages << languageSupport;
0394                 }
0395             }
0396         }
0397     }
0398     return languages;
0399 }
0400 
0401 QList<QString> LanguageController::mimetypesForLanguageName(const QString& languageName)
0402 {
0403     Q_D(LanguageController);
0404 
0405     QMutexLocker lock(&d->dataMutex);
0406 
0407     QList<QString> mimetypes;
0408     for (LanguageCache::ConstIterator iter = d->languageCache.constBegin(); iter != d->languageCache.constEnd(); ++iter) {
0409         bool isFromLanguage = std::any_of(iter.value().begin(), iter.value().end(), [&] (ILanguageSupport* language ) {
0410             return (language->name() == languageName);
0411         });
0412         if (isFromLanguage) {
0413             mimetypes << iter.key();
0414         }
0415     }
0416     return mimetypes;
0417 }
0418 
0419 BackgroundParser *LanguageController::backgroundParser() const
0420 {
0421     Q_D(const LanguageController);
0422 
0423     return d->backgroundParser;
0424 }
0425 
0426 void LanguageController::addLanguageSupport(ILanguageSupport* languageSupport, const QStringList& mimetypes)
0427 {
0428     Q_D(LanguageController);
0429 
0430     d->addLanguageSupport(languageSupport, mimetypes);
0431 }
0432 
0433 }
0434 
0435 #include "moc_languagecontroller.cpp"