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 }