File indexing completed on 2024-05-19 05:42:27

0001 // ct_lvtqtd_parse_codebase.cpp                               -*-C++-*-
0002 
0003 /*
0004 // Copyright 2023 Codethink Ltd <codethink@codethink.co.uk>
0005 // SPDX-License-Identifier: Apache-2.0
0006 //
0007 // Licensed under the Apache License, Version 2.0 (the "License");
0008 // you may not use this file except in compliance with the License.
0009 // You may obtain a copy of the License at
0010 //
0011 //     http://www.apache.org/licenses/LICENSE-2.0
0012 //
0013 // Unless required by applicable law or agreed to in writing, software
0014 // distributed under the License is distributed on an "AS IS" BASIS,
0015 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0016 // See the License for the specific language governing permissions and
0017 // limitations under the License.
0018 */
0019 #include <ct_lvtqtw_parse_codebase.h>
0020 
0021 #include <ct_lvtclp_cpp_tool.h>
0022 #include <ct_lvtmdb_functionobject.h>
0023 #include <ct_lvtmdb_soci_helper.h>
0024 #include <ct_lvtmdb_soci_reader.h>
0025 #include <ct_lvtmdb_soci_writer.h>
0026 #include <ct_lvtprj_projectfile.h>
0027 #include <ct_lvtqtw_textview.h>
0028 #include <ct_lvtshr_iterator.h>
0029 #ifdef CT_ENABLE_FORTRAN_SCANNER
0030 #include <fortran/ct_lvtclp_fortran_c_interop.h>
0031 #include <fortran/ct_lvtclp_fortran_tool.h>
0032 #endif
0033 
0034 #include <ui_ct_lvtqtw_parse_codebase.h>
0035 
0036 #include <KZip>
0037 
0038 #include <QDir>
0039 #include <QElapsedTimer>
0040 #include <QFileDialog>
0041 #include <QFileInfo>
0042 #include <QHeaderView>
0043 #include <QMessageBox>
0044 #include <QMovie>
0045 #include <QProcess>
0046 #include <QSettings>
0047 #include <QStandardPaths>
0048 #include <QSysInfo>
0049 #include <QTabBar>
0050 #include <QTableWidget>
0051 #include <QThread>
0052 #include <QVariant>
0053 
0054 #include <KNotification>
0055 
0056 #include <clang/Tooling/JSONCompilationDatabase.h>
0057 #include <preferences.h>
0058 #include <soci/soci.h>
0059 
0060 using namespace Codethink::lvtqtw;
0061 
0062 namespace {
0063 constexpr const char *COMPILE_COMMANDS = "compile_commands.json";
0064 constexpr const char *NON_LAKOSIAN_DIRS_SETTING = "non_lakosian_dirs";
0065 
0066 bool compressFiles(QFileInfo const& saveTo, QList<QFileInfo> const& files)
0067 {
0068     if (!QDir{}.exists(saveTo.absolutePath()) && !QDir{}.mkdir(saveTo.absolutePath())) {
0069         qDebug() << "[compressFiles] Could not prepare path to save.";
0070         return false;
0071     }
0072 
0073     auto zipFile = KZip(saveTo.absoluteFilePath());
0074     if (!zipFile.open(QIODevice::WriteOnly)) {
0075         qDebug() << "[compressFiles] Could not open file to compress:" << saveTo;
0076         qDebug() << zipFile.errorString();
0077         return false;
0078     }
0079 
0080     for (auto const& fileToCompress : qAsConst(files)) {
0081         auto r = zipFile.addLocalFile(fileToCompress.path(), "");
0082         if (!r) {
0083             qDebug() << "[compressFiles] Could not add files to project:" << fileToCompress;
0084             qDebug() << zipFile.errorString();
0085             return false;
0086         }
0087     }
0088 
0089     return true;
0090 }
0091 
0092 QString createSysinfoFileAt(const QString& lPath, const QString& ignorePattern)
0093 {
0094     QFile systemInformation(lPath + QDir::separator() + "system_information.txt");
0095     if (!systemInformation.open(QIODevice::WriteOnly | QIODevice::Text)) {
0096         qDebug() << "Error opening the sys info file.";
0097         return {};
0098     }
0099 
0100     QString systemInfoData;
0101 
0102     // this string should not be called with "tr", we do not want to
0103     // translate this to other languages, I have no intention on reading
0104     // a log file in russian.
0105     systemInfoData += "CPU: " + QSysInfo::currentCpuArchitecture() + "\n"
0106         + "Operating System: " + QSysInfo::productType() + "\n" + "Version " + QSysInfo::productVersion() + "\n"
0107         + "Ignored File Information: " + ignorePattern + "\n" + "CodeVis version:" + QString(__DATE__);
0108 
0109     systemInformation.write(systemInfoData.toLocal8Bit());
0110     systemInformation.close();
0111 
0112     return lPath + QDir::separator() + "system_information.txt";
0113 }
0114 
0115 } // namespace
0116 
0117 struct PkgMappingDialog : public QDialog {
0118   public:
0119     PkgMappingDialog()
0120     {
0121         setupUi();
0122 
0123         connect(m_addLineBtn, &QPushButton::clicked, this, &PkgMappingDialog::addTableWdgLine);
0124         connect(m_okBtn, &QPushButton::clicked, this, &PkgMappingDialog::acceptChanges);
0125         connect(m_cancelBtn, &QPushButton::clicked, this, &PkgMappingDialog::cancelChanges);
0126     }
0127 
0128     PkgMappingDialog(PkgMappingDialog const&) = delete;
0129 
0130     void populateTable(std::vector<std::pair<std::string, std::string>> const& thirdPartyPathMapping)
0131     {
0132         using Codethink::lvtshr::enumerate;
0133 
0134         for (auto&& [i, mapping] : enumerate(thirdPartyPathMapping)) {
0135             auto&& [k, v] = mapping;
0136             m_tableWdg->insertRow(static_cast<int>(i));
0137 
0138             auto *pathItem = new QTableWidgetItem();
0139             pathItem->setText(QString::fromStdString(k));
0140             m_tableWdg->setItem(static_cast<int>(i), 0, pathItem);
0141 
0142             auto *pkgNameItem = new QTableWidgetItem();
0143             pkgNameItem->setText(QString::fromStdString(v));
0144             m_tableWdg->setItem(static_cast<int>(i), 1, pkgNameItem);
0145         }
0146     }
0147 
0148     [[nodiscard]] bool changesAccepted() const
0149     {
0150         return m_acceptChanges;
0151     }
0152 
0153     [[nodiscard]] std::vector<std::pair<std::string, std::string>> pathMapping()
0154     {
0155         // Remove unexpected/unwanted characters in a given table item text
0156         auto filterText = [](QString&& txt) -> std::string {
0157             txt.replace(",", "");
0158             txt.replace("=", "");
0159             return txt.toStdString();
0160         };
0161 
0162         std::vector<std::pair<std::string, std::string>> pathMapping;
0163         for (auto i = 0; i < m_tableWdg->rowCount(); ++i) {
0164             auto pathText = filterText(m_tableWdg->item(i, 0)->text());
0165             auto pkgText = filterText(m_tableWdg->item(i, 1)->text());
0166             if (pathText.empty() || pkgText.empty()) {
0167                 continue;
0168             }
0169             pathMapping.emplace_back(pathText, pkgText);
0170         }
0171         return pathMapping;
0172     }
0173 
0174   private:
0175     void addTableWdgLine()
0176     {
0177         int row = m_tableWdg->rowCount();
0178         m_tableWdg->insertRow(row);
0179         m_tableWdg->setItem(row, 0, new QTableWidgetItem());
0180         m_tableWdg->setItem(row, 1, new QTableWidgetItem());
0181     }
0182 
0183     void acceptChanges()
0184     {
0185         m_acceptChanges = true;
0186         close();
0187     }
0188 
0189     void cancelChanges()
0190     {
0191         m_acceptChanges = false;
0192         close();
0193     }
0194 
0195     void setupUi()
0196     {
0197         setWindowModality(Qt::ApplicationModal);
0198         setWindowTitle("Third party packages mapping");
0199         auto *layout = new QVBoxLayout{this};
0200         m_tableWdg = new QTableWidget{this};
0201         m_tableWdg->setColumnCount(2);
0202         m_tableWdg->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
0203         m_tableWdg->setHorizontalHeaderLabels({"Path", "Package name"});
0204         layout->addWidget(m_tableWdg);
0205         m_addLineBtn = new QPushButton("+");
0206         layout->addWidget(m_addLineBtn);
0207         auto *okCancelBtnWdg = new QWidget{this};
0208         auto *okCancelBtnLayout = new QHBoxLayout{okCancelBtnWdg};
0209         auto *okCancelSpacer = new QSpacerItem{0, 0, QSizePolicy::Expanding, QSizePolicy::Fixed};
0210         m_okBtn = new QPushButton("Ok");
0211         m_cancelBtn = new QPushButton("Cancel");
0212         okCancelBtnLayout->addItem(okCancelSpacer);
0213         okCancelBtnLayout->addWidget(m_okBtn);
0214         okCancelBtnLayout->addWidget(m_cancelBtn);
0215         layout->addWidget(okCancelBtnWdg);
0216         setLayout(layout);
0217     }
0218 
0219     QTableWidget *m_tableWdg = nullptr;
0220     QPushButton *m_okBtn = nullptr;
0221     QPushButton *m_cancelBtn = nullptr;
0222     QPushButton *m_addLineBtn = nullptr;
0223     bool m_acceptChanges = false;
0224 };
0225 
0226 struct ParseCodebaseDialog::Private {
0227     State dialogState = State::Idle;
0228     std::shared_ptr<lvtmdb::ObjectStore> sharedMemDb = nullptr;
0229     std::unique_ptr<lvtclp::CppTool> tool_p = nullptr;
0230 #ifdef CT_ENABLE_FORTRAN_SCANNER
0231     std::unique_ptr<lvtclp::fortran::Tool> fortran_tool_p = nullptr;
0232 #endif
0233     QThread *parseThread = nullptr;
0234     bool threadSuccess = false;
0235     int progress = 0;
0236 
0237     std::map<long, TextView *> threadIdToWidget;
0238     QString codebasePath;
0239 
0240     using ThirdPartyPath = std::string;
0241     using ThirdPartyPackageName = std::string;
0242     std::vector<std::pair<ThirdPartyPath, ThirdPartyPackageName>> thirdPartyPathMapping;
0243 
0244     std::optional<std::reference_wrapper<Codethink::lvtplg::PluginManager>> pluginManager = std::nullopt;
0245     QElapsedTimer parseTimer;
0246 };
0247 
0248 ParseCodebaseDialog::ParseCodebaseDialog(QWidget *parent):
0249     QDialog(parent),
0250     d(std::make_unique<ParseCodebaseDialog::Private>()),
0251     ui(std::make_unique<Ui::ParseCodebaseDialog>())
0252 {
0253     d->sharedMemDb = std::make_shared<lvtmdb::ObjectStore>();
0254     ui->setupUi(this);
0255 
0256     // TODO: Remove those things / Fix them when we finish the presentation.
0257     ui->runCmake->setVisible(false);
0258     ui->runCmake->setChecked(false);
0259     ui->refreshDb->setVisible(false);
0260     ui->updateDb->setVisible(false);
0261     ui->updateDb->setChecked(true);
0262 
0263     ui->ignorePattern->setText(Preferences::lastIgnorePattern());
0264     ui->compileCommandsFolder->setText(Preferences::lastConfigureJson());
0265     ui->sourceFolder->setText(Preferences::lastSourceFolder());
0266     ui->showDbErrors->setVisible(false);
0267 
0268     ui->nonLakosians->setText(getNonLakosianDirSettings(Preferences::lastConfigureJson()));
0269 
0270     connect(this, &ParseCodebaseDialog::parseFinished, this, [this] {
0271         ui->btnSaveOutput->setEnabled(true);
0272         ui->btnClose->setEnabled(true);
0273     });
0274 
0275     connect(ui->threadCount, QOverload<int>::of(&QSpinBox::valueChanged), this, [this] {
0276         Preferences::setThreadCount(ui->threadCount->value());
0277     });
0278 
0279     ui->threadCount->setValue(Preferences::threadCount());
0280     ui->threadCount->setMaximum(QThread::idealThreadCount() + 1);
0281 
0282     connect(ui->btnSaveOutput, &QPushButton::clicked, this, &ParseCodebaseDialog::saveOutput);
0283 
0284     connect(ui->searchCompileCommands, &QPushButton::clicked, this, &ParseCodebaseDialog::searchForBuildFolder);
0285 
0286     connect(ui->nonLakosiansSearch, &QPushButton::clicked, this, &ParseCodebaseDialog::searchForNonLakosianDir);
0287     connect(ui->sourceFolderSearch, &QPushButton::clicked, this, &ParseCodebaseDialog::searchForSourceFolder);
0288     connect(ui->thirdPartyPkgMappingBtn, &QPushButton::clicked, this, &ParseCodebaseDialog::selectThirdPartyPkgMapping);
0289 
0290     connect(ui->ignorePattern, &QLineEdit::textChanged, this, [this] {
0291         Preferences::setLastIgnorePattern(ui->ignorePattern->text());
0292     });
0293 
0294     connect(ui->compileCommandsFolder, &QLineEdit::textChanged, this, [this] {
0295         Preferences::setLastConfigureJson(ui->compileCommandsFolder->text());
0296 
0297         ui->nonLakosians->setText(getNonLakosianDirSettings(ui->compileCommandsFolder->text()));
0298     });
0299 
0300     connect(ui->sourceFolder, &QLineEdit::textChanged, this, [this] {
0301         Preferences::setLastSourceFolder(ui->sourceFolder->text());
0302     });
0303 
0304     connect(ui->nonLakosians, &QLineEdit::textChanged, this, [this] {
0305         setNonLakosianDirSettings(ui->compileCommandsFolder->text(), ui->nonLakosians->text());
0306     });
0307 
0308     connect(ui->btnClose, &QPushButton::clicked, this, [this] {
0309         // the close button should just hide the dialog. we display the dialog with show()
0310         // so it does not block the event loop. The only correct time to properly close()
0311         // the dialog is when the parse process finishes.
0312         hide();
0313     });
0314 
0315     connect(ui->btnParse, &QPushButton::clicked, this, [this] {
0316         if (d->dialogState == State::Idle) {
0317             initParse();
0318         }
0319         if (d->dialogState == State::RunAllLogical) {
0320             close();
0321         }
0322     });
0323 
0324     connect(ui->btnCancelParse, &QPushButton::clicked, this, [this] {
0325         if (d->parseThread) {
0326             d->dialogState = State::Killed;
0327             ui->btnCancelParse->setEnabled(false);
0328             ui->progressBarText->setText(tr("Cancelling parse threads, this might take a few seconds."));
0329             if (d->tool_p) {
0330                 d->tool_p->cancelRun();
0331             }
0332             // endParse will emit parseFinished
0333         } else {
0334             Q_EMIT parseFinished(State::Idle);
0335         }
0336     });
0337 
0338     ui->progressBar->setMinimum(0);
0339 
0340     connect(ui->compileCommandsFolder, &QLineEdit::textChanged, this, [this] {
0341         validate();
0342     });
0343 
0344     connect(ui->sourceFolder, &QLineEdit::textChanged, this, [this] {
0345         validate();
0346     });
0347 
0348     ui->projectBuildFolderError->setVisible(false);
0349     ui->projectSourceFolderError->setVisible(false);
0350 
0351     QFile markdownFile(":/md/codebase_gen_doc");
0352     markdownFile.open(QIODevice::ReadOnly);
0353     const QString data = markdownFile.readAll();
0354 
0355 // Qt on Appimage is 5.13 aparently.
0356 #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
0357     ui->textBrowser->setText(data);
0358 #else
0359     ui->textBrowser->setMarkdown(data);
0360 #endif
0361 
0362     Qt::WindowFlags flags;
0363     flags =
0364         windowFlags() & ~(Qt::WindowCloseButtonHint | Qt::WindowMinMaxButtonsHint | Qt::WindowContextHelpButtonHint);
0365     setWindowFlags(flags);
0366 
0367     validate();
0368 }
0369 
0370 void ParseCodebaseDialog::validate()
0371 {
0372     // a QValidator will not allow the string to be set, but we need to tell the user the reason that
0373     // the string was not set. So instead of using the `setValidator` calls on QLineEdit, we *accept*
0374     // the wrong string, and if the validator is invalid, we display an error message, while also blocking
0375     // the Parse button.
0376     const auto emptyErrorMsg = tr("This field can't be empty");
0377     const auto wslErrorMsg = tr("The software does not support wsl, use the native linux build.");
0378     const auto errorCss = QString("border: 1px solid red");
0379     const auto missingCompileCommands = tr("The specified folder does not contains compile_commands.json");
0380     const auto wslStr = std::string{"wsl://"};
0381 
0382     bool disableParse = false;
0383     QFileInfo inf(ui->compileCommandsFolder->text() + QDir::separator() + "compile_commands.json");
0384     if (ui->compileCommandsFolder->text().isEmpty()) {
0385         ui->projectBuildFolderError->setVisible(true);
0386         ui->projectBuildFolderError->setText(emptyErrorMsg);
0387         ui->compileCommandsFolder->setStyleSheet(errorCss);
0388         disableParse = true;
0389     } else if (!inf.exists()) {
0390         ui->projectBuildFolderError->setVisible(true);
0391         ui->projectBuildFolderError->setText(missingCompileCommands);
0392         ui->compileCommandsFolder->setStyleSheet(errorCss);
0393         disableParse = true;
0394     } else if (ui->compileCommandsFolder->text().startsWith(wslStr.c_str())) {
0395         ui->projectBuildFolderError->setVisible(true);
0396         ui->projectBuildFolderError->setText(wslErrorMsg);
0397         ui->compileCommandsFolder->setStyleSheet(errorCss);
0398         disableParse = true;
0399     } else {
0400         ui->projectBuildFolderError->setVisible(false);
0401         ui->compileCommandsFolder->setStyleSheet(QString());
0402     }
0403 
0404     if (ui->sourceFolder->text().isEmpty()) {
0405         ui->projectSourceFolderError->setVisible(true);
0406         ui->projectSourceFolderError->setText(emptyErrorMsg);
0407         ui->sourceFolder->setStyleSheet(errorCss);
0408         disableParse = true;
0409     } else if (ui->sourceFolder->text().startsWith(wslStr.c_str())) {
0410         ui->projectSourceFolderError->setVisible(true);
0411         ui->projectSourceFolderError->setText(wslErrorMsg);
0412         ui->sourceFolder->setStyleSheet(errorCss);
0413         disableParse = true;
0414     } else {
0415         ui->projectSourceFolderError->setVisible(false);
0416         ui->sourceFolder->setStyleSheet(QString());
0417     }
0418 
0419     ui->btnParse->setDisabled(disableParse);
0420 }
0421 
0422 ParseCodebaseDialog::~ParseCodebaseDialog()
0423 {
0424     Preferences::self()->save();
0425 }
0426 
0427 QString ParseCodebaseDialog::getNonLakosianDirSettings(const QString& buildDir)
0428 {
0429     QSettings settings;
0430 
0431     // QMap<QString, QString>: buildDir -> nonLakosianDirSettings
0432     QMap<QString, QVariant> nonLakosianDirMap = settings.value(NON_LAKOSIAN_DIRS_SETTING).toMap();
0433 
0434     // if it is not in the map or if the variant is not a string, we return ""
0435     return nonLakosianDirMap.value(buildDir).toString();
0436 }
0437 
0438 void ParseCodebaseDialog::setNonLakosianDirSettings(const QString& buildDir, const QString& nonLakosianDirs)
0439 {
0440     QSettings settings;
0441 
0442     // QMap<QString, QString>: buildDir -> nonLakosianDirSettings
0443     QMap<QString, QVariant> nonLakosianDirMap = settings.value(NON_LAKOSIAN_DIRS_SETTING).toMap();
0444 
0445     nonLakosianDirMap.insert(buildDir, QVariant(nonLakosianDirs));
0446 
0447     settings.setValue(NON_LAKOSIAN_DIRS_SETTING, QVariant(nonLakosianDirMap));
0448 }
0449 
0450 void ParseCodebaseDialog::setCodebasePath(const QString& path)
0451 {
0452     d->codebasePath = path;
0453 }
0454 
0455 QString ParseCodebaseDialog::codebasePath() const
0456 {
0457     // conversion dance. Qt has no conversion from std::string_view. :|
0458     const auto dbFilename = std::string(lvtprj::ProjectFile::codebaseDbFilename());
0459     const auto qDbFilename = QString::fromStdString(dbFilename);
0460     return d->codebasePath + QDir::separator() + qDbFilename;
0461 }
0462 
0463 void ParseCodebaseDialog::searchForBuildFolder()
0464 {
0465     auto openDir = [&]() {
0466         auto lastDir = QDir{ui->compileCommandsFolder->text()};
0467         if (!lastDir.isEmpty() && lastDir.exists()) {
0468             return lastDir.canonicalPath();
0469         }
0470         return QDir::homePath();
0471     }();
0472 
0473     const QString buildDirectory = QFileDialog::getExistingDirectory(this, tr("Project Build Directory"), openDir);
0474 
0475     if (buildDirectory.isEmpty()) {
0476         return;
0477     }
0478 
0479     ui->compileCommandsFolder->setText(buildDirectory);
0480 
0481     // Tries to determine the source folder automatically
0482     auto sourceFolderGuess = std::filesystem::canonical(std::filesystem::path(buildDirectory.toStdString()) / "..");
0483     ui->sourceFolder->setText(QString::fromStdString(sourceFolderGuess.string()));
0484 }
0485 
0486 void ParseCodebaseDialog::searchForSourceFolder()
0487 {
0488     auto openDir = [&]() {
0489         auto lastDir = QDir{ui->sourceFolder->text()};
0490         if (!lastDir.isEmpty() && lastDir.exists()) {
0491             return lastDir.canonicalPath();
0492         }
0493         return QDir::homePath();
0494     }();
0495 
0496     const QString dir = QFileDialog::getExistingDirectory(this, tr("Project Source Directory"), openDir);
0497     if (dir.isEmpty()) {
0498         // User hits cancel
0499         return;
0500     }
0501     ui->sourceFolder->setText(dir);
0502 }
0503 
0504 void ParseCodebaseDialog::searchForNonLakosianDir()
0505 {
0506     QString compileCommandsFolder = ui->compileCommandsFolder->text();
0507     if (compileCommandsFolder.isEmpty()) {
0508         compileCommandsFolder = QDir::homePath();
0509     }
0510 
0511     const QString nonLakosianDir =
0512         QFileDialog::getExistingDirectory(this, tr("Non-lakosian directory"), compileCommandsFolder);
0513     QFileInfo dir(nonLakosianDir);
0514     if (!dir.exists()) {
0515         return;
0516     }
0517 
0518     if (ui->nonLakosians->text().isEmpty()) {
0519         ui->nonLakosians->setText(nonLakosianDir);
0520     } else {
0521         ui->nonLakosians->setText(ui->nonLakosians->text() + "," + nonLakosianDir);
0522     }
0523 }
0524 
0525 void ParseCodebaseDialog::selectThirdPartyPkgMapping()
0526 {
0527     auto pkgMappingWindow = PkgMappingDialog{};
0528     pkgMappingWindow.populateTable(d->thirdPartyPathMapping);
0529     pkgMappingWindow.show();
0530     pkgMappingWindow.exec();
0531 
0532     if (pkgMappingWindow.changesAccepted()) {
0533         d->thirdPartyPathMapping = pkgMappingWindow.pathMapping();
0534 
0535         auto newText = QString{};
0536         for (auto&& [k, v] : d->thirdPartyPathMapping) {
0537             newText += QString::fromStdString(k) + "=" + QString::fromStdString(v) + ",";
0538         }
0539         newText.chop(1);
0540         ui->thirdPartyPkgMapping->setText(newText);
0541     }
0542 }
0543 
0544 void ParseCodebaseDialog::saveOutput()
0545 {
0546     const QUrl directory = QFileDialog::getExistingDirectoryUrl(this);
0547     if (!directory.isValid()) {
0548         return;
0549     }
0550 
0551     const QString lPath = directory.toLocalFile();
0552 
0553     const std::filesystem::path compile_commands_orig =
0554         (ui->compileCommandsFolder->text() + QDir::separator() + COMPILE_COMMANDS).toStdString();
0555     const std::filesystem::path compile_commands_dest = (lPath + QDir::separator() + COMPILE_COMMANDS).toStdString();
0556     try {
0557         std::filesystem::copy_file(compile_commands_orig, compile_commands_dest);
0558     } catch (std::filesystem::filesystem_error& e) {
0559         qDebug() << "Could not copy compile_commands.json to the save folder" << e.what();
0560         return;
0561     }
0562 
0563     const QString sysInfoFile = createSysinfoFileAt(lPath, ui->ignorePattern->text());
0564     const QString compileCommandsFile = lPath + QDir::separator() + COMPILE_COMMANDS;
0565 
0566     QList<QFileInfo> textFiles;
0567     textFiles.append(QFileInfo{compileCommandsFile});
0568     textFiles.append(QFileInfo{sysInfoFile});
0569     for (int i = 0; i < ui->tabWidget->count(); i++) {
0570         auto *textEdit = qobject_cast<TextView *>(ui->tabWidget->widget(i));
0571         QString saveFilePath = ui->tabWidget->tabText(i);
0572         saveFilePath.replace(' ', '_');
0573         saveFilePath.append(".txt");
0574         saveFilePath = lPath + QDir::separator() + saveFilePath;
0575         textEdit->saveFileTo(saveFilePath);
0576         textFiles.append(QFileInfo{saveFilePath});
0577     }
0578 
0579     const QFileInfo outputFile =
0580         QFileInfo{directory.toLocalFile() + QDir::separator() + "codevis_dump_"
0581                   + QString::number(QDateTime::currentDateTime().toMSecsSinceEpoch()) + ".zip"};
0582 
0583     if (compressFiles(outputFile, textFiles)) {
0584         QMessageBox::information(this,
0585                                  tr("Export Debug File"),
0586                                  tr("File saved successfully at \n%1").arg(outputFile.fileName()));
0587     } else {
0588         QMessageBox::critical(this, tr("Export Debug File"), tr("Error exporting the build data."));
0589     }
0590 
0591     for (const auto& textFile : qAsConst(textFiles)) {
0592         std::filesystem::remove(textFile.absoluteFilePath().toStdString());
0593     }
0594 }
0595 
0596 void ParseCodebaseDialog::showEvent(QShowEvent *event)
0597 {
0598     if (d->dialogState != State::RunAllLogical) {
0599         // if the logical parse is currently running in the background
0600         // we should leave the window as it is so that it can be used to view
0601         // the progress
0602         reset();
0603     }
0604     QDialog::showEvent(event);
0605 }
0606 
0607 void ParseCodebaseDialog::reset()
0608 {
0609     d->dialogState = State::Idle;
0610     ui->btnClose->setEnabled(true);
0611     ui->btnCancelParse->setEnabled(false);
0612     ui->errorText->setText(QString());
0613     ui->errorText->setVisible(false);
0614     ui->progressBar->setValue(0);
0615     ui->progressBarText->setVisible(false);
0616 
0617     if (ui->tabWidget->count() != 0) {
0618         // we already have some debug output. Don't close it, allow saving it.
0619         ui->btnSaveOutput->setEnabled(true);
0620         ui->stackedWidget->setCurrentIndex(1);
0621     } else {
0622         // no debug output in memory. Don't show it.
0623         ui->btnSaveOutput->setEnabled(false);
0624         ui->stackedWidget->setCurrentIndex(0);
0625     }
0626     validate();
0627 }
0628 
0629 void ParseCodebaseDialog::initParse()
0630 {
0631     // initParse() is called twice, once for the Physical, and again for the Logical parses.
0632     assert(d->dialogState == State::Idle || d->dialogState == State::RunAllPhysical);
0633 
0634     // re-enable cancel button if it was disabled (e.g. because it was used on
0635     // the last run)
0636     ui->btnCancelParse->setEnabled(true);
0637 
0638     // We can't remove the tabs on ::reset, because the user might want to
0639     // save the tab information on disk. We can't remove the tabs on ::close
0640     // because the user can close and reopen the dialog multiple times while
0641     // the parse is running, so the only time I can safely remove the tabs
0642     // is when we start a new parse from scratch.
0643     removeParseMessageTabs();
0644 
0645     if (ui->refreshDb->isChecked()) {
0646         if (QFileInfo::exists(codebasePath()) && d->dialogState == State::Idle) {
0647             QFile dbFile(codebasePath());
0648             const bool removed = dbFile.remove();
0649             if (!removed) {
0650                 ui->errorText->setText(
0651                     tr("Error removing the database file, check if you have permissions to do that"));
0652                 ui->errorText->setVisible(true);
0653                 ui->btnClose->setEnabled(true);
0654                 ui->btnSaveOutput->setEnabled(true);
0655                 ui->progressBarText->setVisible(false);
0656                 return;
0657             }
0658         }
0659     }
0660 
0661     if (ui->physicalOnly->checkState() != Qt::Unchecked && d->dialogState == State::Idle) {
0662         d->dialogState = State::RunPhysicalOnly;
0663     } else {
0664         if (d->dialogState == State::Idle) {
0665             d->dialogState = State::RunAllPhysical;
0666         } else if (d->dialogState == State::RunAllPhysical) {
0667             d->dialogState = State::RunAllLogical;
0668         }
0669     }
0670 
0671     d->parseTimer.restart();
0672     const auto compileCommandsDir = ui->compileCommandsFolder->text();
0673     const auto compileCommandsJson = (compileCommandsDir + QDir::separator() + COMPILE_COMMANDS).toStdString();
0674     const auto compileCommandsExists = QFileInfo::exists(QString::fromStdString(compileCommandsJson));
0675     const auto physicalRun = d->dialogState == State::RunPhysicalOnly || d->dialogState == State::RunAllPhysical;
0676     const auto mustGenerateCompileCommands = physicalRun && (!compileCommandsExists || ui->runCmake->checkState());
0677     const auto ignoreList = ignoredItemsAsStdVec();
0678     const auto nonLakosianDirs = nonLakosianDirsAsStdVec();
0679     if (mustGenerateCompileCommands) {
0680         runCMakeAndInitParse_Step2(compileCommandsJson, ignoreList, nonLakosianDirs);
0681     } else {
0682         initParse_Step2(compileCommandsJson, ignoreList, nonLakosianDirs);
0683     }
0684 }
0685 
0686 void ParseCodebaseDialog::runCMakeAndInitParse_Step2(const std::string& compileCommandsJson,
0687                                                      const std::vector<std::string>& ignoreList,
0688                                                      const std::vector<std::filesystem::path>& nonLakosianDirs)
0689 {
0690     const QString cmakeExecutable = QStandardPaths::findExecutable("cmake");
0691     if (cmakeExecutable.isEmpty()) {
0692         ui->errorText->setText(tr("CMake executable not found, please install it and add to the PATH"));
0693         ui->btnParse->setEnabled(true);
0694         ui->btnCancelParse->setEnabled(false);
0695         return;
0696     }
0697 
0698     // Force a refresh of the `compile_commands.json` file.
0699     auto *refreshCompileCommands = new QProcess();
0700     auto onFinishCMakeRun =
0701         [this, compileCommandsJson, ignoreList, nonLakosianDirs, refreshCompileCommands](int exitCode,
0702                                                                                          QProcess::ExitStatus) {
0703             if (exitCode != 0) {
0704                 const auto errorStr = QString(refreshCompileCommands->readAllStandardOutput());
0705                 ui->errorText->setText(tr("Error generating the compile_commands.json file\n%1").arg(errorStr));
0706                 ui->errorText->show();
0707                 ui->btnParse->setEnabled(true);
0708                 ui->btnCancelParse->setEnabled(false);
0709                 return;
0710             }
0711             sender()->deleteLater();
0712 
0713             initParse_Step2(compileCommandsJson, ignoreList, nonLakosianDirs);
0714         };
0715     connect(refreshCompileCommands,
0716             QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
0717             this,
0718             onFinishCMakeRun);
0719 
0720     ui->errorText->setText(tr("Generating compile_commands.json, this might take a few minutes."));
0721     ui->errorText->show();
0722     ui->btnParse->setEnabled(false);
0723     refreshCompileCommands->setWorkingDirectory(ui->compileCommandsFolder->text());
0724     refreshCompileCommands->start(cmakeExecutable, QStringList({".", "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON"}));
0725 }
0726 
0727 void ParseCodebaseDialog::initParse_Step2(const std::string& compileCommandsJson,
0728                                           const std::vector<std::string>& ignoreList,
0729                                           const std::vector<std::filesystem::path>& nonLakosianDirs)
0730 {
0731     const bool catchCodeAnalysisOutput = Preferences::enableCodeParseDebugOutput();
0732 
0733     if (!d->tool_p) {
0734         d->tool_p = std::make_unique<lvtclp::CppTool>(sourcePath(),
0735                                                       std::vector<std::filesystem::path>{compileCommandsJson},
0736                                                       codebasePath().toStdString(),
0737                                                       ui->threadCount->value(),
0738                                                       ignoreList,
0739                                                       nonLakosianDirs,
0740                                                       d->thirdPartyPathMapping,
0741                                                       catchCodeAnalysisOutput);
0742     }
0743 #ifdef CT_ENABLE_FORTRAN_SCANNER
0744     if (!d->fortran_tool_p) {
0745         d->fortran_tool_p = lvtclp::fortran::Tool::fromCompileCommands(compileCommandsJson);
0746     }
0747     d->fortran_tool_p->setSharedMemDb(d->sharedMemDb);
0748 #endif
0749     d->tool_p->setSharedMemDb(d->sharedMemDb);
0750 
0751     d->tool_p->setShowDatabaseErrors(ui->showDbErrors->isChecked());
0752     connect(d->tool_p.get(),
0753             &lvtclp::CppTool::processingFileNotification,
0754             this,
0755             &ParseCodebaseDialog::processingFileNotification,
0756             Qt::QueuedConnection);
0757 
0758     connect(d->tool_p.get(),
0759             &lvtclp::CppTool::aboutToCallClangNotification,
0760             this,
0761             &ParseCodebaseDialog::aboutToCallClangNotification,
0762             Qt::QueuedConnection);
0763 
0764     connect(d->tool_p.get(),
0765             &lvtclp::CppTool::messageFromThread,
0766             this,
0767             &ParseCodebaseDialog::receivedMessage,
0768             Qt::QueuedConnection);
0769 
0770 #ifdef CT_ENABLE_FORTRAN_SCANNER
0771     auto threadFn = [this]() {
0772         assert(d->tool_p);
0773         assert(d->fortran_tool_p);
0774         if (d->dialogState == State::RunPhysicalOnly || d->dialogState == State::RunAllPhysical) {
0775             d->threadSuccess = d->tool_p->runPhysical();
0776             d->threadSuccess = d->fortran_tool_p->runPhysical();
0777         } else if (d->dialogState == State::RunAllLogical) {
0778             d->threadSuccess = d->tool_p->runFull(/*skipPhysical=*/true);
0779             d->threadSuccess = d->fortran_tool_p->runFull(/*skipPhysical=*/true);
0780             Codethink::lvtclp::fortran::solveFortranToCInteropDeps(*d->sharedMemDb);
0781         }
0782     };
0783 #else
0784     auto threadFn = [this]() {
0785         assert(d->tool_p);
0786         if (d->dialogState == State::RunPhysicalOnly || d->dialogState == State::RunAllPhysical) {
0787             d->threadSuccess = d->tool_p->runPhysical();
0788         } else if (d->dialogState == State::RunAllLogical) {
0789             d->threadSuccess = d->tool_p->runFull(/*skipPhysical=*/true);
0790         }
0791     };
0792 #endif
0793 
0794     d->parseThread = QThread::create(threadFn);
0795 
0796     connect(d->parseThread, &QThread::finished, this, &ParseCodebaseDialog::readyForDbUpdate);
0797 
0798     ui->progressBar->setValue(0);
0799     ui->progressBarText->setVisible(true);
0800     if (d->dialogState == State::RunPhysicalOnly || d->dialogState == State::RunAllPhysical) {
0801         ui->progressBarText->setText(tr("Initialising physical parse..."));
0802         ui->errorText->setText(tr("Performing physical parse..."));
0803         ui->errorText->show();
0804     } else if (d->dialogState == State::RunAllLogical) {
0805         ui->progressBarText->setText(tr("Initialising logical parse..."));
0806         ui->errorText->setText(tr("Performing logical parse..."));
0807         ui->errorText->show();
0808     } else {
0809         assert(false && "Unreachable");
0810     }
0811 
0812     // it is okay to close the window after the physical parse is completed and
0813     // allow the logical parse to continue in the background. Otherwise disable
0814     // closing while a parse is running.
0815     ui->btnClose->setEnabled(d->dialogState == State::RunAllLogical);
0816 
0817     ui->btnParse->setEnabled(false);
0818     ui->btnSaveOutput->setEnabled(false);
0819     Q_EMIT parseStarted(d->dialogState);
0820     d->parseThread->start();
0821 }
0822 
0823 void ParseCodebaseDialog::updateDatabase()
0824 // parseThread finished, we asked for a callback from the main window when
0825 // it was ready to have its database replaced. That callback just happened
0826 // so lets go! Delete the old database. Write the new database.
0827 {
0828     d->parseThread->deleteLater();
0829     d->parseThread = nullptr;
0830 
0831     assert(d->tool_p);
0832     assert(d->dialogState != State::Idle);
0833 
0834     std::string path = codebasePath().toStdString();
0835 
0836     if (std::filesystem::exists(path)) {
0837         bool success = false;
0838         try {
0839             success = std::filesystem::remove(path);
0840         } catch (const std::exception& e) {
0841             std::cerr << __func__ << ": exception during delete: " << e.what() << std::endl;
0842             success = false;
0843         }
0844 
0845         if (!success) {
0846             qWarning() << "Failed to delete database at" << codebasePath();
0847             ui->errorText->setText(tr("Failed to delete old database"));
0848             ui->errorText->show();
0849             d->dialogState = State::Idle;
0850             Q_EMIT parseFinished(State::Idle);
0851             // TODO: prompt user for somewhere else to write the database
0852             return;
0853         }
0854     }
0855 
0856     {
0857         lvtmdb::SociWriter writer;
0858         writer.createOrOpen(path);
0859         d->sharedMemDb->writeToDatabase(writer);
0860     }
0861 
0862     if (d->pluginManager) {
0863         auto& pm = (*d->pluginManager).get();
0864 
0865         d->tool_p->setHeaderLocationCallback(
0866             [&pm](std::string const& sourceFile, std::string const& includedFile, unsigned lineNo) {
0867                 pm.callHooksPhysicalParserOnHeaderFound(
0868                     [&sourceFile]() {
0869                         return sourceFile;
0870                     },
0871                     [&includedFile]() {
0872                         return includedFile;
0873                     },
0874                     [&lineNo]() {
0875                         return lineNo;
0876                     });
0877             });
0878 
0879         d->tool_p->setHandleCppCommentsCallback(
0880             [&pm](const std::string& filename, const std::string& briefText, unsigned startLine, unsigned endLine) {
0881                 pm.callHooksPluginLogicalParserOnCppCommentFoundHandler(
0882                     [&filename]() {
0883                         return filename;
0884                     },
0885                     [&briefText]() {
0886                         return briefText;
0887                     },
0888                     [&startLine]() {
0889                         return startLine;
0890                     },
0891                     [&endLine]() {
0892                         return endLine;
0893                     });
0894             });
0895     }
0896 
0897     endParse();
0898 }
0899 
0900 void ParseCodebaseDialog::endParse()
0901 {
0902     assert(d->dialogState != State::Idle);
0903 
0904     ui->btnParse->setEnabled(true);
0905     ui->progressBarText->setVisible(false);
0906     ui->progressBar->setValue(0);
0907 
0908     if (d->dialogState == State::Killed) {
0909         ui->errorText->setText(tr("Parsing operation killed."));
0910         ui->errorText->show();
0911         d->dialogState = State::Idle;
0912         Q_EMIT parseFinished(State::Killed);
0913         d->sharedMemDb->withRWLock([&] {
0914             d->sharedMemDb->clear();
0915         });
0916         d->tool_p = nullptr;
0917 #ifdef CT_ENABLE_FORTRAN_SCANNER
0918         d->fortran_tool_p = nullptr;
0919 #endif
0920         return;
0921     }
0922 
0923     if (!d->threadSuccess) {
0924         ui->errorText->setText(tr("Error parsing codebase with clang"));
0925         ui->errorText->show();
0926         d->dialogState = State::Idle;
0927         Q_EMIT parseFinished(State::Idle);
0928         d->sharedMemDb->withRWLock([&] {
0929             d->sharedMemDb->clear();
0930         });
0931         d->tool_p = nullptr;
0932 #ifdef CT_ENABLE_FORTRAN_SCANNER
0933         d->fortran_tool_p = nullptr;
0934 #endif
0935         return;
0936     }
0937 
0938     if (d->dialogState == State::RunAllPhysical) {
0939         // move on to RunAllLogical
0940         ui->errorText->setText(tr("Physical parsing done. Continuing with logical parse"));
0941         ui->errorText->show();
0942         Q_EMIT parseFinished(State::RunAllPhysical);
0943 
0944         QTime time(0, 0);
0945         time = time.addMSecs(d->parseTimer.elapsed());
0946 
0947         KNotification *notification = new KNotification("parserFinished");
0948         notification->setText(
0949             tr("Physical Parse finished with: %1<br/>Starting Logical Parse.").arg(time.toString("mm:ss.zzz")));
0950         notification->sendEvent();
0951         d->parseTimer.restart();
0952         initParse();
0953         return;
0954     }
0955 
0956     if (d->dialogState == State::RunPhysicalOnly) {
0957         Q_EMIT parseFinished(d->dialogState);
0958 
0959         QTime time(0, 0);
0960         time = time.addMSecs(d->parseTimer.elapsed());
0961         KNotification *notification = new KNotification("parserFinished");
0962         notification->setText(tr("Physical Parse finished with: %1.").arg(time.toString("mm:ss.zzz")));
0963         notification->sendEvent();
0964     } else if (d->dialogState == State::RunAllLogical) {
0965         QTime time(0, 0);
0966         time = time.addMSecs(d->parseTimer.elapsed());
0967 
0968         KNotification *notification = new KNotification("parserFinished");
0969         notification->setText(tr("Logical Parse finished with: %1.").arg(time.toString("mm:ss.zzz")));
0970         notification->sendEvent();
0971         Q_EMIT parseFinished(d->dialogState);
0972     }
0973     d->dialogState = State::Idle;
0974     d->sharedMemDb->withRWLock([&] {
0975         d->sharedMemDb->clear();
0976     });
0977     d->tool_p = nullptr;
0978 #ifdef CT_ENABLE_FORTRAN_SCANNER
0979     d->fortran_tool_p = nullptr;
0980 #endif
0981     d->parseTimer.invalidate();
0982 
0983     if (d->pluginManager) {
0984         soci::session db;
0985         std::string path = codebasePath().toStdString();
0986         db.open(*soci::factory_sqlite3(), path);
0987 
0988         auto& pm = (*d->pluginManager).get();
0989         auto runQueryOnDatabase = [&](std::string const& dbQuery) -> std::vector<std::vector<RawDBData>> {
0990             return lvtmdb::SociHelper::runSingleQuery(db, dbQuery);
0991         };
0992         pm.callHooksOnParseCompleted(runQueryOnDatabase);
0993     }
0994 
0995     close();
0996 }
0997 
0998 void ParseCodebaseDialog::processingFileNotification(const QString& path)
0999 {
1000     QFileInfo info(path);
1001 
1002     if (d->tool_p && d->tool_p->finalizingThreads()) {
1003         return;
1004     }
1005 
1006     ui->progressBar->setValue(++d->progress);
1007     ui->progressBarText->setText(info.baseName());
1008     Q_EMIT parseStep(d->dialogState, ui->progressBar->value(), ui->progressBar->maximum());
1009 }
1010 
1011 void ParseCodebaseDialog::aboutToCallClangNotification(int size)
1012 {
1013     d->progress = 0;
1014 
1015     ui->progressBar->setMaximum(size);
1016 }
1017 
1018 void ParseCodebaseDialog::receivedMessage(const QString& message, long threadId)
1019 {
1020     // index 0 - help message, 1 - tab widget.
1021     if (ui->stackedWidget->currentIndex() == 0) {
1022         ui->stackedWidget->setCurrentIndex(1);
1023     }
1024 
1025     auto it = d->threadIdToWidget.find(threadId);
1026     if (it == std::end(d->threadIdToWidget)) {
1027         const int nr = ui->tabWidget->count() + 1;
1028         auto *textView = new TextView(nr);
1029         d->threadIdToWidget[threadId] = textView;
1030 
1031         textView->setAcceptRichText(false);
1032         textView->setReadOnly(true);
1033         textView->appendText(message);
1034 
1035         const QString tabText = [this, nr] {
1036             switch (d->dialogState) {
1037             case State::RunPhysicalOnly:
1038                 [[fallthrough]];
1039             case State::RunAllPhysical:
1040                 return tr("Physical Analysis %1").arg(nr);
1041             case State::RunAllLogical:
1042                 return tr("Logical Analysis %1").arg(nr);
1043             default:
1044                 return tr("Unknown State %1").arg(nr);
1045             }
1046         }();
1047 
1048         ui->tabWidget->addTab(textView, tabText);
1049     }
1050     TextView *textView = d->threadIdToWidget[threadId];
1051     textView->appendText(message);
1052 }
1053 
1054 std::filesystem::path ParseCodebaseDialog::buildPath() const
1055 {
1056     return ui->compileCommandsFolder->text().toStdString();
1057 }
1058 
1059 std::filesystem::path ParseCodebaseDialog::sourcePath() const
1060 {
1061     return ui->sourceFolder->text().toStdString();
1062 }
1063 
1064 void ParseCodebaseDialog::removeParseMessageTabs()
1065 {
1066     for (int i = 0; i < ui->tabWidget->count(); i++) {
1067         ui->tabWidget->removeTab(0);
1068     }
1069     ui->stackedWidget->setCurrentIndex(0);
1070     for (auto [_, view] : d->threadIdToWidget) {
1071         delete view;
1072     }
1073     d->threadIdToWidget.clear();
1074 }
1075 
1076 std::vector<std::string> ParseCodebaseDialog::ignoredItemsAsStdVec()
1077 {
1078 #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
1079     auto splitBehavior = QString::SkipEmptyParts;
1080 #else
1081     auto splitBehavior = Qt::SkipEmptyParts;
1082 #endif
1083     QStringList ignoreItems = ui->ignorePattern->text().split(',', splitBehavior);
1084     std::vector<std::string> ignoreList;
1085     ignoreList.reserve(ignoreItems.size());
1086     std::transform(ignoreItems.begin(), ignoreItems.end(), std::back_inserter(ignoreList), [](const QString& qstr) {
1087         return qstr.toStdString();
1088     });
1089     return ignoreList;
1090 }
1091 
1092 std::vector<std::filesystem::path> ParseCodebaseDialog::nonLakosianDirsAsStdVec()
1093 {
1094 #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
1095     auto splitBehavior = QString::SkipEmptyParts;
1096 #else
1097     auto splitBehavior = Qt::SkipEmptyParts;
1098 #endif
1099     QStringList nonLakosianDirList = ui->nonLakosians->text().split(',', splitBehavior);
1100     std::vector<std::filesystem::path> nonLakosianDirs;
1101     nonLakosianDirs.reserve(nonLakosianDirList.size());
1102     std::transform(nonLakosianDirList.begin(),
1103                    nonLakosianDirList.end(),
1104                    std::back_inserter(nonLakosianDirs),
1105                    [](const QString& qstr) {
1106                        return qstr.toStdString();
1107                    });
1108     return nonLakosianDirs;
1109 }
1110 
1111 void ParseCodebaseDialog::setPluginManager(Codethink::lvtplg::PluginManager& pluginManager)
1112 {
1113     d->pluginManager = pluginManager;
1114 }