From ed15fd40c8b5f6f79f3f95843c6d804fb2dcbe43 Mon Sep 17 00:00:00 2001 From: Dany Thinnes Date: Thu, 13 Aug 2026 08:43:07 +0200 Subject: [PATCH] =?UTF-8?q?-=20Auslisten=20aller=20im=20Projekt=20definier?= =?UTF-8?q?ten=20Funktionen=20-=20Suche=20nach=20toten=20Funktionen=20im?= =?UTF-8?q?=20Projekt=20-=20Doppelklick=20auf=20Funktion=20=C3=B6ffnet=20d?= =?UTF-8?q?ie=20Datei=20mit=20der=20Deklaration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 4 + LICENSE | 2 +- PKGBUILD | 36 +++ PKGBUILD-git | 43 +++ src/core/MainWindow.cpp | 18 ++ src/core/MainWindow.h | 4 + src/editor/CMakeLists.txt | 12 + src/editor/CodeEditor.cpp | 39 +++ src/editor/CodeEditor.h | 6 + src/editor/DeadCodeAnalyzer.cpp | 181 ++++++++++++ src/editor/DeadCodeAnalyzer.h | 39 +++ src/editor/DeadCodeDialog.cpp | 461 ++++++++++++++++++++++++++++++ src/editor/DeadCodeDialog.h | 62 ++++ src/editor/EditorPanel.cpp | 43 +++ src/editor/EditorPanel.h | 18 +- src/editor/FunctionIndex.cpp | 83 ++++++ src/editor/FunctionIndex.h | 47 +++ src/editor/FunctionListDialog.cpp | 52 ++++ src/editor/FunctionListDialog.h | 30 ++ src/editor/FunctionListPanel.cpp | 332 +++++++++++++++++++++ src/editor/FunctionListPanel.h | 67 +++++ src/editor/FunctionScanner.cpp | 158 ++++++++++ src/editor/FunctionScanner.h | 47 +++ 23 files changed, 1778 insertions(+), 6 deletions(-) create mode 100644 PKGBUILD create mode 100644 PKGBUILD-git create mode 100644 src/editor/DeadCodeAnalyzer.cpp create mode 100644 src/editor/DeadCodeAnalyzer.h create mode 100644 src/editor/DeadCodeDialog.cpp create mode 100644 src/editor/DeadCodeDialog.h create mode 100644 src/editor/FunctionIndex.cpp create mode 100644 src/editor/FunctionIndex.h create mode 100644 src/editor/FunctionListDialog.cpp create mode 100644 src/editor/FunctionListDialog.h create mode 100644 src/editor/FunctionListPanel.cpp create mode 100644 src/editor/FunctionListPanel.h create mode 100644 src/editor/FunctionScanner.cpp create mode 100644 src/editor/FunctionScanner.h diff --git a/CMakeLists.txt b/CMakeLists.txt index eb10c64..2b8c544 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -95,6 +95,10 @@ install(TARGETS BareCode RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} ) +install(FILES LICENSE + DESTINATION ${CMAKE_INSTALL_DATADIR}/licenses/barecode +) + # ---- Linux (FreeDesktop) -------------------------------------------------- if(PLATFORM_LINUX) foreach(SIZE 16 32 48 64 128 256 512) diff --git a/LICENSE b/LICENSE index f657ddf..2fbdd76 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 Dany Thinnes – Projekt Hirnfrei +Copyright (c) 2025 Dany Thinnes / Projekt Hirnfrei Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/PKGBUILD b/PKGBUILD new file mode 100644 index 0000000..5ad3a07 --- /dev/null +++ b/PKGBUILD @@ -0,0 +1,36 @@ +# Maintainer: Dany Thinnes +# Projekt Hirnfrei - https://www.projekt-hirnfrei.de + +pkgname=barecode +pkgver=1.1.0 +pkgrel=1 +pkgdesc="Schlanker modularer Code-Editor für HTML, PHP, CSS und C++" +arch=('x86_64' 'aarch64') +url="https://www.projekt-hirnfrei.de" +license=('MIT') +depends=('qt6-base') +makedepends=('cmake' 'ninja') +provides=('barecode') +conflicts=('barecode-git') +source=("$pkgname-$pkgver.tar.gz::https://git.projekt-hirnfrei.de/diabolus/BareCode/archive/v$pkgver.tar.gz") +sha256sums=('SKIP') # Nach erstem Download ersetzen: sha256sum barecode-1.1.0.tar.gz + +build() { + cd "BareCode" + cmake -B build \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=/usr \ + -DCMAKE_SKIP_RPATH=ON + cmake --build build +} + +check() { + : +} + +package() { + cd "BareCode" + DESTDIR="$pkgdir" cmake --install build + install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" +} diff --git a/PKGBUILD-git b/PKGBUILD-git new file mode 100644 index 0000000..566f386 --- /dev/null +++ b/PKGBUILD-git @@ -0,0 +1,43 @@ +# Maintainer: Dany Thinnes +# Projekt Hirnfrei - https://www.projekt-hirnfrei.de + +pkgname=barecode-git +pkgver=1.1.0.r0.g0000000 +pkgrel=1 +pkgdesc="Schlanker modularer Code-Editor für HTML, PHP, CSS und C++ (Git-Version)" +arch=('x86_64' 'aarch64') +url="https://www.projekt-hirnfrei.de" +license=('MIT') +depends=('qt6-base') +makedepends=('cmake' 'ninja' 'git') +provides=('barecode') +conflicts=('barecode') +source=("$pkgname::git+https://git.projekt-hirnfrei.de/diabolus/BareCode.git") +sha256sums=('SKIP') + +pkgver() { + cd "$pkgname" + git describe --long --tags 2>/dev/null \ + | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g' \ + || printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)" +} + +build() { + cd "$pkgname" + cmake -B build \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=/usr \ + -DCMAKE_SKIP_RPATH=ON + cmake --build build +} + +check() { + : +} + +package() { + cd "$pkgname" + DESTDIR="$pkgdir" cmake --install build + install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" +} diff --git a/src/core/MainWindow.cpp b/src/core/MainWindow.cpp index 37d588f..2ec042b 100644 --- a/src/core/MainWindow.cpp +++ b/src/core/MainWindow.cpp @@ -119,6 +119,14 @@ void MainWindow::setupMenuBar() m_actFileSearch = mBearbeiten->addAction(tr("In &Dateien suchen…"), this, &MainWindow::onShowFileSearch); m_actFileSearch->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_F); + mBearbeiten->addSeparator(); + + m_actFuncList = mBearbeiten->addAction(tr("&Projektfunktionen…"), this, &MainWindow::onShowFunctionList); + m_actFuncList->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_P); + + m_actDeadCode = mBearbeiten->addAction(tr("&Tote Funktionen suchen…"), this, &MainWindow::onShowDeadCode); + m_actDeadCode->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_T); + // ---- Ansicht ---- QMenu *mAnsicht = menuBar()->addMenu(tr("&Ansicht")); @@ -281,6 +289,16 @@ void MainWindow::onAbout() // --------------------------------------------------------------------------- // Slots – Projekt // --------------------------------------------------------------------------- +void MainWindow::onShowDeadCode() +{ + m_editor->showDeadCode(); +} + +void MainWindow::onShowFunctionList() +{ + m_editor->showFunctionList(); +} + void MainWindow::onShowFileSearch() { m_editor->showFileSearchPanel(); diff --git a/src/core/MainWindow.h b/src/core/MainWindow.h index 3ae9b17..26a4241 100644 --- a/src/core/MainWindow.h +++ b/src/core/MainWindow.h @@ -42,6 +42,8 @@ private slots: void onRedo(); void onShowSearch(); void onShowFileSearch(); + void onShowFunctionList(); + void onShowDeadCode(); // Ansicht void onToggleDarkMode(bool checked); // Hilfe @@ -82,6 +84,8 @@ private: QAction *m_actRedo = nullptr; QAction *m_actSearch = nullptr; QAction *m_actFileSearch = nullptr; + QAction *m_actFuncList = nullptr; + QAction *m_actDeadCode = nullptr; QAction *m_actDarkMode = nullptr; QAction *m_actAbout = nullptr; }; diff --git a/src/editor/CMakeLists.txt b/src/editor/CMakeLists.txt index fe64f27..b889116 100644 --- a/src/editor/CMakeLists.txt +++ b/src/editor/CMakeLists.txt @@ -17,6 +17,18 @@ set(EDITOR_SOURCES SignatureHelper.h SignatureTooltip.cpp SignatureTooltip.h + FunctionScanner.cpp + FunctionScanner.h + FunctionIndex.cpp + FunctionIndex.h + FunctionListPanel.cpp + FunctionListPanel.h + FunctionListDialog.cpp + FunctionListDialog.h + DeadCodeAnalyzer.cpp + DeadCodeAnalyzer.h + DeadCodeDialog.cpp + DeadCodeDialog.h ) add_library(BareCode_Editor STATIC ${EDITOR_SOURCES}) diff --git a/src/editor/CodeEditor.cpp b/src/editor/CodeEditor.cpp index f6aca9b..3f2701c 100644 --- a/src/editor/CodeEditor.cpp +++ b/src/editor/CodeEditor.cpp @@ -2,6 +2,7 @@ #include "LineNumberArea.h" #include "ColorIndicator.h" #include "SignatureHelper.h" +#include "FunctionIndex.h" #include "core/Settings.h" #include "highlighter/HighlighterFactory.h" @@ -289,6 +290,44 @@ void CodeEditor::paintEvent(QPaintEvent *event) } } +void CodeEditor::setFunctionIndex(FunctionIndex *index) +{ + m_functionIndex = index; +} + +void CodeEditor::mouseDoubleClickEvent(QMouseEvent *event) +{ + // Zuerst normales Verhalten — markiert das Wort unter dem Cursor + QPlainTextEdit::mouseDoubleClickEvent(event); + + if (!m_functionIndex || !m_functionIndex->isReady()) + { + return; + } + + // Markiertes Wort auslesen + const QString word = textCursor().selectedText().trimmed(); + if (word.isEmpty() || word.contains(' ')) + { + return; + } + + // Im Funktionsindex nachschlagen + const FunctionScanner::FunctionInfo info = m_functionIndex->lookup(word); + if (info.filePath.isEmpty()) + { + return; // Nicht gefunden — normales Verhalten bleibt + } + + // Nicht zur eigenen Definition springen wenn wir bereits dort sind + if (info.filePath == m_filePath && info.line == textCursor().blockNumber() + 1) + { + return; + } + + emit navigateToRequested(info.filePath, info.line); +} + void CodeEditor::mousePressEvent(QMouseEvent *event) { // Zuerst prüfen ob ein Farbquadrat geklickt wurde diff --git a/src/editor/CodeEditor.h b/src/editor/CodeEditor.h index 21581fe..24f94aa 100644 --- a/src/editor/CodeEditor.h +++ b/src/editor/CodeEditor.h @@ -10,6 +10,7 @@ class Settings; class SyntaxHighlighter; class ColorIndicator; class SignatureHelper; +class FunctionIndex; // --------------------------------------------------------------------------- // CodeEditor – Core editing widget. @@ -48,14 +49,18 @@ public: QRectF blockBoundingGeometryPublic(const QTextBlock &b) const { return blockBoundingGeometry(b); } QPointF contentOffsetPublic() const { return contentOffset(); } + void setFunctionIndex(FunctionIndex *index); + signals: void fileSaved(const QString &filePath); + void navigateToRequested(const QString &filePath, int line); protected: void resizeEvent(QResizeEvent *event) override; void keyPressEvent(QKeyEvent *event) override; void paintEvent(QPaintEvent *event) override; void mousePressEvent(QMouseEvent *event) override; + void mouseDoubleClickEvent(QMouseEvent *event) override; private slots: void updateLineNumberAreaWidth(int newBlockCount); @@ -72,5 +77,6 @@ private: SyntaxHighlighter *m_highlighter = nullptr; ColorIndicator *m_colorIndicator = nullptr; SignatureHelper *m_signatureHelper = nullptr; + FunctionIndex *m_functionIndex = nullptr; QString m_filePath; }; diff --git a/src/editor/DeadCodeAnalyzer.cpp b/src/editor/DeadCodeAnalyzer.cpp new file mode 100644 index 0000000..3a930d6 --- /dev/null +++ b/src/editor/DeadCodeAnalyzer.cpp @@ -0,0 +1,181 @@ +#include "DeadCodeAnalyzer.h" + +#include +#include +#include +#include +#include +#include +#include + +DeadCodeAnalyzer::DeadCodeAnalyzer(QObject *parent) + : QObject(parent) +{ + m_scanner = new FunctionScanner(this); +} + +QList DeadCodeAnalyzer::analyze( + const QString &projectRoot, + const QStringList &extensions, + const QStringList &excludeDirs) const +{ + // ----------------------------------------------------------------------- + // Schritt 1: Alle Dateien sammeln (mit Exclude-Filter) + // ----------------------------------------------------------------------- + QStringList allFiles; + QDirIterator it(projectRoot, extensions, QDir::Files, + QDirIterator::Subdirectories); + + while (it.hasNext()) + { + const QString path = it.next(); + + // Exclude-Verzeichnisse prüfen + bool excluded = false; + if (!excludeDirs.isEmpty()) + { + const QStringList parts = path.split('/'); + for (const QString &excl : excludeDirs) + { + if (parts.contains(excl.trimmed(), Qt::CaseInsensitive)) + { + excluded = true; + break; + } + } + } + + if (!excluded) + { + allFiles.append(path); + } + } + + const int totalFiles = allFiles.size(); + + // ----------------------------------------------------------------------- + // Schritt 2: Alle Funktionsdefinitionen aus nicht-excluded Dateien + // ----------------------------------------------------------------------- + QHash definitions; + + for (const QString &path : allFiles) + { + const QList funcs = m_scanner->scanFile(path); + for (const FunctionScanner::FunctionInfo &func : funcs) + { + if (func.name.startsWith("__")) + { + continue; + } + const QString key = func.name.toLower(); + if (!definitions.contains(key)) + { + definitions.insert(key, func); + } + } + } + + if (definitions.isEmpty()) + { + return {}; + } + + // ----------------------------------------------------------------------- + // Schritt 3: Alle Dateien EINMAL lesen, alle Aufrufe sammeln + // ----------------------------------------------------------------------- + QSet calledFunctions; + + static const QRegularExpression identifierRegex( + R"((?:->|::|\b)([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\s*\()" + R"(|['"]([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)['"])" + ); + + static const QRegularExpression defLineRegex( + R"(\bfunction\s+([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*))" + ); + + int filesScanned = 0; + + for (const QString &filePath : allFiles) + { + ++filesScanned; + + // Fortschritt signalisieren + emit progressUpdate(filesScanned, totalFiles, + QFileInfo(filePath).fileName()); + + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + continue; + } + + QTextStream stream(&file); + stream.setEncoding(QStringConverter::Utf8); + + while (!stream.atEnd()) + { + const QString line = stream.readLine(); + const QString trimmed = line.trimmed(); + + if (trimmed.startsWith("//") || + trimmed.startsWith("*") || + trimmed.startsWith("#") || + trimmed.startsWith("/*")) + { + continue; + } + + // Definitionen auf dieser Zeile nicht als Aufruf werten + QSet definedOnThisLine; + { + QRegularExpressionMatchIterator dit = defLineRegex.globalMatch(line); + while (dit.hasNext()) + { + definedOnThisLine.insert(dit.next().captured(1).toLower()); + } + } + + QRegularExpressionMatchIterator mit = identifierRegex.globalMatch(line); + while (mit.hasNext()) + { + const QRegularExpressionMatch match = mit.next(); + const QString name = match.captured(1).isEmpty() + ? match.captured(2).toLower() + : match.captured(1).toLower(); + + if (name.isEmpty() || definedOnThisLine.contains(name)) + { + continue; + } + + if (definitions.contains(name)) + { + calledFunctions.insert(name); + } + } + } + } + + // ----------------------------------------------------------------------- + // Schritt 4: Nicht aufgerufene Funktionen zurückgeben + // ----------------------------------------------------------------------- + QList dead; + + for (auto it2 = definitions.begin(); it2 != definitions.end(); ++it2) + { + if (!calledFunctions.contains(it2.key())) + { + const FunctionScanner::FunctionInfo &func = it2.value(); + DeadFunction d; + d.info = func; + d.reason = func.className.isEmpty() + ? QObject::tr("Globale Funktion — kein Aufruf gefunden") + : QObject::tr("Methode von %1 — kein Aufruf gefunden") + .arg(func.className); + dead.append(d); + } + } + + return dead; +} diff --git a/src/editor/DeadCodeAnalyzer.h b/src/editor/DeadCodeAnalyzer.h new file mode 100644 index 0000000..0431fde --- /dev/null +++ b/src/editor/DeadCodeAnalyzer.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include + +#include "FunctionScanner.h" + +// --------------------------------------------------------------------------- +// DeadCodeAnalyzer – Findet Funktionen die definiert aber nie aufgerufen werden. +// --------------------------------------------------------------------------- +class DeadCodeAnalyzer : public QObject +{ + Q_OBJECT + +public: + struct DeadFunction + { + FunctionScanner::FunctionInfo info; + QString reason; + }; + + explicit DeadCodeAnalyzer(QObject *parent = nullptr); + + // excludeDirs: Verzeichnisnamen die komplett übersprungen werden + // z.B. {"vendor", "lib", "node_modules"} + QList analyze(const QString &projectRoot, + const QStringList &extensions = {"*.php"}, + const QStringList &excludeDirs = {}) const; + +signals: + // Fortschritt während der Analyse (läuft im Thread) + void progressUpdate(int filesScanned, int filesTotal, + const QString ¤tFile) const; + +private: + FunctionScanner *m_scanner = nullptr; +}; diff --git a/src/editor/DeadCodeDialog.cpp b/src/editor/DeadCodeDialog.cpp new file mode 100644 index 0000000..9669c85 --- /dev/null +++ b/src/editor/DeadCodeDialog.cpp @@ -0,0 +1,461 @@ +#include "DeadCodeDialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +DeadCodeDialog::DeadCodeDialog(QWidget *parent) + : QDialog(parent, Qt::Window) +{ + setWindowTitle(tr("Tote Funktionen – BareCode")); + setMinimumSize(650, 500); + + QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode"); + if (s.contains("deadcode/geometry")) + { + restoreGeometry(s.value("deadcode/geometry").toByteArray()); + } + else + { + resize(780, 620); + } + + m_analyzer = new DeadCodeAnalyzer(this); + m_watcher = new QFutureWatcher>(this); + + connect(m_watcher, &QFutureWatcher>::finished, + this, &DeadCodeDialog::onAnalysisFinished); + + // Fortschritts-Signal aus dem Analyzer-Thread sicher in den UI-Thread leiten + connect(m_analyzer, &DeadCodeAnalyzer::progressUpdate, + this, &DeadCodeDialog::onProgressUpdate, + Qt::QueuedConnection); + + setupUi(); +} + +// --------------------------------------------------------------------------- +// UI +// --------------------------------------------------------------------------- +void DeadCodeDialog::setupUi() +{ + QVBoxLayout *root = new QVBoxLayout(this); + root->setSpacing(8); + root->setContentsMargins(12, 12, 12, 12); + + // ---- Warnbox ---- + QFrame *warningBox = new QFrame(this); + warningBox->setStyleSheet( + "QFrame {" + " background: #3a2a00;" + " border: 1px solid #7a5a00;" + " border-radius: 4px;" + "}" + ); + QHBoxLayout *warnLayout = new QHBoxLayout(warningBox); + warnLayout->setContentsMargins(8, 6, 8, 6); + + QLabel *warnIcon = new QLabel(warningBox); + warnIcon->setFixedSize(20, 20); + warnIcon->setStyleSheet( + "background: #ffcc00;" + "color: #1a1a00;" + "font-weight: bold;" + "font-size: 13px;" + "border-radius: 10px;" + ); + warnIcon->setText("!"); + warnIcon->setAlignment(Qt::AlignCenter); + warnLayout->addWidget(warnIcon); + + QLabel *warnText = new QLabel( + tr("Kandidatenliste — kein Aufruf per Regex gefunden. " + "Dynamische Aufrufe (call_user_func, Strings, Hooks) werden nicht erkannt. " + "Bitte vor dem Löschen manuell prüfen."), + warningBox + ); + warnText->setWordWrap(true); + warnText->setStyleSheet("color: #ffcc88;"); + warnLayout->addWidget(warnText, 1); + root->addWidget(warningBox); + + // ---- Exclude-Verzeichnisse ---- + QHBoxLayout *excludeRow = new QHBoxLayout(); + excludeRow->addWidget(new QLabel(tr("Ausschließen:"), this)); + + m_excludeEdit = new QLineEdit(this); + m_excludeEdit->setPlaceholderText(tr("vendor lib node_modules (leerzeichen-getrennt)")); + m_excludeEdit->setToolTip(tr( + "Verzeichnisnamen die bei der Analyse übersprungen werden.\n" + "Leerzeichen-getrennt, z.B.: vendor lib node_modules cache" + )); + + // Letzte Einstellung wiederherstellen + QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode"); + m_excludeEdit->setText(s.value("deadcode/excludeDirs", "vendor lib node_modules").toString()); + + excludeRow->addWidget(m_excludeEdit, 1); + + // Verzeichnis-Auswahl Button + QPushButton *btnBrowse = new QPushButton(tr("+ Verzeichnis"), this); + btnBrowse->setToolTip(tr("Verzeichnis aus dem Projekt auswählen und zur Ausschlussliste hinzufügen")); + connect(btnBrowse, &QPushButton::clicked, this, [this]() + { + const QString startPath = m_projectRoot.isEmpty() + ? QDir::homePath() + : m_projectRoot; + + const QString dir = QFileDialog::getExistingDirectory( + this, + tr("Verzeichnis ausschließen"), + startPath, + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks + ); + + if (dir.isEmpty()) + { + return; + } + + // Nur den letzten Verzeichnisnamen verwenden, nicht den vollen Pfad + // So funktioniert der Exclude auch für gleichnamige Unterverzeichnisse + const QString dirName = QFileInfo(dir).fileName(); + + // Prüfen ob bereits in der Liste + const QStringList current = m_excludeEdit->text() + .simplified() + .split(' ', Qt::SkipEmptyParts); + if (current.contains(dirName, Qt::CaseInsensitive)) + { + return; + } + + // Anhängen + QString newText = m_excludeEdit->text().trimmed(); + if (!newText.isEmpty()) + { + newText += ' '; + } + newText += dirName; + m_excludeEdit->setText(newText); + }); + excludeRow->addWidget(btnBrowse); + + // Löschen-Button — ganzes Feld leeren + QPushButton *btnClear = new QPushButton(tr("✕"), this); + btnClear->setFixedWidth(28); + btnClear->setFlat(true); + btnClear->setToolTip(tr("Ausschlussliste leeren")); + connect(btnClear, &QPushButton::clicked, this, [this]() + { + m_excludeEdit->clear(); + }); + excludeRow->addWidget(btnClear); + + root->addLayout(excludeRow); + + // ---- Steuerleiste ---- + QHBoxLayout *ctrlRow = new QHBoxLayout(); + + m_btnAnalyze = new QPushButton(tr("Analyse starten"), this); + ctrlRow->addWidget(m_btnAnalyze); + + m_btnExport = new QPushButton(tr("Als TXT exportieren"), this); + m_btnExport->setEnabled(false); + ctrlRow->addWidget(m_btnExport); + + ctrlRow->addStretch(); + + m_statusLabel = new QLabel(this); + m_statusLabel->setStyleSheet("color: palette(mid);"); + ctrlRow->addWidget(m_statusLabel); + + root->addLayout(ctrlRow); + + // ---- Fortschrittsbereich ---- + m_progress = new QProgressBar(this); + m_progress->setRange(0, 100); + m_progress->setTextVisible(true); + m_progress->setFormat("%v / %m Dateien"); + m_progress->hide(); + root->addWidget(m_progress); + + m_progressLabel = new QLabel(this); + m_progressLabel->setStyleSheet("color: palette(mid); font-size: 11px;"); + m_progressLabel->hide(); + root->addWidget(m_progressLabel); + + // ---- Ergebnistabelle ---- + m_results = new QTreeWidget(this); + m_results->setColumnCount(4); + m_results->setHeaderLabels({ + tr("Funktion"), + tr("Klasse"), + tr("Datei"), + tr("Zeile") + }); + m_results->setRootIsDecorated(false); + m_results->setAlternatingRowColors(true); + m_results->setSortingEnabled(true); + m_results->sortByColumn(0, Qt::AscendingOrder); + m_results->setSelectionMode(QAbstractItemView::SingleSelection); + + m_results->header()->setSectionResizeMode(0, QHeaderView::Interactive); + m_results->header()->setSectionResizeMode(1, QHeaderView::Interactive); + m_results->header()->setSectionResizeMode(2, QHeaderView::Stretch); + m_results->header()->setSectionResizeMode(3, QHeaderView::Fixed); + m_results->header()->resizeSection(0, 200); + m_results->header()->resizeSection(1, 120); + m_results->header()->resizeSection(3, 60); + + QFont mono("Monospace"); + mono.setStyleHint(QFont::Monospace); + m_results->setFont(mono); + + root->addWidget(m_results, 1); + + // ---- Verbindungen ---- + connect(m_btnAnalyze, &QPushButton::clicked, this, &DeadCodeDialog::onAnalyze); + connect(m_btnExport, &QPushButton::clicked, this, &DeadCodeDialog::onExportClicked); + connect(m_results, &QTreeWidget::itemActivated, + this, &DeadCodeDialog::onItemActivated); +} + +// --------------------------------------------------------------------------- +// Öffentliche Schnittstelle +// --------------------------------------------------------------------------- +void DeadCodeDialog::setProjectRoot(const QString &path) +{ + // Alte Exclude-Liste für das vorherige Projekt speichern + if (!m_projectRoot.isEmpty()) + { + QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode"); + const QString oldKey = "deadcode/exclude/" + + QString(m_projectRoot).replace('/', '_').replace('\\', '_'); + s.setValue(oldKey, m_excludeEdit->text()); + } + + m_projectRoot = path; + m_results->clear(); + m_statusLabel->setText(QString()); + m_btnExport->setEnabled(false); + + // Exclude-Liste für das neue Projekt laden + if (!path.isEmpty()) + { + QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode"); + const QString key = "deadcode/exclude/" + + QString(path).replace('/', '_').replace('\\', '_'); + + // Projektspezifisch vorhanden? Sonst globalen Fallback nehmen + const QString saved = s.value(key, + s.value("deadcode/excludeDirs", "vendor lib node_modules").toString() + ).toString(); + + m_excludeEdit->setText(saved); + } +} + +// --------------------------------------------------------------------------- +// Analyse starten +// --------------------------------------------------------------------------- +void DeadCodeDialog::onAnalyze() +{ + if (m_projectRoot.isEmpty()) + { + QMessageBox::information(this, tr("Analyse"), + tr("Bitte zuerst ein Projekt öffnen.")); + return; + } + + if (m_watcher->isRunning()) + { + m_watcher->cancel(); + m_watcher->waitForFinished(); + } + + // Exclude-Liste projektspezifisch speichern + QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode"); + const QString key = "deadcode/exclude/" + + QString(m_projectRoot).replace('/', '_').replace('\\', '_'); + s.setValue(key, m_excludeEdit->text()); + s.setValue("deadcode/excludeDirs", m_excludeEdit->text()); // globaler Fallback + + const QStringList excludeDirs = m_excludeEdit->text() + .simplified() + .split(' ', Qt::SkipEmptyParts); + + m_results->clear(); + m_btnAnalyze->setEnabled(false); + m_btnExport->setEnabled(false); + m_progress->setValue(0); + m_progress->setMaximum(0); // Unbestimmter Modus bis wir die Dateizahl kennen + m_progress->show(); + m_progressLabel->setText(tr("Dateien werden gezählt…")); + m_progressLabel->show(); + m_statusLabel->setText(tr("Analysiere…")); + + const QString root = m_projectRoot; + DeadCodeAnalyzer *analyzer = m_analyzer; + + QFuture> future = + QtConcurrent::run([analyzer, root, excludeDirs]() + { + return analyzer->analyze(root, {"*.php"}, excludeDirs); + }); + + m_watcher->setFuture(future); +} + +// --------------------------------------------------------------------------- +// Fortschritt aktualisieren (QueuedConnection — Thread-sicher) +// --------------------------------------------------------------------------- +void DeadCodeDialog::onProgressUpdate(int filesScanned, int filesTotal, + const QString ¤tFile) +{ + if (m_progress->maximum() != filesTotal) + { + m_progress->setMaximum(filesTotal); + } + m_progress->setValue(filesScanned); + m_progressLabel->setText(tr("(%1 / %2) %3") + .arg(filesScanned) + .arg(filesTotal) + .arg(currentFile)); +} + +// --------------------------------------------------------------------------- +// Analyse abgeschlossen +// --------------------------------------------------------------------------- +void DeadCodeDialog::onAnalysisFinished() +{ + m_progress->hide(); + m_progressLabel->hide(); + m_btnAnalyze->setEnabled(true); + + if (m_watcher->isCanceled()) + { + return; + } + + const QList dead = m_watcher->result(); + + m_results->setSortingEnabled(false); + + for (const DeadCodeAnalyzer::DeadFunction &d : dead) + { + QTreeWidgetItem *item = new QTreeWidgetItem(m_results); + item->setText(0, d.info.signature); + item->setText(1, d.info.className.isEmpty() ? tr("(global)") : d.info.className); + item->setText(2, QFileInfo(d.info.filePath).fileName()); + item->setText(3, QString::number(d.info.line)); + item->setToolTip(0, d.reason); + item->setToolTip(2, d.info.filePath); + item->setData(0, Qt::UserRole, d.info.filePath); + item->setData(0, Qt::UserRole + 1, d.info.line); + + if (d.info.className.isEmpty()) + { + item->setForeground(1, QColor("#888888")); + } + } + + m_results->setSortingEnabled(true); + m_results->sortByColumn(0, Qt::AscendingOrder); + + if (dead.isEmpty()) + { + m_statusLabel->setText(tr("✓ Keine ungenutzten Funktionen gefunden.")); + m_statusLabel->setStyleSheet("color: #44bb44;"); + } + else + { + m_statusLabel->setText(tr("%1 Kandidaten gefunden").arg(dead.size())); + m_statusLabel->setStyleSheet("color: palette(mid);"); + m_btnExport->setEnabled(true); + } +} + +// --------------------------------------------------------------------------- +// Klick auf Treffer +// --------------------------------------------------------------------------- +void DeadCodeDialog::onItemActivated(QTreeWidgetItem *item, int /*column*/) +{ + const QString path = item->data(0, Qt::UserRole).toString(); + const int line = item->data(0, Qt::UserRole + 1).toInt(); + if (!path.isEmpty() && line > 0) + { + emit fileLineRequested(path, line); + } +} + +// --------------------------------------------------------------------------- +// Export +// --------------------------------------------------------------------------- +void DeadCodeDialog::onExportClicked() +{ + const QString path = QFileDialog::getSaveFileName( + this, + tr("Ergebnis exportieren"), + QString("barecode_deadcode_%1.txt") + .arg(QDateTime::currentDateTime().toString("yyyyMMdd_HHmm")), + tr("Textdateien (*.txt)") + ); + if (path.isEmpty()) { return; } + + QFile file(path); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) + { + QMessageBox::warning(this, tr("Export"), + tr("Datei konnte nicht geschrieben werden:\n%1").arg(path)); + return; + } + + QTextStream out(&file); + out.setEncoding(QStringConverter::Utf8); + + out << "BareCode – Analyse ungenutzter Funktionen\n"; + out << "Projekt: " << m_projectRoot << "\n"; + out << "Ausgeschlossen: " << m_excludeEdit->text() << "\n"; + out << "Datum: " << QDateTime::currentDateTime().toString("dd.MM.yyyy HH:mm") << "\n"; + out << QString("─").repeated(70) << "\n\n"; + + for (int i = 0; i < m_results->topLevelItemCount(); ++i) + { + QTreeWidgetItem *item = m_results->topLevelItem(i); + out << item->text(0) << "\n"; + out << " Klasse: " << item->text(1) << "\n"; + out << " Datei: " << item->toolTip(2) << "\n"; + out << " Zeile: " << item->text(3) << "\n\n"; + } + + out << QString("─").repeated(70) << "\n"; + out << m_results->topLevelItemCount() << " Kandidaten\n"; + out << "Hinweis: Dynamische Aufrufe werden nicht erkannt.\n"; + + QMessageBox::information(this, tr("Export"), + tr("Exportiert nach:\n%1").arg(path)); +} + +// --------------------------------------------------------------------------- +// Fenstergeometrie speichern +// --------------------------------------------------------------------------- +void DeadCodeDialog::closeEvent(QCloseEvent *event) +{ + if (m_watcher->isRunning()) + { + m_watcher->cancel(); + } + QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode"); + s.setValue("deadcode/geometry", saveGeometry()); + event->accept(); +} diff --git a/src/editor/DeadCodeDialog.h b/src/editor/DeadCodeDialog.h new file mode 100644 index 0000000..f7a4028 --- /dev/null +++ b/src/editor/DeadCodeDialog.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "DeadCodeAnalyzer.h" + +class DeadCodeDialog : public QDialog +{ + Q_OBJECT + +public: + explicit DeadCodeDialog(QWidget *parent = nullptr); + + void setProjectRoot(const QString &path); + +signals: + void fileLineRequested(const QString &filePath, int line); + +private slots: + void onAnalyze(); + void onAnalysisFinished(); + void onProgressUpdate(int filesScanned, int filesTotal, + const QString ¤tFile); + void onItemActivated(QTreeWidgetItem *item, int column); + void onExportClicked(); + +protected: + void closeEvent(QCloseEvent *event) override; + +private: + void setupUi(); + + QString m_projectRoot; + + // Einstellungen + QLineEdit *m_excludeEdit = nullptr; + + // Steuerung + QPushButton *m_btnAnalyze = nullptr; + QPushButton *m_btnExport = nullptr; + + // Fortschritt + QProgressBar *m_progress = nullptr; + QLabel *m_progressLabel = nullptr; + + // Status + Ergebnisse + QLabel *m_statusLabel = nullptr; + QTreeWidget *m_results = nullptr; + + DeadCodeAnalyzer *m_analyzer = nullptr; + QFutureWatcher> *m_watcher = nullptr; +}; diff --git a/src/editor/EditorPanel.cpp b/src/editor/EditorPanel.cpp index e2c424c..92766b3 100644 --- a/src/editor/EditorPanel.cpp +++ b/src/editor/EditorPanel.cpp @@ -3,6 +3,9 @@ #include "CodeEditor.h" #include "SearchPanel.h" #include "FileSearchPanel.h" +#include "FunctionListDialog.h" +#include "DeadCodeDialog.h" +#include "FunctionIndex.h" #include #include @@ -32,6 +35,9 @@ void EditorPanel::setupUi() m_searchPanel = new SearchPanel(this); m_fileSearch = new FileSearchPanel(this); + m_funcDialog = new FunctionListDialog(window()); + m_deadCode = new DeadCodeDialog(window()); + m_funcIndex = new FunctionIndex(this); m_layout->addWidget(m_tabWidget, 1); m_layout->addWidget(m_searchPanel, 0); @@ -45,6 +51,12 @@ void EditorPanel::setupUi() connect(m_fileSearch, &FileSearchPanel::fileLineRequested, this, &EditorPanel::goToLine); + + connect(m_funcDialog, &FunctionListDialog::fileLineRequested, + this, &EditorPanel::goToLine); + + connect(m_deadCode, &DeadCodeDialog::fileLineRequested, + this, &EditorPanel::goToLine); } // --------------------------------------------------------------------------- @@ -79,6 +91,13 @@ void EditorPanel::openFile(const QString &filePath) m_tabWidget->setTabToolTip(index, filePath); m_openTabs.insert(filePath, tab); + // FunctionIndex dem Editor mitgeben für Doppelklick-Navigation + tab->editor()->setFunctionIndex(m_funcIndex); + + // Doppelklick auf Funktion → zur Definition springen + connect(tab->editor(), &CodeEditor::navigateToRequested, + this, &EditorPanel::goToLine); + // Änderungsindikator im Tab-Titel (● = ungespeichert) connect(tab->editor()->document(), &QTextDocument::modificationChanged, this, [this, tab](bool modified) @@ -102,6 +121,13 @@ void EditorPanel::openFile(const QString &filePath) m_tabWidget->setTabToolTip(idx, savedPath); } emit currentFileSaved(savedPath); + + // Index und Funktionsliste aktualisieren + m_funcIndex->refresh(); + if (m_funcDialog->isVisible()) + { + m_funcDialog->refresh(); + } }); } @@ -145,9 +171,26 @@ void EditorPanel::showFileSearchPanel() m_fileSearch->activate(); } +void EditorPanel::showFunctionList() +{ + m_funcDialog->show(); + m_funcDialog->raise(); + m_funcDialog->activateWindow(); +} + +void EditorPanel::showDeadCode() +{ + m_deadCode->show(); + m_deadCode->raise(); + m_deadCode->activateWindow(); +} + void EditorPanel::setSearchRoot(const QString &path) { m_fileSearch->setSearchRoot(path); + m_funcDialog->setProjectRoot(path); + m_deadCode->setProjectRoot(path); + m_funcIndex->setProjectRoot(path); } void EditorPanel::goToLine(const QString &filePath, int line) diff --git a/src/editor/EditorPanel.h b/src/editor/EditorPanel.h index 5c789ac..e90c518 100644 --- a/src/editor/EditorPanel.h +++ b/src/editor/EditorPanel.h @@ -10,6 +10,9 @@ class EditorTab; class Settings; class SearchPanel; class FileSearchPanel; +class FunctionListDialog; +class DeadCodeDialog; +class FunctionIndex; // --------------------------------------------------------------------------- // EditorPanel – Rechtes Panel: Tab-Leiste + Editoren + Such/Ersetzen-Panel. @@ -29,6 +32,8 @@ public slots: void setSearchRoot(const QString &path); void showSearchPanel(); void showFileSearchPanel(); + void showFunctionList(); + void showDeadCode(); void goToLine(const QString &filePath, int line); // Session @@ -50,11 +55,14 @@ private: int findTabForFile(const QString &filePath) const; EditorTab *currentTab() const; - Settings *m_settings = nullptr; - QVBoxLayout *m_layout = nullptr; - QTabWidget *m_tabWidget = nullptr; - SearchPanel *m_searchPanel = nullptr; - FileSearchPanel *m_fileSearch = nullptr; + Settings *m_settings = nullptr; + QVBoxLayout *m_layout = nullptr; + QTabWidget *m_tabWidget = nullptr; + SearchPanel *m_searchPanel = nullptr; + FileSearchPanel *m_fileSearch = nullptr; + FunctionListDialog *m_funcDialog = nullptr; + DeadCodeDialog *m_deadCode = nullptr; + FunctionIndex *m_funcIndex = nullptr; QHash m_openTabs; }; diff --git a/src/editor/FunctionIndex.cpp b/src/editor/FunctionIndex.cpp new file mode 100644 index 0000000..a2ec7ce --- /dev/null +++ b/src/editor/FunctionIndex.cpp @@ -0,0 +1,83 @@ +#include "FunctionIndex.h" + +#include + +FunctionIndex::FunctionIndex(QObject *parent) + : QObject(parent) +{ + m_scanner = new FunctionScanner(this); + m_watcher = new QFutureWatcher>(this); + + connect(m_watcher, &QFutureWatcher>::finished, + this, &FunctionIndex::onScanFinished); +} + +void FunctionIndex::setProjectRoot(const QString &path) +{ + m_projectRoot = path; + m_ready = false; + m_index.clear(); + + if (!path.isEmpty()) + { + refresh(); + } +} + +void FunctionIndex::refresh() +{ + if (m_projectRoot.isEmpty()) + { + return; + } + + if (m_watcher->isRunning()) + { + m_watcher->cancel(); + m_watcher->waitForFinished(); + } + + FunctionScanner *scanner = m_scanner; + const QString root = m_projectRoot; + + m_watcher->setFuture( + QtConcurrent::run([scanner, root]() + { + return scanner->scanDirectory(root); + }) + ); +} + +void FunctionIndex::onScanFinished() +{ + if (m_watcher->isCanceled()) + { + return; + } + + m_index.clear(); + const QList all = m_watcher->result(); + + for (const FunctionScanner::FunctionInfo &func : all) + { + const QString key = func.name.toLower(); + // Ersten Eintrag bevorzugen (globale Funktionen vor Methoden) + if (!m_index.contains(key) || func.className.isEmpty()) + { + m_index.insert(key, func); + } + } + + m_ready = true; + emit indexReady(); +} + +FunctionScanner::FunctionInfo FunctionIndex::lookup(const QString &name) const +{ + return m_index.value(name.toLower(), FunctionScanner::FunctionInfo{}); +} + +bool FunctionIndex::isReady() const +{ + return m_ready; +} diff --git a/src/editor/FunctionIndex.h b/src/editor/FunctionIndex.h new file mode 100644 index 0000000..2446d14 --- /dev/null +++ b/src/editor/FunctionIndex.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "FunctionScanner.h" + +// --------------------------------------------------------------------------- +// FunctionIndex – Singleton-artiger Index aller Projektfunktionen. +// Wird von CodeEditor (Doppelklick-Navigation) und SignatureHelper genutzt. +// Aktualisiert sich asynchron wenn setProjectRoot() aufgerufen wird. +// --------------------------------------------------------------------------- +class FunctionIndex : public QObject +{ + Q_OBJECT + +public: + explicit FunctionIndex(QObject *parent = nullptr); + + void setProjectRoot(const QString &path); + void refresh(); + + // Sucht eine Funktion nach Name (Groß-/Kleinschreibung ignoriert) + // Gibt ungültige FunctionInfo zurück wenn nicht gefunden (filePath ist leer) + FunctionScanner::FunctionInfo lookup(const QString &name) const; + + bool isReady() const; + +signals: + void indexReady(); + +private slots: + void onScanFinished(); + +private: + QString m_projectRoot; + FunctionScanner *m_scanner = nullptr; + + QFutureWatcher> *m_watcher = nullptr; + + // name.toLower() → FunctionInfo + QHash m_index; + bool m_ready = false; +}; diff --git a/src/editor/FunctionListDialog.cpp b/src/editor/FunctionListDialog.cpp new file mode 100644 index 0000000..ab7c56b --- /dev/null +++ b/src/editor/FunctionListDialog.cpp @@ -0,0 +1,52 @@ +#include "FunctionListDialog.h" +#include "FunctionListPanel.h" + +#include +#include +#include + +FunctionListDialog::FunctionListDialog(QWidget *parent) + : QDialog(parent, Qt::Window) +{ + setWindowTitle(tr("Projektfunktionen – BareCode")); + setMinimumSize(500, 400); + + // Fenstergröße und -position wiederherstellen + QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode"); + if (s.contains("funclist/geometry")) + { + restoreGeometry(s.value("funclist/geometry").toByteArray()); + } + else + { + resize(600, 700); + } + + QVBoxLayout *layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + + m_panel = new FunctionListPanel(this); + m_panel->show(); // Immer sichtbar — Panel ist jetzt der gesamte Dialog-Inhalt + layout->addWidget(m_panel); + + connect(m_panel, &FunctionListPanel::fileLineRequested, + this, &FunctionListDialog::fileLineRequested); +} + +void FunctionListDialog::setProjectRoot(const QString &path) +{ + m_panel->setProjectRoot(path); +} + +void FunctionListDialog::refresh() +{ + m_panel->refresh(); +} + +void FunctionListDialog::closeEvent(QCloseEvent *event) +{ + QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode"); + s.setValue("funclist/geometry", saveGeometry()); + event->accept(); +} diff --git a/src/editor/FunctionListDialog.h b/src/editor/FunctionListDialog.h new file mode 100644 index 0000000..9310f60 --- /dev/null +++ b/src/editor/FunctionListDialog.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +class FunctionListPanel; + +// --------------------------------------------------------------------------- +// FunctionListDialog – Eigenständiges Fenster für die Projektfunktionen. +// Bleibt offen während man im Editor arbeitet (non-modal). +// --------------------------------------------------------------------------- +class FunctionListDialog : public QDialog +{ + Q_OBJECT + +public: + explicit FunctionListDialog(QWidget *parent = nullptr); + + void setProjectRoot(const QString &path); + void refresh(); + +protected: + void closeEvent(QCloseEvent *event) override; + +signals: + void fileLineRequested(const QString &filePath, int line); + +private: + FunctionListPanel *m_panel = nullptr; +}; diff --git a/src/editor/FunctionListPanel.cpp b/src/editor/FunctionListPanel.cpp new file mode 100644 index 0000000..c6441b3 --- /dev/null +++ b/src/editor/FunctionListPanel.cpp @@ -0,0 +1,332 @@ +#include "FunctionListPanel.h" + +#include +#include +#include +#include + +FunctionListPanel::FunctionListPanel(QWidget *parent) + : QWidget(parent) +{ + m_scanner = new FunctionScanner(this); + m_watcher = new QFutureWatcher>(this); + + connect(m_watcher, &QFutureWatcher>::finished, + this, &FunctionListPanel::onScanFinished); + + setupUi(); +} + +// --------------------------------------------------------------------------- +// UI +// --------------------------------------------------------------------------- +void FunctionListPanel::setupUi() +{ + QVBoxLayout *root = new QVBoxLayout(this); + root->setContentsMargins(0, 0, 0, 0); + root->setSpacing(0); + + // ---- Kopfzeile ---- + QWidget *header = new QWidget(this); + header->setStyleSheet("background: palette(mid);"); + QHBoxLayout *headerLayout = new QHBoxLayout(header); + headerLayout->setContentsMargins(6, 4, 6, 4); + headerLayout->setSpacing(4); + + QLabel *title = new QLabel(tr("Projektfunktionen"), header); + QFont boldFont = title->font(); + boldFont.setBold(true); + title->setFont(boldFont); + headerLayout->addWidget(title); + headerLayout->addStretch(); + + m_btnRefresh = new QPushButton(tr("↻"), header); + m_btnRefresh->setFixedSize(24, 24); + m_btnRefresh->setFlat(true); + m_btnRefresh->setToolTip(tr("Neu scannen")); + headerLayout->addWidget(m_btnRefresh); + + root->addWidget(header); + + // ---- Filter + Gruppierung ---- + QHBoxLayout *toolRow = new QHBoxLayout(); + toolRow->setContentsMargins(6, 4, 6, 4); + toolRow->setSpacing(4); + + m_filterEdit = new QLineEdit(this); + m_filterEdit->setPlaceholderText(tr("Funktion suchen…")); + m_filterEdit->setClearButtonEnabled(true); + toolRow->addWidget(m_filterEdit, 1); + + m_groupCombo = new QComboBox(this); + m_groupCombo->addItem(tr("Nach Datei"), "file"); + m_groupCombo->addItem(tr("Nach Klasse"), "class"); + m_groupCombo->addItem(tr("Alphabetisch"), "alpha"); + m_groupCombo->setFixedWidth(120); + toolRow->addWidget(m_groupCombo); + + root->addLayout(toolRow); + + // ---- Status ---- + m_statusLabel = new QLabel(this); + m_statusLabel->setContentsMargins(6, 0, 6, 2); + m_statusLabel->setStyleSheet("color: palette(mid); font-size: 11px;"); + root->addWidget(m_statusLabel); + + // ---- Ergebnisbaum ---- + m_tree = new QTreeWidget(this); + m_tree->setHeaderHidden(true); + m_tree->setRootIsDecorated(true); + m_tree->setIndentation(14); + m_tree->setAlternatingRowColors(false); + m_tree->setAnimated(true); + + // Monospace-Font für Parameter + QFont monoFont("Monospace", m_tree->font().pointSize()); + monoFont.setStyleHint(QFont::Monospace); + m_tree->setFont(monoFont); + + root->addWidget(m_tree, 1); + + // ---- Verbindungen ---- + connect(m_btnRefresh, &QPushButton::clicked, + this, &FunctionListPanel::refresh); + + connect(m_filterEdit, &QLineEdit::textChanged, + this, &FunctionListPanel::onFilterChanged); + + connect(m_groupCombo, QOverload::of(&QComboBox::currentIndexChanged), + this, &FunctionListPanel::onGroupingChanged); + + connect(m_tree, &QTreeWidget::itemActivated, + this, &FunctionListPanel::onItemActivated); +} + +// --------------------------------------------------------------------------- +// Öffentliche Schnittstelle +// --------------------------------------------------------------------------- +void FunctionListPanel::setProjectRoot(const QString &path) +{ + m_projectRoot = path; + if (!path.isEmpty()) + { + refresh(); + } + else + { + m_tree->clear(); + m_statusLabel->setText(QString()); + m_lastResult.clear(); + } +} + +void FunctionListPanel::activate() +{ + m_filterEdit->setFocus(); + m_filterEdit->selectAll(); + if (m_lastResult.isEmpty() && !m_projectRoot.isEmpty()) + { + refresh(); + } +} + +// --------------------------------------------------------------------------- +// Scan starten +// --------------------------------------------------------------------------- +void FunctionListPanel::refresh() +{ + if (m_projectRoot.isEmpty()) + { + m_statusLabel->setText(tr("Kein Projekt geöffnet.")); + return; + } + + if (m_watcher->isRunning()) + { + m_watcher->cancel(); + m_watcher->waitForFinished(); + } + + m_statusLabel->setText(tr("Scanne…")); + m_btnRefresh->setEnabled(false); + + const QString root = m_projectRoot; + FunctionScanner *scanner = m_scanner; + + QFuture> future = + QtConcurrent::run([scanner, root]() + { + return scanner->scanDirectory(root); + }); + + m_watcher->setFuture(future); +} + +// --------------------------------------------------------------------------- +// Scan abgeschlossen +// --------------------------------------------------------------------------- +void FunctionListPanel::onScanFinished() +{ + m_btnRefresh->setEnabled(true); + + if (m_watcher->isCanceled()) + { + return; + } + + m_lastResult = m_watcher->result(); + populateTree(m_lastResult); +} + +// --------------------------------------------------------------------------- +// Baum befüllen +// --------------------------------------------------------------------------- +void FunctionListPanel::populateTree(const QList &functions) +{ + m_tree->clear(); + + const QString groupBy = m_groupCombo->currentData().toString(); + const QString filter = m_filterEdit->text().trimmed().toLower(); + + // Funktionen filtern + QList filtered; + for (const auto &f : functions) + { + if (filter.isEmpty() || + f.name.toLower().contains(filter) || + f.parameters.toLower().contains(filter) || + f.className.toLower().contains(filter)) + { + filtered.append(f); + } + } + + if (groupBy == "alpha") + { + // Alphabetisch — keine Gruppen + std::sort(filtered.begin(), filtered.end(), + [](const FunctionScanner::FunctionInfo &a, + const FunctionScanner::FunctionInfo &b) + { + return a.name.toLower() < b.name.toLower(); + }); + + for (const auto &f : filtered) + { + QTreeWidgetItem *item = new QTreeWidgetItem(m_tree); + formatFunctionItem(item, f); + } + } + else + { + // Gruppiert nach Datei oder Klasse + QMap> groups; + + for (const auto &f : filtered) + { + QString key; + if (groupBy == "class") + { + key = f.className.isEmpty() ? tr("(global)") : f.className; + } + else + { + key = QFileInfo(f.filePath).fileName(); + } + groups[key].append(f); + } + + for (auto it = groups.begin(); it != groups.end(); ++it) + { + QTreeWidgetItem *groupItem = new QTreeWidgetItem(m_tree); + groupItem->setText(0, QString("%1 (%2)") + .arg(it.key()) + .arg(it.value().size())); + groupItem->setData(0, Qt::UserRole, QString()); + groupItem->setData(0, Qt::UserRole + 1, -1); + + QFont boldFont = groupItem->font(0); + boldFont.setBold(true); + groupItem->setFont(0, boldFont); + groupItem->setExpanded(true); + + // Funktionen innerhalb der Gruppe alphabetisch + auto &list = it.value(); + std::sort(list.begin(), list.end(), + [](const FunctionScanner::FunctionInfo &a, + const FunctionScanner::FunctionInfo &b) + { + return a.name.toLower() < b.name.toLower(); + }); + + for (const auto &f : list) + { + QTreeWidgetItem *item = new QTreeWidgetItem(groupItem); + formatFunctionItem(item, f); + } + } + } + + // Statuszeile + const int total = functions.size(); + const int shown = filtered.size(); + if (filter.isEmpty()) + { + m_statusLabel->setText(tr("%1 Funktionen gefunden").arg(total)); + } + else + { + m_statusLabel->setText(tr("%1 von %2 Funktionen").arg(shown).arg(total)); + } +} + +void FunctionListPanel::formatFunctionItem(QTreeWidgetItem *item, + const FunctionScanner::FunctionInfo &f) +{ + // Anzeige: name(parameter) — Zeilennummer klein dahinter + const QString display = QString("%1(%2)") + .arg(f.name) + .arg(f.parameters); + + item->setText(0, display); + item->setToolTip(0, QString("%1\nZeile %2").arg(f.filePath).arg(f.line)); + item->setData(0, Qt::UserRole, f.filePath); + item->setData(0, Qt::UserRole + 1, f.line); + + // Zeilennummer in gedimmter Farbe als zweite Spalte simulieren + // (über ToolTip gelöst da wir nur eine Spalte haben) +} + +// --------------------------------------------------------------------------- +// Slots +// --------------------------------------------------------------------------- +void FunctionListPanel::onItemActivated(QTreeWidgetItem *item, int /*column*/) +{ + const QString path = item->data(0, Qt::UserRole).toString(); + const int line = item->data(0, Qt::UserRole + 1).toInt(); + + if (path.isEmpty() || line < 0) + { + item->setExpanded(!item->isExpanded()); + return; + } + + emit fileLineRequested(path, line); +} + +void FunctionListPanel::onFilterChanged(const QString &text) +{ + Q_UNUSED(text) + if (!m_lastResult.isEmpty()) + { + populateTree(m_lastResult); + } +} + +void FunctionListPanel::onGroupingChanged(int /*index*/) +{ + if (!m_lastResult.isEmpty()) + { + populateTree(m_lastResult); + } +} diff --git a/src/editor/FunctionListPanel.h b/src/editor/FunctionListPanel.h new file mode 100644 index 0000000..fd283be --- /dev/null +++ b/src/editor/FunctionListPanel.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "FunctionScanner.h" + +// --------------------------------------------------------------------------- +// FunctionListPanel – Zeigt alle im Projekt definierten Funktionen +// als durchsuchbare, sortierbare Liste. +// +// Gruppierung: nach Datei oder nach Klasse +// Klick: öffnet Datei und springt zur Definition +// Aktualisierung: automatisch nach jedem Speichern +// --------------------------------------------------------------------------- +class FunctionListPanel : public QWidget +{ + Q_OBJECT + +public: + explicit FunctionListPanel(QWidget *parent = nullptr); + + void setProjectRoot(const QString &path); + +public slots: + void refresh(); + void activate(); + +signals: + void fileLineRequested(const QString &filePath, int line); + +private slots: + void onScanFinished(); + void onItemActivated(QTreeWidgetItem *item, int column); + void onFilterChanged(const QString &text); + void onGroupingChanged(int index); + +private: + void setupUi(); + void populateTree(const QList &functions); + void applyFilter(const QString &text); + static void formatFunctionItem(QTreeWidgetItem *item, + const FunctionScanner::FunctionInfo &f); + + QString m_projectRoot; + + QLineEdit *m_filterEdit = nullptr; + QComboBox *m_groupCombo = nullptr; + QPushButton *m_btnRefresh = nullptr; + QLabel *m_statusLabel = nullptr; + QTreeWidget *m_tree = nullptr; + + FunctionScanner *m_scanner = nullptr; + QFutureWatcher> *m_watcher = nullptr; + + // Letztes Scan-Ergebnis für Filter ohne Neuscan + QList m_lastResult; +}; diff --git a/src/editor/FunctionScanner.cpp b/src/editor/FunctionScanner.cpp new file mode 100644 index 0000000..10a8a02 --- /dev/null +++ b/src/editor/FunctionScanner.cpp @@ -0,0 +1,158 @@ +#include "FunctionScanner.h" + +#include +#include +#include +#include +#include + +FunctionScanner::FunctionScanner(QObject *parent) + : QObject(parent) +{ +} + +// --------------------------------------------------------------------------- +// Verzeichnis rekursiv scannen +// --------------------------------------------------------------------------- +QList FunctionScanner::scanDirectory( + const QString &rootPath, + const QStringList &extensions) const +{ + QList results; + + QDirIterator it(rootPath, + extensions, + QDir::Files, + QDirIterator::Subdirectories); + + while (it.hasNext()) + { + const QString path = it.next(); + results.append(scanFile(path)); + } + + return results; +} + +// --------------------------------------------------------------------------- +// Einzelne Datei scannen +// --------------------------------------------------------------------------- +QList FunctionScanner::scanFile(const QString &filePath) const +{ + QList results; + + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + return results; + } + + QTextStream stream(&file); + stream.setEncoding(QStringConverter::Utf8); + + QStringList lines; + while (!stream.atEnd()) + { + lines.append(stream.readLine()); + } + + // Regex für Funktionsdefinitionen: + // optional: public/protected/private/static/abstract/final + // gefolgt von: function name( + static const QRegularExpression funcRegex( + R"((?:(?:public|protected|private|static|abstract|final)\s+)*)" + R"(function\s+(&?\s*[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\s*\()", + QRegularExpression::CaseInsensitiveOption + ); + + for (int i = 0; i < lines.size(); ++i) + { + const QString &line = lines[i]; + + // Zeilen in Kommentaren überspringen + const QString trimmed = line.trimmed(); + if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("#")) + { + continue; + } + + QRegularExpressionMatch match = funcRegex.match(line); + if (!match.hasMatch()) + { + continue; + } + + FunctionInfo info; + info.name = match.captured(1).trimmed(); + info.filePath = filePath; + info.line = i + 1; + info.className = detectClassContext(lines, i); + info.parameters = extractParameters(line, static_cast(match.capturedEnd()) - 1); + info.signature = info.name + "(" + info.parameters + ")"; + + // Konstruktoren und magische Methoden kennzeichnen + // aber trotzdem aufnehmen — sie sind nützlich in der Liste + + results.append(info); + } + + return results; +} + +// --------------------------------------------------------------------------- +// Parameter aus der Funktionssignatur extrahieren +// Behandelt auch mehrzeilige Signaturen mit öffnender Klammer am Ende +// --------------------------------------------------------------------------- +QString FunctionScanner::extractParameters(const QString &line, int parenPos) +{ + // Inhalt zwischen ( und ) extrahieren + // Einfache Version: nur die erste Zeile — reicht für 99% aller Fälle + const int openParen = parenPos; + int depth = 0; + int closePos = -1; + + for (int i = openParen; i < line.length(); ++i) + { + if (line[i] == '(') { ++depth; } + else if (line[i] == ')') + { + --depth; + if (depth == 0) + { + closePos = i; + break; + } + } + } + + if (closePos == -1) + { + // Klammer geht über Zeilenende — gekürzt anzeigen + return line.mid(openParen + 1).trimmed() + "…"; + } + + return line.mid(openParen + 1, closePos - openParen - 1).trimmed(); +} + +// --------------------------------------------------------------------------- +// Klassenkontext erkennen — rückwärts durch den Code suchen +// --------------------------------------------------------------------------- +QString FunctionScanner::detectClassContext(const QStringList &lines, int functionLineIndex) +{ + static const QRegularExpression classRegex( + R"((?:class|interface|trait|enum)\s+([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*))", + QRegularExpression::CaseInsensitiveOption + ); + + // Rückwärts suchen — letztes class/interface/trait vor dieser Zeile + for (int i = functionLineIndex - 1; i >= 0; --i) + { + QRegularExpressionMatch match = classRegex.match(lines[i]); + if (match.hasMatch()) + { + return match.captured(1); + } + } + + return QString(); // Globale Funktion +} diff --git a/src/editor/FunctionScanner.h b/src/editor/FunctionScanner.h new file mode 100644 index 0000000..cd08243 --- /dev/null +++ b/src/editor/FunctionScanner.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// FunctionScanner – Scannt PHP-Dateien nach Funktionsdefinitionen. +// +// Liefert für jede gefundene Funktion: +// - Name +// - Parameter (vollständige Signatur) +// - Dateipfad +// - Zeilennummer +// +// Wird von FunctionListPanel (Anzeige) und DeadCodeAnalyzer (Stufe 1) +// gemeinsam genutzt. +// --------------------------------------------------------------------------- +class FunctionScanner : public QObject +{ + Q_OBJECT + +public: + struct FunctionInfo + { + QString name; // Funktionsname + QString parameters; // Parameter wie definiert, z.B. "$id, $name = null" + QString signature; // name(parameters) — fertig formatiert + QString filePath; // Absoluter Pfad zur Datei + int line = 0; // Zeilennummer der Definition + QString className; // Klassen- oder Namespace-Kontext, leer = global + }; + + explicit FunctionScanner(QObject *parent = nullptr); + + // Synchroner Scan — für direkten Aufruf aus Threads + QList scanDirectory(const QString &rootPath, + const QStringList &extensions = {"*.php"}) const; + + // Scannt eine einzelne Datei + QList scanFile(const QString &filePath) const; + +private: + static QString extractParameters(const QString &line, int parenPos); + static QString detectClassContext(const QStringList &lines, int functionLineIndex); +};