1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "qmljsbind.h"
27 #include "qmljsconstants.h"
28 #include "qmljsfindexportedcpptypes.h"
29 #include "qmljsinterpreter.h"
30 #include "qmljsmodelmanagerinterface.h"
31 #include "qmljsplugindumper.h"
32 #include "qmljstypedescriptionreader.h"
33 #include "qmljsdialect.h"
34 #include "qmljsviewercontext.h"
35 #include "qmljsutils.h"
36 
37 #include <cplusplus/cppmodelmanagerbase.h>
38 #include <utils/algorithm.h>
39 #include <utils/hostosinfo.h>
40 #include <utils/runextensions.h>
41 #include <utils/stringutils.h>
42 
43 #ifdef WITH_TESTS
44 #include <extensionsystem/pluginmanager.h>
45 #endif
46 
47 #include <QDir>
48 #include <QDirIterator>
49 #include <QFile>
50 #include <QFileInfo>
51 #include <QMetaObject>
52 #include <QTextDocument>
53 #include <QTextStream>
54 #include <QTimer>
55 #include <QtAlgorithms>
56 #include <QLibraryInfo>
57 
58 using namespace Utils;
59 
60 namespace QmlJS {
61 
62 QMLJS_EXPORT Q_LOGGING_CATEGORY(qmljsLog, "qtc.qmljs.common", QtWarningMsg)
63 
64 /*!
65     \class QmlJS::ModelManagerInterface
66     \brief The ModelManagerInterface class acts as an interface to the
67     global state of the QmlJS code model.
68     \sa QmlJS::Document QmlJS::Snapshot QmlJSTools::Internal::ModelManager
69 
70     The ModelManagerInterface is an interface for global state and actions in
71     the QmlJS code model. It is implemented by \l{QmlJSTools::Internal::ModelManager}
72     and the instance can be accessed through ModelManagerInterface::instance().
73 
74     One of its primary concerns is to keep the Snapshots it
75     maintains up to date by parsing documents and finding QML modules.
76 
77     It has a Snapshot that contains only valid Documents,
78     accessible through ModelManagerInterface::snapshot() and a Snapshot with
79     potentially more recent, but invalid documents that is exposed through
80     ModelManagerInterface::newestSnapshot().
81 */
82 
83 static ModelManagerInterface *g_instance = nullptr;
84 static QMutex g_instanceMutex;
85 static const char *qtQuickUISuffix = "ui.qml";
86 
maybeAddPath(ViewerContext & context,const QString & path)87 static void maybeAddPath(ViewerContext &context, const QString &path)
88 {
89     if (!path.isEmpty() && !context.paths.contains(path))
90         context.paths.append(path);
91 }
92 
environmentImportPaths()93 static QStringList environmentImportPaths()
94 {
95     QStringList paths;
96 
97     const QStringList importPaths = QString::fromLocal8Bit(qgetenv("QML_IMPORT_PATH")).split(
98         Utils::HostOsInfo::pathListSeparator(), Qt::SkipEmptyParts);
99 
100     for (const QString &path : importPaths) {
101         const QString canonicalPath = QDir(path).canonicalPath();
102         if (!canonicalPath.isEmpty() && !paths.contains(canonicalPath))
103             paths.append(canonicalPath);
104     }
105 
106     return paths;
107 }
108 
ModelManagerInterface(QObject * parent)109 ModelManagerInterface::ModelManagerInterface(QObject *parent)
110     : QObject(parent),
111       m_defaultImportPaths(environmentImportPaths()),
112       m_pluginDumper(new PluginDumper(this))
113 {
114     m_futureSynchronizer.setCancelOnWait(false);
115     m_indexerDisabled = qEnvironmentVariableIsSet("QTC_NO_CODE_INDEXER");
116 
117     m_updateCppQmlTypesTimer = new QTimer(this);
118     const int second = 1000;
119     m_updateCppQmlTypesTimer->setInterval(second);
120     m_updateCppQmlTypesTimer->setSingleShot(true);
121     connect(m_updateCppQmlTypesTimer, &QTimer::timeout,
122             this, &ModelManagerInterface::startCppQmlTypeUpdate);
123 
124     m_asyncResetTimer = new QTimer(this);
125     const int fifteenSeconds = 15000;
126     m_asyncResetTimer->setInterval(fifteenSeconds);
127     m_asyncResetTimer->setSingleShot(true);
128     connect(m_asyncResetTimer, &QTimer::timeout, this, &ModelManagerInterface::resetCodeModel);
129 
130     qRegisterMetaType<QmlJS::Document::Ptr>("QmlJS::Document::Ptr");
131     qRegisterMetaType<QmlJS::LibraryInfo>("QmlJS::LibraryInfo");
132     qRegisterMetaType<QmlJS::Dialect>("QmlJS::Dialect");
133     qRegisterMetaType<QmlJS::PathAndLanguage>("QmlJS::PathAndLanguage");
134     qRegisterMetaType<QmlJS::PathsAndLanguages>("QmlJS::PathsAndLanguages");
135 
136     m_defaultProjectInfo.qtQmlPath = QFileInfo(
137                 QLibraryInfo::location(QLibraryInfo::Qml2ImportsPath)).canonicalFilePath();
138     m_defaultProjectInfo.qtVersionString = QLibraryInfo::version().toString();
139 
140     updateImportPaths();
141 
142     QMutexLocker locker(&g_instanceMutex);
143     Q_ASSERT(! g_instance);
144     g_instance = this;
145 }
146 
~ModelManagerInterface()147 ModelManagerInterface::~ModelManagerInterface()
148 {
149     Q_ASSERT(g_instance == this);
150     m_cppQmlTypesUpdater.cancel();
151     m_cppQmlTypesUpdater.waitForFinished();
152 
153     while (true) {
154         joinAllThreads(true);
155         // Keep these 2 mutexes in the same order as inside instanceForFuture()
156         QMutexLocker instanceLocker(&g_instanceMutex);
157         QMutexLocker futureLocker(&m_futuresMutex);
158         if (m_futureSynchronizer.isEmpty()) {
159             g_instance = nullptr;
160             return;
161         }
162     }
163 }
164 
defaultLanguageMapping()165 static QHash<QString, Dialect> defaultLanguageMapping()
166 {
167     static QHash<QString, Dialect> res{
168         {QLatin1String("js"), Dialect::JavaScript},
169         {QLatin1String("qml"), Dialect::Qml},
170         {QLatin1String("qmltypes"), Dialect::QmlTypeInfo},
171         {QLatin1String("qmlproject"), Dialect::QmlProject},
172         {QLatin1String("json"), Dialect::Json},
173         {QLatin1String("qbs"), Dialect::QmlQbs},
174         {QLatin1String(qtQuickUISuffix), Dialect::QmlQtQuick2Ui}
175     };
176     return res;
177 }
178 
guessLanguageOfFile(const QString & fileName)179 Dialect ModelManagerInterface::guessLanguageOfFile(const QString &fileName)
180 {
181     QHash<QString, Dialect> lMapping;
182     if (instance())
183         lMapping = instance()->languageForSuffix();
184     else
185         lMapping = defaultLanguageMapping();
186     const QFileInfo info(fileName);
187     QString fileSuffix = info.suffix();
188 
189     /*
190      * I was reluctant to use complete suffix in all cases, because it is a huge
191      * change in behaivour. But in case of .qml this should be safe.
192      */
193 
194     if (fileSuffix == QLatin1String("qml"))
195         fileSuffix = info.completeSuffix();
196 
197     return lMapping.value(fileSuffix, Dialect::NoLanguage);
198 }
199 
globPatternsForLanguages(const QList<Dialect> & languages)200 QStringList ModelManagerInterface::globPatternsForLanguages(const QList<Dialect> &languages)
201 {
202     QStringList patterns;
203     const QHash<QString, Dialect> lMapping =
204             instance() ? instance()->languageForSuffix() : defaultLanguageMapping();
205     for (auto i = lMapping.cbegin(), end = lMapping.cend(); i != end; ++i) {
206         if (languages.contains(i.value()))
207             patterns << QLatin1String("*.") + i.key();
208     }
209     return patterns;
210 }
211 
instance()212 ModelManagerInterface *ModelManagerInterface::instance()
213 {
214     return g_instance;
215 }
216 
217 // If the returned instance is not null, it's guaranteed that it will be valid at least as long
218 // as the passed QFuture object isn't finished.
instanceForFuture(const QFuture<void> & future)219 ModelManagerInterface *ModelManagerInterface::instanceForFuture(const QFuture<void> &future)
220 {
221     QMutexLocker locker(&g_instanceMutex);
222     if (g_instance)
223         g_instance->addFuture(future);
224     return g_instance;
225 }
writeWarning(const QString & msg)226 void ModelManagerInterface::writeWarning(const QString &msg)
227 {
228     if (ModelManagerInterface *i = instance())
229         i->writeMessageInternal(msg);
230     else
231         qCWarning(qmljsLog) << msg;
232 }
233 
workingCopy()234 ModelManagerInterface::WorkingCopy ModelManagerInterface::workingCopy()
235 {
236     if (ModelManagerInterface *i = instance())
237         return i->workingCopyInternal();
238     return WorkingCopy();
239 }
240 
activateScan()241 void ModelManagerInterface::activateScan()
242 {
243     if (!m_shouldScanImports) {
244         m_shouldScanImports = true;
245         updateImportPaths();
246     }
247 }
248 
languageForSuffix() const249 QHash<QString, Dialect> ModelManagerInterface::languageForSuffix() const
250 {
251     return defaultLanguageMapping();
252 }
253 
writeMessageInternal(const QString & msg) const254 void ModelManagerInterface::writeMessageInternal(const QString &msg) const
255 {
256     qCDebug(qmljsLog) << msg;
257 }
258 
workingCopyInternal() const259 ModelManagerInterface::WorkingCopy ModelManagerInterface::workingCopyInternal() const
260 {
261     ModelManagerInterface::WorkingCopy res;
262     return res;
263 }
264 
addTaskInternal(const QFuture<void> & result,const QString & msg,const char * taskId) const265 void ModelManagerInterface::addTaskInternal(const QFuture<void> &result, const QString &msg,
266                                             const char *taskId) const
267 {
268     Q_UNUSED(result)
269     qCDebug(qmljsLog) << "started " << taskId << " " << msg;
270 }
271 
loadQmlTypeDescriptionsInternal(const QString & resourcePath)272 void ModelManagerInterface::loadQmlTypeDescriptionsInternal(const QString &resourcePath)
273 {
274     const QDir typeFileDir(resourcePath + QLatin1String("/qml-type-descriptions"));
275     const QStringList qmlTypesExtensions = QStringList("*.qmltypes");
276     QFileInfoList qmlTypesFiles = typeFileDir.entryInfoList(
277                 qmlTypesExtensions,
278                 QDir::Files,
279                 QDir::Name);
280 
281     QStringList errors;
282     QStringList warnings;
283 
284     // filter out the actual Qt builtins
285     for (int i = 0; i < qmlTypesFiles.size(); ++i) {
286         if (qmlTypesFiles.at(i).baseName() == QLatin1String("builtins")) {
287             QFileInfoList list;
288             list.append(qmlTypesFiles.at(i));
289             CppQmlTypesLoader::defaultQtObjects =
290                     CppQmlTypesLoader::loadQmlTypes(list, &errors, &warnings);
291             qmlTypesFiles.removeAt(i);
292             break;
293         }
294     }
295 
296     // load the fallbacks for libraries
297     const CppQmlTypesLoader::BuiltinObjects objs =
298             CppQmlTypesLoader::loadQmlTypes(qmlTypesFiles, &errors, &warnings);
299     for (auto it = objs.cbegin(); it != objs.cend(); ++it)
300         CppQmlTypesLoader::defaultLibraryObjects.insert(it.key(), it.value());
301 
302     for (const QString &error : qAsConst(errors))
303         writeMessageInternal(error);
304     for (const QString &warning : qAsConst(warnings))
305         writeMessageInternal(warning);
306 }
307 
setDefaultProject(const ModelManagerInterface::ProjectInfo & pInfo,ProjectExplorer::Project * p)308 void ModelManagerInterface::setDefaultProject(const ModelManagerInterface::ProjectInfo &pInfo,
309                                               ProjectExplorer::Project *p)
310 {
311     QMutexLocker locker(&m_mutex);
312     m_defaultProject = p;
313     m_defaultProjectInfo = pInfo;
314 }
315 
snapshot() const316 Snapshot ModelManagerInterface::snapshot() const
317 {
318     QMutexLocker locker(&m_mutex);
319     return m_validSnapshot;
320 }
321 
newestSnapshot() const322 Snapshot ModelManagerInterface::newestSnapshot() const
323 {
324     QMutexLocker locker(&m_mutex);
325     return m_newestSnapshot;
326 }
327 
updateSourceFiles(const QStringList & files,bool emitDocumentOnDiskChanged)328 void ModelManagerInterface::updateSourceFiles(const QStringList &files,
329                                               bool emitDocumentOnDiskChanged)
330 {
331     if (m_indexerDisabled)
332         return;
333     refreshSourceFiles(files, emitDocumentOnDiskChanged);
334 }
335 
refreshSourceFiles(const QStringList & sourceFiles,bool emitDocumentOnDiskChanged)336 QFuture<void> ModelManagerInterface::refreshSourceFiles(const QStringList &sourceFiles,
337                                                         bool emitDocumentOnDiskChanged)
338 {
339     if (sourceFiles.isEmpty())
340         return QFuture<void>();
341 
342     QFuture<void> result = Utils::runAsync(&ModelManagerInterface::parse,
343                                            workingCopyInternal(), sourceFiles,
344                                            this, Dialect(Dialect::Qml),
345                                            emitDocumentOnDiskChanged);
346     addFuture(result);
347 
348     if (sourceFiles.count() > 1)
349          addTaskInternal(result, tr("Parsing QML Files"), Constants::TASK_INDEX);
350 
351     if (sourceFiles.count() > 1 && !m_shouldScanImports) {
352         bool scan = false;
353         {
354             QMutexLocker l(&m_mutex);
355             if (!m_shouldScanImports) {
356                 m_shouldScanImports = true;
357                 scan = true;
358             }
359         }
360         if (scan)
361         updateImportPaths();
362     }
363 
364     return result;
365 }
366 
367 
fileChangedOnDisk(const QString & path)368 void ModelManagerInterface::fileChangedOnDisk(const QString &path)
369 {
370     addFuture(Utils::runAsync(&ModelManagerInterface::parse,
371                     workingCopyInternal(), QStringList(path),
372                     this, Dialect(Dialect::AnyLanguage), true));
373 }
374 
removeFiles(const QStringList & files)375 void ModelManagerInterface::removeFiles(const QStringList &files)
376 {
377     emit aboutToRemoveFiles(files);
378 
379     QMutexLocker locker(&m_mutex);
380 
381     for (const QString &file : files) {
382         m_validSnapshot.remove(file);
383         m_newestSnapshot.remove(file);
384     }
385 }
386 
387 namespace {
pInfoLessThanActive(const ModelManagerInterface::ProjectInfo & p1,const ModelManagerInterface::ProjectInfo & p2)388 bool pInfoLessThanActive(const ModelManagerInterface::ProjectInfo &p1,
389                          const ModelManagerInterface::ProjectInfo &p2)
390 {
391     QStringList s1 = p1.activeResourceFiles;
392     QStringList s2 = p2.activeResourceFiles;
393     if (s1.size() < s2.size())
394         return true;
395     if (s1.size() > s2.size())
396         return false;
397     for (int i = 0; i < s1.size(); ++i) {
398         if (s1.at(i) < s2.at(i))
399             return true;
400         if (s1.at(i) > s2.at(i))
401             return false;
402     }
403     return false;
404 }
405 
pInfoLessThanAll(const ModelManagerInterface::ProjectInfo & p1,const ModelManagerInterface::ProjectInfo & p2)406 bool pInfoLessThanAll(const ModelManagerInterface::ProjectInfo &p1,
407                       const ModelManagerInterface::ProjectInfo &p2)
408 {
409     QStringList s1 = p1.allResourceFiles;
410     QStringList s2 = p2.allResourceFiles;
411     if (s1.size() < s2.size())
412         return true;
413     if (s1.size() > s2.size())
414         return false;
415     for (int i = 0; i < s1.size(); ++i) {
416         if (s1.at(i) < s2.at(i))
417             return true;
418         if (s1.at(i) > s2.at(i))
419             return false;
420     }
421     return false;
422 }
423 
pInfoLessThanImports(const ModelManagerInterface::ProjectInfo & p1,const ModelManagerInterface::ProjectInfo & p2)424 bool pInfoLessThanImports(const ModelManagerInterface::ProjectInfo &p1,
425                           const ModelManagerInterface::ProjectInfo &p2)
426 {
427     if (p1.qtQmlPath < p2.qtQmlPath)
428         return true;
429     if (p1.qtQmlPath > p2.qtQmlPath)
430         return false;
431     const PathsAndLanguages &s1 = p1.importPaths;
432     const PathsAndLanguages &s2 = p2.importPaths;
433     if (s1.size() < s2.size())
434         return true;
435     if (s1.size() > s2.size())
436         return false;
437     for (int i = 0; i < s1.size(); ++i) {
438         if (s1.at(i) < s2.at(i))
439             return true;
440         if (s2.at(i) < s1.at(i))
441             return false;
442     }
443     return false;
444 }
445 
446 }
447 
iterateQrcFiles(ProjectExplorer::Project * project,QrcResourceSelector resources,const std::function<void (QrcParser::ConstPtr)> & callback)448 void ModelManagerInterface::iterateQrcFiles(
449         ProjectExplorer::Project *project, QrcResourceSelector resources,
450         const std::function<void(QrcParser::ConstPtr)> &callback)
451 {
452     QList<ProjectInfo> pInfos;
453     if (project) {
454         pInfos.append(projectInfo(project));
455     } else {
456         pInfos = projectInfos();
457         if (resources == ActiveQrcResources) // make the result predictable
458             Utils::sort(pInfos, &pInfoLessThanActive);
459         else
460             Utils::sort(pInfos, &pInfoLessThanAll);
461     }
462 
463     QSet<QString> pathsChecked;
464     for (const ModelManagerInterface::ProjectInfo &pInfo : qAsConst(pInfos)) {
465         QStringList qrcFilePaths;
466         if (resources == ActiveQrcResources)
467             qrcFilePaths = pInfo.activeResourceFiles;
468         else
469             qrcFilePaths = pInfo.allResourceFiles;
470         for (const QString &qrcFilePath : qAsConst(qrcFilePaths)) {
471             if (pathsChecked.contains(qrcFilePath))
472                 continue;
473             pathsChecked.insert(qrcFilePath);
474             QrcParser::ConstPtr qrcFile = m_qrcCache.parsedPath(qrcFilePath);
475             if (qrcFile.isNull())
476                 continue;
477             callback(qrcFile);
478         }
479     }
480 }
481 
qrcPathsForFile(const QString & file,const QLocale * locale,ProjectExplorer::Project * project,QrcResourceSelector resources)482 QStringList ModelManagerInterface::qrcPathsForFile(const QString &file, const QLocale *locale,
483                                                    ProjectExplorer::Project *project,
484                                                    QrcResourceSelector resources)
485 {
486     QStringList res;
487     iterateQrcFiles(project, resources, [&](const QrcParser::ConstPtr &qrcFile) {
488         qrcFile->collectResourceFilesForSourceFile(file, &res, locale);
489     });
490     return res;
491 }
492 
filesAtQrcPath(const QString & path,const QLocale * locale,ProjectExplorer::Project * project,QrcResourceSelector resources)493 QStringList ModelManagerInterface::filesAtQrcPath(const QString &path, const QLocale *locale,
494                                          ProjectExplorer::Project *project,
495                                          QrcResourceSelector resources)
496 {
497     QString normPath = QrcParser::normalizedQrcFilePath(path);
498     QStringList res;
499     iterateQrcFiles(project, resources, [&](const QrcParser::ConstPtr &qrcFile) {
500         qrcFile->collectFilesAtPath(normPath, &res, locale);
501     });
502     return res;
503 }
504 
filesInQrcPath(const QString & path,const QLocale * locale,ProjectExplorer::Project * project,bool addDirs,QrcResourceSelector resources)505 QMap<QString, QStringList> ModelManagerInterface::filesInQrcPath(const QString &path,
506                                                         const QLocale *locale,
507                                                         ProjectExplorer::Project *project,
508                                                         bool addDirs,
509                                                         QrcResourceSelector resources)
510 {
511     QString normPath = QrcParser::normalizedQrcDirectoryPath(path);
512     QMap<QString, QStringList> res;
513     iterateQrcFiles(project, resources, [&](const QrcParser::ConstPtr &qrcFile) {
514         qrcFile->collectFilesInPath(normPath, &res, addDirs, locale);
515     });
516     return res;
517 }
518 
projectInfos() const519 QList<ModelManagerInterface::ProjectInfo> ModelManagerInterface::projectInfos() const
520 {
521     QMutexLocker locker(&m_mutex);
522 
523     return m_projects.values();
524 }
525 
containsProject(ProjectExplorer::Project * project) const526 bool ModelManagerInterface::containsProject(ProjectExplorer::Project *project) const
527 {
528     QMutexLocker locker(&m_mutex);
529     return m_projects.contains(project);
530 }
531 
projectInfo(ProjectExplorer::Project * project) const532 ModelManagerInterface::ProjectInfo ModelManagerInterface::projectInfo(
533         ProjectExplorer::Project *project) const
534 {
535     QMutexLocker locker(&m_mutex);
536     return m_projects.value(project);
537 }
538 
updateProjectInfo(const ProjectInfo & pinfo,ProjectExplorer::Project * p)539 void ModelManagerInterface::updateProjectInfo(const ProjectInfo &pinfo, ProjectExplorer::Project *p)
540 {
541     if (pinfo.project.isNull() || !p || m_indexerDisabled)
542         return;
543 
544     Snapshot snapshot;
545     ProjectInfo oldInfo;
546     {
547         QMutexLocker locker(&m_mutex);
548         oldInfo = m_projects.value(p);
549         m_projects.insert(p, pinfo);
550         if (p == m_defaultProject)
551             m_defaultProjectInfo = pinfo;
552         snapshot = m_validSnapshot;
553     }
554 
555     if (oldInfo.qmlDumpPath != pinfo.qmlDumpPath
556             || oldInfo.qmlDumpEnvironment != pinfo.qmlDumpEnvironment) {
557         m_pluginDumper->scheduleRedumpPlugins();
558     }
559 
560 
561     updateImportPaths();
562 
563     // remove files that are no longer in the project and have been deleted
564     QStringList deletedFiles;
565     for (const QString &oldFile : qAsConst(oldInfo.sourceFiles)) {
566         if (snapshot.document(oldFile)
567                 && !pinfo.sourceFiles.contains(oldFile)
568                 && !QFile::exists(oldFile)) {
569             deletedFiles += oldFile;
570         }
571     }
572     removeFiles(deletedFiles);
573     for (const QString &oldFile : qAsConst(deletedFiles))
574         m_fileToProject.remove(oldFile, p);
575 
576     // parse any files not yet in the snapshot
577     QStringList newFiles;
578     for (const QString &file : qAsConst(pinfo.sourceFiles)) {
579         if (!m_fileToProject.contains(file, p))
580             m_fileToProject.insert(file, p);
581         if (!snapshot.document(file))
582             newFiles += file;
583     }
584 
585     updateSourceFiles(newFiles, false);
586 
587     // update qrc cache
588     m_qrcContents = pinfo.resourceFileContents;
589     for (const QString &newQrc : qAsConst(pinfo.allResourceFiles))
590         m_qrcCache.addPath(newQrc, m_qrcContents.value(newQrc));
591     for (const QString &oldQrc : qAsConst(oldInfo.allResourceFiles))
592         m_qrcCache.removePath(oldQrc);
593 
594     m_pluginDumper->loadBuiltinTypes(pinfo);
595     emit projectInfoUpdated(pinfo);
596 }
597 
598 
removeProjectInfo(ProjectExplorer::Project * project)599 void ModelManagerInterface::removeProjectInfo(ProjectExplorer::Project *project)
600 {
601     ProjectInfo info;
602     info.sourceFiles.clear();
603     // update with an empty project info to clear data
604     updateProjectInfo(info, project);
605 
606     {
607         QMutexLocker locker(&m_mutex);
608         m_projects.remove(project);
609     }
610 }
611 
612 /*!
613     Returns project info with summarized info for \a path
614 
615     \note Project pointer will be empty
616  */
projectInfoForPath(const QString & path) const617 ModelManagerInterface::ProjectInfo ModelManagerInterface::projectInfoForPath(
618         const QString &path) const
619 {
620     ProjectInfo res;
621     const auto allProjectInfos = allProjectInfosForPath(path);
622     for (const ProjectInfo &pInfo : allProjectInfos) {
623         if (res.qtQmlPath.isEmpty()) {
624             res.qtQmlPath = pInfo.qtQmlPath;
625             res.qtVersionString = pInfo.qtVersionString;
626         }
627         res.applicationDirectories.append(pInfo.applicationDirectories);
628         for (const auto &importPath : pInfo.importPaths)
629             res.importPaths.maybeInsert(importPath);
630         auto end = pInfo.moduleMappings.cend();
631         for (auto it = pInfo.moduleMappings.cbegin(); it != end; ++it)
632             res.moduleMappings.insert(it.key(), it.value());
633     }
634     res.applicationDirectories = Utils::filteredUnique(res.applicationDirectories);
635     return res;
636 }
637 
638 /*!
639     Returns list of project infos for \a path
640  */
allProjectInfosForPath(const QString & path) const641 QList<ModelManagerInterface::ProjectInfo> ModelManagerInterface::allProjectInfosForPath(
642         const QString &path) const
643 {
644     QList<ProjectExplorer::Project *> projects;
645     {
646         QMutexLocker locker(&m_mutex);
647         projects = m_fileToProject.values(path);
648         if (projects.isEmpty()) {
649             QFileInfo fInfo(path);
650             projects = m_fileToProject.values(fInfo.canonicalFilePath());
651         }
652     }
653     QList<ProjectInfo> infos;
654     for (ProjectExplorer::Project *project : qAsConst(projects)) {
655         ProjectInfo info = projectInfo(project);
656         if (!info.project.isNull())
657             infos.append(info);
658     }
659     if (infos.isEmpty()) {
660         QMutexLocker locker(&m_mutex);
661         return { m_defaultProjectInfo };
662     }
663     std::sort(infos.begin(), infos.end(), &pInfoLessThanImports);
664     return infos;
665 }
666 
emitDocumentChangedOnDisk(Document::Ptr doc)667 void ModelManagerInterface::emitDocumentChangedOnDisk(Document::Ptr doc)
668 {
669     emit documentChangedOnDisk(std::move(doc));
670 }
671 
updateQrcFile(const QString & path)672 void ModelManagerInterface::updateQrcFile(const QString &path)
673 {
674     m_qrcCache.updatePath(path, m_qrcContents.value(path));
675 }
676 
updateDocument(const Document::Ptr & doc)677 void ModelManagerInterface::updateDocument(const Document::Ptr &doc)
678 {
679     {
680         QMutexLocker locker(&m_mutex);
681         m_validSnapshot.insert(doc);
682         m_newestSnapshot.insert(doc, true);
683     }
684     emit documentUpdated(doc);
685 }
686 
updateLibraryInfo(const QString & path,const LibraryInfo & info)687 void ModelManagerInterface::updateLibraryInfo(const QString &path, const LibraryInfo &info)
688 {
689     if (!info.pluginTypeInfoError().isEmpty())
690         qCDebug(qmljsLog) << "Dumping errors for " << path << ":" << info.pluginTypeInfoError();
691 
692     {
693         QMutexLocker locker(&m_mutex);
694         m_validSnapshot.insertLibraryInfo(path, info);
695         m_newestSnapshot.insertLibraryInfo(path, info);
696     }
697     // only emit if we got new useful information
698     if (info.isValid())
699         emit libraryInfoUpdated(path, info);
700 }
701 
filesInDirectoryForLanguages(const QString & path,const QList<Dialect> & languages)702 static QStringList filesInDirectoryForLanguages(const QString &path,
703                                                 const QList<Dialect> &languages)
704 {
705     const QStringList pattern = ModelManagerInterface::globPatternsForLanguages(languages);
706     QStringList files;
707 
708     const QDir dir(path);
709     const auto entries = dir.entryInfoList(pattern, QDir::Files);
710     for (const QFileInfo &fi : entries)
711         files += fi.absoluteFilePath();
712 
713     return files;
714 }
715 
findNewImplicitImports(const Document::Ptr & doc,const Snapshot & snapshot,QStringList * importedFiles,QSet<QString> * scannedPaths)716 static void findNewImplicitImports(const Document::Ptr &doc, const Snapshot &snapshot,
717                                    QStringList *importedFiles, QSet<QString> *scannedPaths)
718 {
719     // scan files that could be implicitly imported
720     // it's important we also do this for JS files, otherwise the isEmpty check will fail
721     if (snapshot.documentsInDirectory(doc->path()).isEmpty()) {
722         if (!scannedPaths->contains(doc->path())) {
723             *importedFiles += filesInDirectoryForLanguages(doc->path(),
724                                                            doc->language().companionLanguages());
725             scannedPaths->insert(doc->path());
726         }
727     }
728 }
729 
findNewFileImports(const Document::Ptr & doc,const Snapshot & snapshot,QStringList * importedFiles,QSet<QString> * scannedPaths)730 static void findNewFileImports(const Document::Ptr &doc, const Snapshot &snapshot,
731                         QStringList *importedFiles, QSet<QString> *scannedPaths)
732 {
733     // scan files and directories that are explicitly imported
734     const auto imports = doc->bind()->imports();
735     for (const ImportInfo &import : imports) {
736         const QString &importName = import.path();
737         if (import.type() == ImportType::File) {
738             if (! snapshot.document(importName))
739                 *importedFiles += importName;
740         } else if (import.type() == ImportType::Directory) {
741             if (snapshot.documentsInDirectory(importName).isEmpty()) {
742                 if (! scannedPaths->contains(importName)) {
743                     *importedFiles += filesInDirectoryForLanguages(
744                                 importName, doc->language().companionLanguages());
745                     scannedPaths->insert(importName);
746                 }
747             }
748         } else if (import.type() == ImportType::QrcFile) {
749             const QStringList importPaths
750                     = ModelManagerInterface::instance()->filesAtQrcPath(importName);
751             for (const QString &importPath : importPaths) {
752                 if (!snapshot.document(importPath))
753                     *importedFiles += importPath;
754             }
755         } else if (import.type() == ImportType::QrcDirectory) {
756             const QMap<QString, QStringList> files
757                     = ModelManagerInterface::instance()->filesInQrcPath(importName);
758             for (auto qrc = files.cbegin(), end = files.cend(); qrc != end; ++qrc) {
759                 if (ModelManagerInterface::guessLanguageOfFile(qrc.key()).isQmlLikeOrJsLanguage()) {
760                     for (const QString &sourceFile : qrc.value()) {
761                         if (!snapshot.document(sourceFile))
762                             *importedFiles += sourceFile;
763                     }
764                 }
765             }
766         }
767     }
768 }
769 
770 enum class LibraryStatus {
771     Accepted,
772     Rejected,
773     Unknown
774 };
775 
libraryStatus(const QString & path,const Snapshot & snapshot,QSet<QString> * newLibraries)776 static LibraryStatus libraryStatus(const QString &path, const Snapshot &snapshot,
777                                    QSet<QString> *newLibraries)
778 {
779     if (path.isEmpty())
780         return LibraryStatus::Rejected;
781     // if we know there is a library, done
782     const LibraryInfo &existingInfo = snapshot.libraryInfo(path);
783     if (existingInfo.isValid())
784         return LibraryStatus::Accepted;
785     if (newLibraries->contains(path))
786         return LibraryStatus::Accepted;
787     // if we looked at the path before, done
788     return existingInfo.wasScanned()
789             ? LibraryStatus::Rejected
790             : LibraryStatus::Unknown;
791 }
792 
findNewQmlApplicationInPath(const QString & path,const Snapshot & snapshot,ModelManagerInterface * modelManager,QSet<QString> * newLibraries)793 static bool findNewQmlApplicationInPath(const QString &path,
794                                         const Snapshot &snapshot,
795                                         ModelManagerInterface *modelManager,
796                                         QSet<QString> *newLibraries)
797 {
798     switch (libraryStatus(path, snapshot, newLibraries)) {
799     case LibraryStatus::Accepted: return true;
800     case LibraryStatus::Rejected: return false;
801     default: break;
802     }
803 
804     QString qmltypesFile;
805 
806     QDir dir(path);
807     QDirIterator it(path, QStringList { "*.qmltypes" }, QDir::Files);
808 
809     if (!it.hasNext())
810         return false;
811 
812     qmltypesFile = it.next();
813 
814     LibraryInfo libraryInfo = LibraryInfo(qmltypesFile);
815     const QString libraryPath = dir.absolutePath();
816     newLibraries->insert(libraryPath);
817     modelManager->updateLibraryInfo(path, libraryInfo);
818     modelManager->loadPluginTypes(QFileInfo(libraryPath).canonicalFilePath(), libraryPath,
819                                   QString(), QString());
820     return true;
821 }
822 
findNewQmlLibraryInPath(const QString & path,const Snapshot & snapshot,ModelManagerInterface * modelManager,QStringList * importedFiles,QSet<QString> * scannedPaths,QSet<QString> * newLibraries,bool ignoreMissing)823 static bool findNewQmlLibraryInPath(const QString &path,
824                                     const Snapshot &snapshot,
825                                     ModelManagerInterface *modelManager,
826                                     QStringList *importedFiles,
827                                     QSet<QString> *scannedPaths,
828                                     QSet<QString> *newLibraries,
829                                     bool ignoreMissing)
830 {
831     switch (libraryStatus(path, snapshot, newLibraries)) {
832     case LibraryStatus::Accepted: return true;
833     case LibraryStatus::Rejected: return false;
834     default: break;
835     }
836 
837     const QDir dir(path);
838     QFile qmldirFile(dir.filePath(QLatin1String("qmldir")));
839     if (!qmldirFile.exists()) {
840         if (!ignoreMissing) {
841             LibraryInfo libraryInfo(LibraryInfo::NotFound);
842             modelManager->updateLibraryInfo(path, libraryInfo);
843         }
844         return false;
845     }
846 
847     if (Utils::HostOsInfo::isWindowsHost()) {
848         // QTCREATORBUG-3402 - be case sensitive even here?
849     }
850 
851     // found a new library!
852     if (!qmldirFile.open(QFile::ReadOnly))
853         return false;
854     QString qmldirData = QString::fromUtf8(qmldirFile.readAll());
855 
856     QmlDirParser qmldirParser;
857     qmldirParser.parse(qmldirData);
858 
859     const QString libraryPath = QFileInfo(qmldirFile).absolutePath();
860     newLibraries->insert(libraryPath);
861     modelManager->updateLibraryInfo(libraryPath, LibraryInfo(qmldirParser));
862     modelManager->loadPluginTypes(QFileInfo(libraryPath).canonicalFilePath(), libraryPath,
863                 QString(), QString());
864 
865     // scan the qml files in the library
866     const auto components = qmldirParser.components();
867     for (const QmlDirParser::Component &component : components) {
868         if (!component.fileName.isEmpty()) {
869             const QFileInfo componentFileInfo(dir.filePath(component.fileName));
870             const QString path = QDir::cleanPath(componentFileInfo.absolutePath());
871             if (!scannedPaths->contains(path)) {
872                 *importedFiles += filesInDirectoryForLanguages(path, Dialect(Dialect::AnyLanguage)
873                                                                .companionLanguages());
874                 scannedPaths->insert(path);
875             }
876         }
877     }
878 
879     return true;
880 }
881 
modulePath(const ImportInfo & import,const QStringList & paths)882 static QString modulePath(const ImportInfo &import, const QStringList &paths)
883 {
884     if (!import.version().isValid())
885         return QString();
886 
887     const QStringList modPaths = modulePaths(import.name(), import.version().toString(), paths);
888     return modPaths.value(0); // first is best match
889 }
890 
findNewLibraryImports(const Document::Ptr & doc,const Snapshot & snapshot,ModelManagerInterface * modelManager,QStringList * importedFiles,QSet<QString> * scannedPaths,QSet<QString> * newLibraries)891 static void findNewLibraryImports(const Document::Ptr &doc, const Snapshot &snapshot,
892                                   ModelManagerInterface *modelManager, QStringList *importedFiles,
893                                   QSet<QString> *scannedPaths, QSet<QString> *newLibraries)
894 {
895     // scan current dir
896     findNewQmlLibraryInPath(doc->path(), snapshot, modelManager,
897                             importedFiles, scannedPaths, newLibraries, false);
898 
899     // scan dir and lib imports
900     const QStringList importPaths = modelManager->importPathsNames();
901     const auto imports = doc->bind()->imports();
902     for (const ImportInfo &import : imports) {
903         switch (import.type()) {
904         case ImportType::Directory:
905             findNewQmlLibraryInPath(import.path(), snapshot, modelManager,
906                                     importedFiles, scannedPaths, newLibraries, false);
907             break;
908         case ImportType::Library:
909             findNewQmlLibraryInPath(modulePath(import, importPaths), snapshot, modelManager,
910                                     importedFiles, scannedPaths, newLibraries, false);
911             break;
912         default:
913             break;
914         }
915     }
916 }
917 
parseLoop(QSet<QString> & scannedPaths,QSet<QString> & newLibraries,const WorkingCopy & workingCopy,QStringList files,ModelManagerInterface * modelManager,Dialect mainLanguage,bool emitDocChangedOnDisk,const std::function<bool (qreal)> & reportProgress)918 void ModelManagerInterface::parseLoop(QSet<QString> &scannedPaths,
919                                       QSet<QString> &newLibraries,
920                                       const WorkingCopy &workingCopy,
921                                       QStringList files,
922                                       ModelManagerInterface *modelManager,
923                                       Dialect mainLanguage,
924                                       bool emitDocChangedOnDisk,
925                                       const std::function<bool(qreal)> &reportProgress)
926 {
927     for (int i = 0; i < files.size(); ++i) {
928         if (!reportProgress(qreal(i) / files.size()))
929             return;
930 
931         const QString fileName = files.at(i);
932 
933         Dialect language = guessLanguageOfFile(fileName);
934         if (language == Dialect::NoLanguage) {
935             if (fileName.endsWith(QLatin1String(".qrc")))
936                 modelManager->updateQrcFile(fileName);
937             continue;
938         }
939         if (language == Dialect::Qml
940                 && (mainLanguage == Dialect::QmlQtQuick2))
941             language = mainLanguage;
942         if (language == Dialect::Qml && mainLanguage == Dialect::QmlQtQuick2Ui)
943             language = Dialect::QmlQtQuick2;
944         if (language == Dialect::QmlTypeInfo || language == Dialect::QmlProject)
945             continue;
946         QString contents;
947         int documentRevision = 0;
948 
949         if (workingCopy.contains(fileName)) {
950             QPair<QString, int> entry = workingCopy.get(fileName);
951             contents = entry.first;
952             documentRevision = entry.second;
953         } else {
954             QFile inFile(fileName);
955 
956             if (inFile.open(QIODevice::ReadOnly)) {
957                 QTextStream ins(&inFile);
958                 contents = ins.readAll();
959                 inFile.close();
960             } else {
961                 continue;
962             }
963         }
964 
965         Document::MutablePtr doc = Document::create(fileName, language);
966         doc->setEditorRevision(documentRevision);
967         doc->setSource(contents);
968         doc->parse();
969 
970 #ifdef WITH_TESTS
971         if (ExtensionSystem::PluginManager::isScenarioRunning("TestModelManagerInterface")) {
972             ExtensionSystem::PluginManager::waitForScenarioFullyInitialized();
973             if (ExtensionSystem::PluginManager::finishScenario()) {
974                 qDebug() << "Point 1: Shutdown triggered";
975                 QThread::currentThread()->sleep(2);
976                 qDebug() << "Point 3: If Point 2 was already reached, expect a crash now";
977             }
978         }
979 #endif
980         // get list of referenced files not yet in snapshot or in directories already scanned
981         QStringList importedFiles;
982 
983         // update snapshot. requires synchronization, but significantly reduces amount of file
984         // system queries for library imports because queries are cached in libraryInfo
985         {
986             // Make sure the snapshot is destroyed before updateDocument, so that we don't trigger
987             // the copy-on-write mechanism on its internals.
988             const Snapshot snapshot = modelManager->snapshot();
989 
990             findNewImplicitImports(doc, snapshot, &importedFiles, &scannedPaths);
991             findNewFileImports(doc, snapshot, &importedFiles, &scannedPaths);
992             findNewLibraryImports(doc, snapshot, modelManager, &importedFiles, &scannedPaths,
993                                   &newLibraries);
994         }
995 
996         // add new files to parse list
997         for (const QString &file : qAsConst(importedFiles)) {
998             if (!files.contains(file))
999                 files.append(file);
1000         }
1001 
1002         modelManager->updateDocument(doc);
1003         if (emitDocChangedOnDisk)
1004             modelManager->emitDocumentChangedOnDisk(doc);
1005     }
1006 }
1007 
1008 class FutureReporter
1009 {
1010 public:
FutureReporter(QFutureInterface<void> & future,int multiplier,int base)1011     FutureReporter(QFutureInterface<void> &future, int multiplier, int base)
1012         : future(future), multiplier(multiplier), base(base)
1013     {}
1014 
operator ()(qreal val)1015     bool operator()(qreal val)
1016     {
1017         if (future.isCanceled())
1018             return false;
1019         future.setProgressValue(int(base + multiplier * val));
1020         return true;
1021     }
1022 private:
1023     QFutureInterface<void> &future;
1024     int multiplier;
1025     int base;
1026 };
1027 
parse(QFutureInterface<void> & future,const WorkingCopy & workingCopy,QStringList files,ModelManagerInterface * modelManager,Dialect mainLanguage,bool emitDocChangedOnDisk)1028 void ModelManagerInterface::parse(QFutureInterface<void> &future,
1029                                   const WorkingCopy &workingCopy,
1030                                   QStringList files,
1031                                   ModelManagerInterface *modelManager,
1032                                   Dialect mainLanguage,
1033                                   bool emitDocChangedOnDisk)
1034 {
1035     const int progressMax = 100;
1036     FutureReporter reporter(future, progressMax, 0);
1037     future.setProgressRange(0, progressMax);
1038 
1039     // paths we have scanned for files and added to the files list
1040     QSet<QString> scannedPaths;
1041     // libraries we've found while scanning imports
1042     QSet<QString> newLibraries;
1043     parseLoop(scannedPaths, newLibraries, workingCopy, std::move(files), modelManager, mainLanguage,
1044               emitDocChangedOnDisk, reporter);
1045     future.setProgressValue(progressMax);
1046 }
1047 
1048 struct ScanItem {
1049     QString path;
1050     int depth = 0;
1051     Dialect language = Dialect::AnyLanguage;
1052 };
1053 
importScan(QFutureInterface<void> & future,const ModelManagerInterface::WorkingCopy & workingCopy,const PathsAndLanguages & paths,ModelManagerInterface * modelManager,bool emitDocChangedOnDisk,bool libOnly,bool forceRescan)1054 void ModelManagerInterface::importScan(QFutureInterface<void> &future,
1055                                        const ModelManagerInterface::WorkingCopy &workingCopy,
1056                                        const PathsAndLanguages &paths,
1057                                        ModelManagerInterface *modelManager,
1058                                        bool emitDocChangedOnDisk, bool libOnly, bool forceRescan)
1059 {
1060     // paths we have scanned for files and added to the files list
1061     QSet<QString> scannedPaths;
1062     {
1063         QMutexLocker l(&modelManager->m_mutex);
1064         scannedPaths = modelManager->m_scannedPaths;
1065     }
1066     // libraries we've found while scanning imports
1067     QSet<QString> newLibraries;
1068 
1069     QVector<ScanItem> pathsToScan;
1070     pathsToScan.reserve(paths.size());
1071     {
1072         QMutexLocker l(&modelManager->m_mutex);
1073         for (const auto &path : paths) {
1074             QString cPath = QDir::cleanPath(path.path().toString());
1075             if (!forceRescan && modelManager->m_scannedPaths.contains(cPath))
1076                 continue;
1077             pathsToScan.append({cPath, 0, path.language()});
1078             modelManager->m_scannedPaths.insert(cPath);
1079         }
1080     }
1081     const int maxScanDepth = 5;
1082     int progressRange = pathsToScan.size() * (1 << (2 + maxScanDepth));
1083     int totalWork = progressRange;
1084     int workDone = 0;
1085     future.setProgressRange(0, progressRange); // update max length while iterating?
1086     const Snapshot snapshot = modelManager->snapshot();
1087     bool isCanceled = future.isCanceled();
1088     while (!pathsToScan.isEmpty() && !isCanceled) {
1089         ScanItem toScan = pathsToScan.last();
1090         pathsToScan.pop_back();
1091         int pathBudget = (1 << (maxScanDepth + 2 - toScan.depth));
1092         if (forceRescan || !scannedPaths.contains(toScan.path)) {
1093             QStringList importedFiles;
1094             if (forceRescan ||
1095                     (!findNewQmlLibraryInPath(toScan.path, snapshot, modelManager, &importedFiles,
1096                                               &scannedPaths, &newLibraries, true)
1097                     && !libOnly && snapshot.documentsInDirectory(toScan.path).isEmpty())) {
1098                 importedFiles += filesInDirectoryForLanguages(toScan.path,
1099                                                               toScan.language.companionLanguages());
1100             }
1101             workDone += 1;
1102             future.setProgressValue(progressRange * workDone / totalWork);
1103             if (!importedFiles.isEmpty()) {
1104                 FutureReporter reporter(future, progressRange * pathBudget / (4 * totalWork),
1105                                         progressRange * workDone / totalWork);
1106                 parseLoop(scannedPaths, newLibraries, workingCopy, importedFiles, modelManager,
1107                           toScan.language, emitDocChangedOnDisk, reporter); // run in parallel??
1108                 importedFiles.clear();
1109             }
1110             workDone += pathBudget / 4 - 1;
1111             future.setProgressValue(progressRange * workDone / totalWork);
1112         } else {
1113             workDone += pathBudget / 4;
1114         }
1115         // always descend tree, as we might have just scanned with a smaller depth
1116         if (toScan.depth < maxScanDepth) {
1117             QDir dir(toScan.path);
1118             QStringList subDirs(dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot));
1119             workDone += 1;
1120             totalWork += pathBudget / 2 * subDirs.size() - pathBudget * 3 / 4 + 1;
1121             for (const QString &path : qAsConst(subDirs))
1122                 pathsToScan.append({dir.absoluteFilePath(path), toScan.depth + 1, toScan.language});
1123         } else {
1124             workDone += pathBudget * 3 / 4;
1125         }
1126         future.setProgressValue(progressRange * workDone / totalWork);
1127         isCanceled = future.isCanceled();
1128     }
1129     future.setProgressValue(progressRange);
1130     if (isCanceled) {
1131         // assume no work has been done
1132         QMutexLocker l(&modelManager->m_mutex);
1133         for (const auto &path : paths)
1134             modelManager->m_scannedPaths.remove(path.path().toString());
1135     }
1136 }
1137 
importPathsNames() const1138 QStringList ModelManagerInterface::importPathsNames() const
1139 {
1140     QStringList names;
1141     QMutexLocker l(&m_mutex);
1142     names.reserve(m_allImportPaths.size());
1143     for (const PathAndLanguage &x: m_allImportPaths)
1144         names << x.path().toString();
1145     return names;
1146 }
1147 
activeBundles() const1148 QmlLanguageBundles ModelManagerInterface::activeBundles() const
1149 {
1150     QMutexLocker l(&m_mutex);
1151     return m_activeBundles;
1152 }
1153 
extendedBundles() const1154 QmlLanguageBundles ModelManagerInterface::extendedBundles() const
1155 {
1156     QMutexLocker l(&m_mutex);
1157     return m_extendedBundles;
1158 }
1159 
maybeScan(const PathsAndLanguages & importPaths)1160 void ModelManagerInterface::maybeScan(const PathsAndLanguages &importPaths)
1161 {
1162     if (m_indexerDisabled)
1163         return;
1164     PathsAndLanguages pathToScan;
1165     {
1166         QMutexLocker l(&m_mutex);
1167         for (const PathAndLanguage &importPath : importPaths)
1168             if (!m_scannedPaths.contains(importPath.path().toString()))
1169                 pathToScan.maybeInsert(importPath);
1170     }
1171 
1172     if (pathToScan.length() >= 1) {
1173         QFuture<void> result = Utils::runAsync(&ModelManagerInterface::importScan,
1174                                                workingCopyInternal(), pathToScan,
1175                                                this, true, true, false);
1176         addFuture(result);
1177         addTaskInternal(result, tr("Scanning QML Imports"), Constants::TASK_IMPORT_SCAN);
1178     }
1179 }
1180 
updateImportPaths()1181 void ModelManagerInterface::updateImportPaths()
1182 {
1183     if (m_indexerDisabled)
1184         return;
1185     PathsAndLanguages allImportPaths;
1186     QStringList allApplicationDirectories;
1187     QmlLanguageBundles activeBundles;
1188     QmlLanguageBundles extendedBundles;
1189     for (const ProjectInfo &pInfo : qAsConst(m_projects)) {
1190         for (const auto &importPath : pInfo.importPaths) {
1191             const QString canonicalPath = importPath.path().toFileInfo().canonicalFilePath();
1192             if (!canonicalPath.isEmpty()) {
1193                 allImportPaths.maybeInsert(Utils::FilePath::fromString(canonicalPath),
1194                                            importPath.language());
1195             }
1196         }
1197         allApplicationDirectories.append(pInfo.applicationDirectories);
1198     }
1199 
1200     for (const ViewerContext &vContext : qAsConst(m_defaultVContexts)) {
1201         for (const QString &path : vContext.paths)
1202             allImportPaths.maybeInsert(Utils::FilePath::fromString(path), vContext.language);
1203         allApplicationDirectories.append(vContext.applicationDirectories);
1204     }
1205 
1206     for (const ProjectInfo &pInfo : qAsConst(m_projects)) {
1207         activeBundles.mergeLanguageBundles(pInfo.activeBundle);
1208         const auto languages = pInfo.activeBundle.languages();
1209         for (Dialect l : languages) {
1210             const auto paths = pInfo.activeBundle.bundleForLanguage(l).searchPaths().stringList();
1211             for (const QString &path : paths) {
1212                 const QString canonicalPath = QFileInfo(path).canonicalFilePath();
1213                 if (!canonicalPath.isEmpty())
1214                     allImportPaths.maybeInsert(Utils::FilePath::fromString(canonicalPath), l);
1215             }
1216         }
1217     }
1218 
1219     for (const ProjectInfo &pInfo : qAsConst(m_projects)) {
1220         if (!pInfo.qtQmlPath.isEmpty()) {
1221             allImportPaths.maybeInsert(Utils::FilePath::fromString(pInfo.qtQmlPath),
1222                                        Dialect::QmlQtQuick2);
1223         }
1224     }
1225 
1226     {
1227         const QString pathAtt = defaultProjectInfo().qtQmlPath;
1228         if (!pathAtt.isEmpty())
1229             allImportPaths.maybeInsert(Utils::FilePath::fromString(pathAtt), Dialect::QmlQtQuick2);
1230     }
1231 
1232     for (const auto &importPath : defaultProjectInfo().importPaths) {
1233         allImportPaths.maybeInsert(importPath);
1234     }
1235 
1236     for (const QString &path : qAsConst(m_defaultImportPaths))
1237         allImportPaths.maybeInsert(Utils::FilePath::fromString(path), Dialect::Qml);
1238     allImportPaths.compact();
1239     allApplicationDirectories = Utils::filteredUnique(allApplicationDirectories);
1240 
1241     {
1242         QMutexLocker l(&m_mutex);
1243         m_allImportPaths = allImportPaths;
1244         m_activeBundles = activeBundles;
1245         m_extendedBundles = extendedBundles;
1246     }
1247 
1248 
1249     // check if any file in the snapshot imports something new in the new paths
1250     Snapshot snapshot = m_validSnapshot;
1251     QStringList importedFiles;
1252     QSet<QString> scannedPaths;
1253     QSet<QString> newLibraries;
1254     for (const Document::Ptr &doc : qAsConst(snapshot))
1255         findNewLibraryImports(doc, snapshot, this, &importedFiles, &scannedPaths, &newLibraries);
1256     for (const QString &path : qAsConst(allApplicationDirectories))
1257         findNewQmlApplicationInPath(path, snapshot, this, &newLibraries);
1258 
1259     updateSourceFiles(importedFiles, true);
1260 
1261     if (!m_shouldScanImports)
1262         return;
1263     maybeScan(allImportPaths);
1264 }
1265 
loadPluginTypes(const QString & libraryPath,const QString & importPath,const QString & importUri,const QString & importVersion)1266 void ModelManagerInterface::loadPluginTypes(const QString &libraryPath, const QString &importPath,
1267                                    const QString &importUri, const QString &importVersion)
1268 {
1269     m_pluginDumper->loadPluginTypes(libraryPath, importPath, importUri, importVersion);
1270 }
1271 
1272 // is called *inside a c++ parsing thread*, to allow hanging on to source and ast
maybeQueueCppQmlTypeUpdate(const CPlusPlus::Document::Ptr & doc)1273 void ModelManagerInterface::maybeQueueCppQmlTypeUpdate(const CPlusPlus::Document::Ptr &doc)
1274 {
1275     // avoid scanning documents without source code available
1276     doc->keepSourceAndAST();
1277     if (doc->utf8Source().isEmpty()) {
1278         doc->releaseSourceAndAST();
1279         return;
1280     }
1281 
1282     // keep source and AST alive if we want to scan for register calls
1283     const bool scan = FindExportedCppTypes::maybeExportsTypes(doc);
1284     if (!scan)
1285         doc->releaseSourceAndAST();
1286 
1287     QMutexLocker locker(&g_instanceMutex);
1288     if (g_instance) // delegate actual queuing to the gui thread
1289         QMetaObject::invokeMethod(g_instance, [=] { queueCppQmlTypeUpdate(doc, scan); });
1290 }
1291 
queueCppQmlTypeUpdate(const CPlusPlus::Document::Ptr & doc,bool scan)1292 void ModelManagerInterface::queueCppQmlTypeUpdate(const CPlusPlus::Document::Ptr &doc, bool scan)
1293 {
1294     QPair<CPlusPlus::Document::Ptr, bool> prev = m_queuedCppDocuments.value(doc->fileName());
1295     if (prev.first && prev.second)
1296         prev.first->releaseSourceAndAST();
1297     m_queuedCppDocuments.insert(doc->fileName(), {doc, scan});
1298     m_updateCppQmlTypesTimer->start();
1299 }
1300 
startCppQmlTypeUpdate()1301 void ModelManagerInterface::startCppQmlTypeUpdate()
1302 {
1303     // if a future is still running, delay
1304     if (m_cppQmlTypesUpdater.isRunning()) {
1305         m_updateCppQmlTypesTimer->start();
1306         return;
1307     }
1308 
1309     CPlusPlus::CppModelManagerBase *cppModelManager =
1310             CPlusPlus::CppModelManagerBase::instance();
1311     if (!cppModelManager)
1312         return;
1313 
1314     m_cppQmlTypesUpdater = Utils::runAsync(&ModelManagerInterface::updateCppQmlTypes,
1315                 this, cppModelManager->snapshot(), m_queuedCppDocuments);
1316     m_queuedCppDocuments.clear();
1317 }
1318 
asyncReset()1319 void ModelManagerInterface::asyncReset()
1320 {
1321     m_asyncResetTimer->start();
1322 }
1323 
rescanExports(const QString & fileName,FindExportedCppTypes & finder,ModelManagerInterface::CppDataHash & newData)1324 bool rescanExports(const QString &fileName, FindExportedCppTypes &finder,
1325                    ModelManagerInterface::CppDataHash &newData)
1326 {
1327     bool hasNewInfo = false;
1328 
1329     QList<LanguageUtils::FakeMetaObject::ConstPtr> exported = finder.exportedTypes();
1330     QHash<QString, QString> contextProperties = finder.contextProperties();
1331     if (exported.isEmpty() && contextProperties.isEmpty()) {
1332         hasNewInfo = hasNewInfo || newData.remove(fileName) > 0;
1333     } else {
1334         ModelManagerInterface::CppData &data = newData[fileName];
1335         if (!hasNewInfo && (data.exportedTypes.size() != exported.size()
1336                             || data.contextProperties != contextProperties)) {
1337             hasNewInfo = true;
1338         }
1339         if (!hasNewInfo) {
1340             QHash<QString, QByteArray> newFingerprints;
1341             for (const auto &newType : qAsConst(exported))
1342                 newFingerprints[newType->className()]=newType->fingerprint();
1343             for (const auto &oldType : qAsConst(data.exportedTypes)) {
1344                 if (newFingerprints.value(oldType->className()) != oldType->fingerprint()) {
1345                     hasNewInfo = true;
1346                     break;
1347                 }
1348             }
1349         }
1350         data.exportedTypes = exported;
1351         data.contextProperties = contextProperties;
1352     }
1353     return hasNewInfo;
1354 }
1355 
updateCppQmlTypes(QFutureInterface<void> & futureInterface,ModelManagerInterface * qmlModelManager,const CPlusPlus::Snapshot & snapshot,const QHash<QString,QPair<CPlusPlus::Document::Ptr,bool>> & documents)1356 void ModelManagerInterface::updateCppQmlTypes(
1357         QFutureInterface<void> &futureInterface, ModelManagerInterface *qmlModelManager,
1358         const CPlusPlus::Snapshot &snapshot,
1359         const QHash<QString, QPair<CPlusPlus::Document::Ptr, bool>> &documents)
1360 {
1361     futureInterface.setProgressRange(0, documents.size());
1362     futureInterface.setProgressValue(0);
1363 
1364     CppDataHash newData;
1365     QHash<QString, QList<CPlusPlus::Document::Ptr>> newDeclarations;
1366     {
1367         QMutexLocker locker(&qmlModelManager->m_cppDataMutex);
1368         newData = qmlModelManager->m_cppDataHash;
1369         newDeclarations = qmlModelManager->m_cppDeclarationFiles;
1370     }
1371 
1372     FindExportedCppTypes finder(snapshot);
1373 
1374     bool hasNewInfo = false;
1375     using DocScanPair = QPair<CPlusPlus::Document::Ptr, bool>;
1376     for (const DocScanPair &pair : documents) {
1377         if (futureInterface.isCanceled())
1378             return;
1379         futureInterface.setProgressValue(futureInterface.progressValue() + 1);
1380 
1381         CPlusPlus::Document::Ptr doc = pair.first;
1382         const bool scan = pair.second;
1383         const QString fileName = doc->fileName();
1384         if (!scan) {
1385             hasNewInfo = newData.remove(fileName) > 0 || hasNewInfo;
1386             const auto savedDocs = newDeclarations.value(fileName);
1387             for (const CPlusPlus::Document::Ptr &savedDoc : savedDocs) {
1388                 finder(savedDoc);
1389                 hasNewInfo = rescanExports(savedDoc->fileName(), finder, newData) || hasNewInfo;
1390             }
1391             continue;
1392         }
1393 
1394         for (auto it = newDeclarations.begin(), end = newDeclarations.end(); it != end;) {
1395             for (auto docIt = it->begin(), endDocIt = it->end(); docIt != endDocIt;) {
1396                 const CPlusPlus::Document::Ptr &savedDoc = *docIt;
1397                 if (savedDoc->fileName() == fileName) {
1398                     savedDoc->releaseSourceAndAST();
1399                     it->erase(docIt);
1400                     break;
1401                 }
1402                 ++docIt;
1403             }
1404             if (it->isEmpty())
1405                 it = newDeclarations.erase(it);
1406             else
1407                 ++it;
1408         }
1409 
1410         const auto found = finder(doc);
1411         for (const QString &declarationFile : found) {
1412             newDeclarations[declarationFile].append(doc);
1413             doc->keepSourceAndAST(); // keep for later reparsing when dependent doc changes
1414         }
1415 
1416         hasNewInfo = rescanExports(fileName, finder, newData) || hasNewInfo;
1417         doc->releaseSourceAndAST();
1418     }
1419 
1420     QMutexLocker locker(&qmlModelManager->m_cppDataMutex);
1421     qmlModelManager->m_cppDataHash = newData;
1422     qmlModelManager->m_cppDeclarationFiles = newDeclarations;
1423     if (hasNewInfo)
1424         // one could get away with re-linking the cpp types...
1425         QMetaObject::invokeMethod(qmlModelManager, &ModelManagerInterface::asyncReset);
1426 }
1427 
cppData() const1428 ModelManagerInterface::CppDataHash ModelManagerInterface::cppData() const
1429 {
1430     QMutexLocker locker(&m_cppDataMutex);
1431     return m_cppDataHash;
1432 }
1433 
builtins(const Document::Ptr & doc) const1434 LibraryInfo ModelManagerInterface::builtins(const Document::Ptr &doc) const
1435 {
1436     const ProjectInfo info = projectInfoForPath(doc->fileName());
1437     if (!info.qtQmlPath.isEmpty())
1438         return m_validSnapshot.libraryInfo(info.qtQmlPath);
1439     return LibraryInfo();
1440 }
1441 
completeVContext(const ViewerContext & vCtx,const Document::Ptr & doc) const1442 ViewerContext ModelManagerInterface::completeVContext(const ViewerContext &vCtx,
1443                                                       const Document::Ptr &doc) const
1444 {
1445     return getVContext(vCtx, doc, false);
1446 }
1447 
getVContext(const ViewerContext & vCtx,const Document::Ptr & doc,bool limitToProject) const1448 ViewerContext ModelManagerInterface::getVContext(const ViewerContext &vCtx,
1449                                                  const Document::Ptr &doc,
1450                                                  bool limitToProject) const
1451 {
1452     ViewerContext res = vCtx;
1453 
1454     if (!doc.isNull()
1455             && ((vCtx.language == Dialect::AnyLanguage && doc->language() != Dialect::NoLanguage)
1456                 || (vCtx.language == Dialect::Qml
1457                     && (doc->language() == Dialect::QmlQtQuick2
1458                         || doc->language() == Dialect::QmlQtQuick2Ui))))
1459         res.language = doc->language();
1460     ProjectInfo info;
1461     if (!doc.isNull())
1462         info = projectInfoForPath(doc->fileName());
1463     ViewerContext defaultVCtx = defaultVContext(res.language, Document::Ptr(nullptr), false);
1464     ProjectInfo defaultInfo = defaultProjectInfo();
1465     if (info.qtQmlPath.isEmpty()) {
1466         info.qtQmlPath = defaultInfo.qtQmlPath;
1467         info.qtVersionString = defaultInfo.qtVersionString;
1468     }
1469     if (info.qtQmlPath.isEmpty() && info.importPaths.size() == 0)
1470         info.importPaths = defaultInfo.importPaths;
1471     info.applicationDirectories = Utils::filteredUnique(info.applicationDirectories
1472                                                         + defaultInfo.applicationDirectories);
1473     switch (res.flags) {
1474     case ViewerContext::Complete:
1475         break;
1476     case ViewerContext::AddAllPathsAndDefaultSelectors:
1477         res.selectors.append(defaultVCtx.selectors);
1478         Q_FALLTHROUGH();
1479     case ViewerContext::AddAllPaths:
1480     {
1481         for (const QString &path : qAsConst(defaultVCtx.paths))
1482             maybeAddPath(res, path);
1483         switch (res.language.dialect()) {
1484         case Dialect::AnyLanguage:
1485         case Dialect::Qml:
1486             maybeAddPath(res, info.qtQmlPath);
1487             Q_FALLTHROUGH();
1488         case Dialect::QmlQtQuick2:
1489         case Dialect::QmlQtQuick2Ui:
1490         {
1491             if (res.language == Dialect::QmlQtQuick2 || res.language == Dialect::QmlQtQuick2Ui)
1492                 maybeAddPath(res, info.qtQmlPath);
1493 
1494             QList<Dialect> languages = res.language.companionLanguages();
1495             auto addPathsOnLanguageMatch = [&](const PathsAndLanguages &importPaths) {
1496                 for (const auto &importPath : importPaths) {
1497                     if (languages.contains(importPath.language())
1498                             || importPath.language().companionLanguages().contains(res.language)) {
1499                         maybeAddPath(res, importPath.path().toString());
1500                     }
1501                 }
1502             };
1503             if (limitToProject) {
1504                 addPathsOnLanguageMatch(info.importPaths);
1505             } else {
1506                 QList<ProjectInfo> allProjects;
1507                 {
1508                     QMutexLocker locker(&m_mutex);
1509                     allProjects = m_projects.values();
1510                 }
1511                 std::sort(allProjects.begin(), allProjects.end(), &pInfoLessThanImports);
1512                 for (const ProjectInfo &pInfo : qAsConst(allProjects))
1513                     addPathsOnLanguageMatch(pInfo.importPaths);
1514             }
1515             const auto environmentPaths = environmentImportPaths();
1516             for (const QString &path : environmentPaths)
1517                 maybeAddPath(res, path);
1518             break;
1519         }
1520         case Dialect::NoLanguage:
1521         case Dialect::JavaScript:
1522         case Dialect::QmlTypeInfo:
1523         case Dialect::Json:
1524         case Dialect::QmlQbs:
1525         case Dialect::QmlProject:
1526             break;
1527         }
1528         break;
1529     }
1530     case ViewerContext::AddDefaultPathsAndSelectors:
1531         res.selectors.append(defaultVCtx.selectors);
1532         Q_FALLTHROUGH();
1533     case ViewerContext::AddDefaultPaths:
1534         for (const QString &path : qAsConst(defaultVCtx.paths))
1535             maybeAddPath(res, path);
1536         if (res.language == Dialect::AnyLanguage || res.language == Dialect::Qml)
1537             maybeAddPath(res, info.qtQmlPath);
1538         if (res.language == Dialect::AnyLanguage || res.language == Dialect::Qml
1539                 || res.language == Dialect::QmlQtQuick2 || res.language == Dialect::QmlQtQuick2Ui) {
1540             const auto environemntPaths = environmentImportPaths();
1541             for (const QString &path : environemntPaths)
1542                 maybeAddPath(res, path);
1543         }
1544         break;
1545     }
1546     res.flags = ViewerContext::Complete;
1547     res.applicationDirectories = info.applicationDirectories;
1548     return res;
1549 }
1550 
defaultVContext(Dialect language,const Document::Ptr & doc,bool autoComplete) const1551 ViewerContext ModelManagerInterface::defaultVContext(Dialect language,
1552                                                      const Document::Ptr &doc,
1553                                                      bool autoComplete) const
1554 {
1555     if (!doc.isNull()) {
1556         if (language == Dialect::AnyLanguage && doc->language() != Dialect::NoLanguage)
1557             language = doc->language();
1558         else if (language == Dialect::Qml &&
1559                  (doc->language() == Dialect::QmlQtQuick2
1560                   || doc->language() == Dialect::QmlQtQuick2Ui))
1561             language = doc->language();
1562     }
1563     ViewerContext defaultCtx;
1564     {
1565         QMutexLocker locker(&m_mutex);
1566         defaultCtx = m_defaultVContexts.value(language);
1567     }
1568     defaultCtx.language = language;
1569     return autoComplete ? completeVContext(defaultCtx, doc) : defaultCtx;
1570 }
1571 
projectVContext(Dialect language,const Document::Ptr & doc) const1572 ViewerContext ModelManagerInterface::projectVContext(Dialect language, const Document::Ptr &doc) const
1573 {
1574     // Returns context limited to the project the file belongs to
1575     ViewerContext defaultCtx = defaultVContext(language, doc, false);
1576     return getVContext(defaultCtx, doc, true);
1577 }
1578 
defaultProjectInfo() const1579 ModelManagerInterface::ProjectInfo ModelManagerInterface::defaultProjectInfo() const
1580 {
1581     QMutexLocker locker(&m_mutex);
1582     return m_defaultProjectInfo;
1583 }
1584 
defaultProjectInfoForProject(ProjectExplorer::Project *) const1585 ModelManagerInterface::ProjectInfo ModelManagerInterface::defaultProjectInfoForProject(
1586         ProjectExplorer::Project *) const
1587 {
1588     return ModelManagerInterface::ProjectInfo();
1589 }
1590 
setDefaultVContext(const ViewerContext & vContext)1591 void ModelManagerInterface::setDefaultVContext(const ViewerContext &vContext)
1592 {
1593     QMutexLocker locker(&m_mutex);
1594     m_defaultVContexts[vContext.language] = vContext;
1595 }
1596 
joinAllThreads(bool cancelOnWait)1597 void ModelManagerInterface::joinAllThreads(bool cancelOnWait)
1598 {
1599     while (true) {
1600         FutureSynchronizer futureSynchronizer;
1601         {
1602             QMutexLocker locker(&m_futuresMutex);
1603             futureSynchronizer = m_futureSynchronizer;
1604             m_futureSynchronizer.clearFutures();
1605         }
1606         futureSynchronizer.setCancelOnWait(cancelOnWait);
1607         if (futureSynchronizer.isEmpty())
1608             return;
1609     }
1610 }
1611 
test_joinAllThreads()1612 void ModelManagerInterface::test_joinAllThreads()
1613 {
1614     while (true) {
1615         joinAllThreads();
1616         // In order to process all onFinished handlers of finished futures
1617         QCoreApplication::processEvents();
1618         QMutexLocker lock(&m_futuresMutex);
1619         // If handlers created new futures, repeat the loop
1620         if (m_futureSynchronizer.isEmpty())
1621             return;
1622     }
1623 }
1624 
addFuture(const QFuture<void> & future)1625 void ModelManagerInterface::addFuture(const QFuture<void> &future)
1626 {
1627     QMutexLocker lock(&m_futuresMutex);
1628     m_futureSynchronizer.addFuture(future);
1629 }
1630 
ensuredGetDocumentForPath(const QString & filePath)1631 Document::Ptr ModelManagerInterface::ensuredGetDocumentForPath(const QString &filePath)
1632 {
1633     QmlJS::Document::Ptr document = newestSnapshot().document(filePath);
1634     if (!document) {
1635         document = QmlJS::Document::create(filePath, QmlJS::Dialect::Qml);
1636         QMutexLocker lock(&m_mutex);
1637 
1638         m_newestSnapshot.insert(document);
1639     }
1640 
1641     return document;
1642 }
1643 
resetCodeModel()1644 void ModelManagerInterface::resetCodeModel()
1645 {
1646     QStringList documents;
1647 
1648     {
1649         QMutexLocker locker(&m_mutex);
1650 
1651         // find all documents currently in the code model
1652         for (const Document::Ptr &doc : qAsConst(m_validSnapshot))
1653             documents.append(doc->fileName());
1654 
1655         // reset the snapshot
1656         m_validSnapshot = Snapshot();
1657         m_newestSnapshot = Snapshot();
1658         m_scannedPaths.clear();
1659     }
1660 
1661     // start a reparse thread
1662     updateSourceFiles(documents, false);
1663 
1664     // rescan import directories
1665     m_shouldScanImports = true;
1666     updateImportPaths();
1667 }
1668 
1669 } // namespace QmlJS
1670