- Auslisten aller im Projekt definierten Funktionen

- Suche nach toten Funktionen im Projekt
- Doppelklick auf Funktion öffnet die Datei mit der Deklaration
This commit is contained in:
2026-08-13 08:43:07 +02:00
parent d9d60b73f9
commit ed15fd40c8
23 changed files with 1778 additions and 6 deletions

View File

@@ -95,6 +95,10 @@ install(TARGETS BareCode
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
) )
install(FILES LICENSE
DESTINATION ${CMAKE_INSTALL_DATADIR}/licenses/barecode
)
# ---- Linux (FreeDesktop) -------------------------------------------------- # ---- Linux (FreeDesktop) --------------------------------------------------
if(PLATFORM_LINUX) if(PLATFORM_LINUX)
foreach(SIZE 16 32 48 64 128 256 512) foreach(SIZE 16 32 48 64 128 256 512)

View File

@@ -1,6 +1,6 @@
MIT License 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

36
PKGBUILD Normal file
View File

@@ -0,0 +1,36 @@
# Maintainer: Dany Thinnes <dany@projekt-hirnfrei.de>
# 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"
}

43
PKGBUILD-git Normal file
View File

@@ -0,0 +1,43 @@
# Maintainer: Dany Thinnes <dany@projekt-hirnfrei.de>
# 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"
}

View File

@@ -119,6 +119,14 @@ void MainWindow::setupMenuBar()
m_actFileSearch = mBearbeiten->addAction(tr("In &Dateien suchen…"), this, &MainWindow::onShowFileSearch); m_actFileSearch = mBearbeiten->addAction(tr("In &Dateien suchen…"), this, &MainWindow::onShowFileSearch);
m_actFileSearch->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_F); 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 ---- // ---- Ansicht ----
QMenu *mAnsicht = menuBar()->addMenu(tr("&Ansicht")); QMenu *mAnsicht = menuBar()->addMenu(tr("&Ansicht"));
@@ -281,6 +289,16 @@ void MainWindow::onAbout()
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Slots Projekt // Slots Projekt
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
void MainWindow::onShowDeadCode()
{
m_editor->showDeadCode();
}
void MainWindow::onShowFunctionList()
{
m_editor->showFunctionList();
}
void MainWindow::onShowFileSearch() void MainWindow::onShowFileSearch()
{ {
m_editor->showFileSearchPanel(); m_editor->showFileSearchPanel();

View File

@@ -42,6 +42,8 @@ private slots:
void onRedo(); void onRedo();
void onShowSearch(); void onShowSearch();
void onShowFileSearch(); void onShowFileSearch();
void onShowFunctionList();
void onShowDeadCode();
// Ansicht // Ansicht
void onToggleDarkMode(bool checked); void onToggleDarkMode(bool checked);
// Hilfe // Hilfe
@@ -82,6 +84,8 @@ private:
QAction *m_actRedo = nullptr; QAction *m_actRedo = nullptr;
QAction *m_actSearch = nullptr; QAction *m_actSearch = nullptr;
QAction *m_actFileSearch = nullptr; QAction *m_actFileSearch = nullptr;
QAction *m_actFuncList = nullptr;
QAction *m_actDeadCode = nullptr;
QAction *m_actDarkMode = nullptr; QAction *m_actDarkMode = nullptr;
QAction *m_actAbout = nullptr; QAction *m_actAbout = nullptr;
}; };

View File

@@ -17,6 +17,18 @@ set(EDITOR_SOURCES
SignatureHelper.h SignatureHelper.h
SignatureTooltip.cpp SignatureTooltip.cpp
SignatureTooltip.h 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}) add_library(BareCode_Editor STATIC ${EDITOR_SOURCES})

View File

@@ -2,6 +2,7 @@
#include "LineNumberArea.h" #include "LineNumberArea.h"
#include "ColorIndicator.h" #include "ColorIndicator.h"
#include "SignatureHelper.h" #include "SignatureHelper.h"
#include "FunctionIndex.h"
#include "core/Settings.h" #include "core/Settings.h"
#include "highlighter/HighlighterFactory.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) void CodeEditor::mousePressEvent(QMouseEvent *event)
{ {
// Zuerst prüfen ob ein Farbquadrat geklickt wurde // Zuerst prüfen ob ein Farbquadrat geklickt wurde

View File

@@ -10,6 +10,7 @@ class Settings;
class SyntaxHighlighter; class SyntaxHighlighter;
class ColorIndicator; class ColorIndicator;
class SignatureHelper; class SignatureHelper;
class FunctionIndex;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// CodeEditor Core editing widget. // CodeEditor Core editing widget.
@@ -48,14 +49,18 @@ public:
QRectF blockBoundingGeometryPublic(const QTextBlock &b) const { return blockBoundingGeometry(b); } QRectF blockBoundingGeometryPublic(const QTextBlock &b) const { return blockBoundingGeometry(b); }
QPointF contentOffsetPublic() const { return contentOffset(); } QPointF contentOffsetPublic() const { return contentOffset(); }
void setFunctionIndex(FunctionIndex *index);
signals: signals:
void fileSaved(const QString &filePath); void fileSaved(const QString &filePath);
void navigateToRequested(const QString &filePath, int line);
protected: protected:
void resizeEvent(QResizeEvent *event) override; void resizeEvent(QResizeEvent *event) override;
void keyPressEvent(QKeyEvent *event) override; void keyPressEvent(QKeyEvent *event) override;
void paintEvent(QPaintEvent *event) override; void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override; void mousePressEvent(QMouseEvent *event) override;
void mouseDoubleClickEvent(QMouseEvent *event) override;
private slots: private slots:
void updateLineNumberAreaWidth(int newBlockCount); void updateLineNumberAreaWidth(int newBlockCount);
@@ -72,5 +77,6 @@ private:
SyntaxHighlighter *m_highlighter = nullptr; SyntaxHighlighter *m_highlighter = nullptr;
ColorIndicator *m_colorIndicator = nullptr; ColorIndicator *m_colorIndicator = nullptr;
SignatureHelper *m_signatureHelper = nullptr; SignatureHelper *m_signatureHelper = nullptr;
FunctionIndex *m_functionIndex = nullptr;
QString m_filePath; QString m_filePath;
}; };

View File

@@ -0,0 +1,181 @@
#include "DeadCodeAnalyzer.h"
#include <QDir>
#include <QDirIterator>
#include <QFile>
#include <QTextStream>
#include <QRegularExpression>
#include <QSet>
#include <QFileInfo>
DeadCodeAnalyzer::DeadCodeAnalyzer(QObject *parent)
: QObject(parent)
{
m_scanner = new FunctionScanner(this);
}
QList<DeadCodeAnalyzer::DeadFunction> 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<QString, FunctionScanner::FunctionInfo> definitions;
for (const QString &path : allFiles)
{
const QList<FunctionScanner::FunctionInfo> 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<QString> 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<QString> 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<DeadFunction> 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;
}

View File

@@ -0,0 +1,39 @@
#pragma once
#include <QObject>
#include <QString>
#include <QStringList>
#include <QList>
#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<DeadFunction> 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 &currentFile) const;
private:
FunctionScanner *m_scanner = nullptr;
};

View File

@@ -0,0 +1,461 @@
#include "DeadCodeDialog.h"
#include <QtConcurrent/QtConcurrent>
#include <QFileInfo>
#include <QDir>
#include <QFont>
#include <QSettings>
#include <QCloseEvent>
#include <QFileDialog>
#include <QTextStream>
#include <QMessageBox>
#include <QHeaderView>
#include <QDateTime>
#include <QMetaObject>
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<QList<DeadCodeAnalyzer::DeadFunction>>(this);
connect(m_watcher, &QFutureWatcher<QList<DeadCodeAnalyzer::DeadFunction>>::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<QList<DeadCodeAnalyzer::DeadFunction>> 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 &currentFile)
{
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<DeadCodeAnalyzer::DeadFunction> 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();
}

View File

@@ -0,0 +1,62 @@
#pragma once
#include <QDialog>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QLabel>
#include <QPushButton>
#include <QProgressBar>
#include <QLineEdit>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFutureWatcher>
#include <QString>
#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 &currentFile);
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<QList<DeadCodeAnalyzer::DeadFunction>> *m_watcher = nullptr;
};

View File

@@ -3,6 +3,9 @@
#include "CodeEditor.h" #include "CodeEditor.h"
#include "SearchPanel.h" #include "SearchPanel.h"
#include "FileSearchPanel.h" #include "FileSearchPanel.h"
#include "FunctionListDialog.h"
#include "DeadCodeDialog.h"
#include "FunctionIndex.h"
#include <QFileInfo> #include <QFileInfo>
#include <QFile> #include <QFile>
@@ -32,6 +35,9 @@ void EditorPanel::setupUi()
m_searchPanel = new SearchPanel(this); m_searchPanel = new SearchPanel(this);
m_fileSearch = new FileSearchPanel(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_tabWidget, 1);
m_layout->addWidget(m_searchPanel, 0); m_layout->addWidget(m_searchPanel, 0);
@@ -45,6 +51,12 @@ void EditorPanel::setupUi()
connect(m_fileSearch, &FileSearchPanel::fileLineRequested, connect(m_fileSearch, &FileSearchPanel::fileLineRequested,
this, &EditorPanel::goToLine); 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_tabWidget->setTabToolTip(index, filePath);
m_openTabs.insert(filePath, tab); 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) // Änderungsindikator im Tab-Titel (● = ungespeichert)
connect(tab->editor()->document(), &QTextDocument::modificationChanged, connect(tab->editor()->document(), &QTextDocument::modificationChanged,
this, [this, tab](bool modified) this, [this, tab](bool modified)
@@ -102,6 +121,13 @@ void EditorPanel::openFile(const QString &filePath)
m_tabWidget->setTabToolTip(idx, savedPath); m_tabWidget->setTabToolTip(idx, savedPath);
} }
emit currentFileSaved(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(); 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) void EditorPanel::setSearchRoot(const QString &path)
{ {
m_fileSearch->setSearchRoot(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) void EditorPanel::goToLine(const QString &filePath, int line)

View File

@@ -10,6 +10,9 @@ class EditorTab;
class Settings; class Settings;
class SearchPanel; class SearchPanel;
class FileSearchPanel; class FileSearchPanel;
class FunctionListDialog;
class DeadCodeDialog;
class FunctionIndex;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// EditorPanel Rechtes Panel: Tab-Leiste + Editoren + Such/Ersetzen-Panel. // EditorPanel Rechtes Panel: Tab-Leiste + Editoren + Such/Ersetzen-Panel.
@@ -29,6 +32,8 @@ public slots:
void setSearchRoot(const QString &path); void setSearchRoot(const QString &path);
void showSearchPanel(); void showSearchPanel();
void showFileSearchPanel(); void showFileSearchPanel();
void showFunctionList();
void showDeadCode();
void goToLine(const QString &filePath, int line); void goToLine(const QString &filePath, int line);
// Session // Session
@@ -50,11 +55,14 @@ private:
int findTabForFile(const QString &filePath) const; int findTabForFile(const QString &filePath) const;
EditorTab *currentTab() const; EditorTab *currentTab() const;
Settings *m_settings = nullptr; Settings *m_settings = nullptr;
QVBoxLayout *m_layout = nullptr; QVBoxLayout *m_layout = nullptr;
QTabWidget *m_tabWidget = nullptr; QTabWidget *m_tabWidget = nullptr;
SearchPanel *m_searchPanel = nullptr; SearchPanel *m_searchPanel = nullptr;
FileSearchPanel *m_fileSearch = nullptr; FileSearchPanel *m_fileSearch = nullptr;
FunctionListDialog *m_funcDialog = nullptr;
DeadCodeDialog *m_deadCode = nullptr;
FunctionIndex *m_funcIndex = nullptr;
QHash<QString, EditorTab *> m_openTabs; QHash<QString, EditorTab *> m_openTabs;
}; };

View File

@@ -0,0 +1,83 @@
#include "FunctionIndex.h"
#include <QtConcurrent/QtConcurrent>
FunctionIndex::FunctionIndex(QObject *parent)
: QObject(parent)
{
m_scanner = new FunctionScanner(this);
m_watcher = new QFutureWatcher<QList<FunctionScanner::FunctionInfo>>(this);
connect(m_watcher, &QFutureWatcher<QList<FunctionScanner::FunctionInfo>>::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<FunctionScanner::FunctionInfo> 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;
}

View File

@@ -0,0 +1,47 @@
#pragma once
#include <QObject>
#include <QHash>
#include <QString>
#include <QStringList>
#include <QFutureWatcher>
#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<QList<FunctionScanner::FunctionInfo>> *m_watcher = nullptr;
// name.toLower() → FunctionInfo
QHash<QString, FunctionScanner::FunctionInfo> m_index;
bool m_ready = false;
};

View File

@@ -0,0 +1,52 @@
#include "FunctionListDialog.h"
#include "FunctionListPanel.h"
#include <QVBoxLayout>
#include <QSettings>
#include <QCloseEvent>
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();
}

View File

@@ -0,0 +1,30 @@
#pragma once
#include <QDialog>
#include <QString>
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;
};

View File

@@ -0,0 +1,332 @@
#include "FunctionListPanel.h"
#include <QtConcurrent/QtConcurrent>
#include <QFileInfo>
#include <QFont>
#include <QProgressBar>
FunctionListPanel::FunctionListPanel(QWidget *parent)
: QWidget(parent)
{
m_scanner = new FunctionScanner(this);
m_watcher = new QFutureWatcher<QList<FunctionScanner::FunctionInfo>>(this);
connect(m_watcher, &QFutureWatcher<QList<FunctionScanner::FunctionInfo>>::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<int>::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<QList<FunctionScanner::FunctionInfo>> 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<FunctionScanner::FunctionInfo> &functions)
{
m_tree->clear();
const QString groupBy = m_groupCombo->currentData().toString();
const QString filter = m_filterEdit->text().trimmed().toLower();
// Funktionen filtern
QList<FunctionScanner::FunctionInfo> 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<QString, QList<FunctionScanner::FunctionInfo>> 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);
}
}

View File

@@ -0,0 +1,67 @@
#pragma once
#include <QWidget>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QLineEdit>
#include <QLabel>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QComboBox>
#include <QFutureWatcher>
#include <QString>
#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<FunctionScanner::FunctionInfo> &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<QList<FunctionScanner::FunctionInfo>> *m_watcher = nullptr;
// Letztes Scan-Ergebnis für Filter ohne Neuscan
QList<FunctionScanner::FunctionInfo> m_lastResult;
};

View File

@@ -0,0 +1,158 @@
#include "FunctionScanner.h"
#include <QDir>
#include <QDirIterator>
#include <QFile>
#include <QTextStream>
#include <QRegularExpression>
FunctionScanner::FunctionScanner(QObject *parent)
: QObject(parent)
{
}
// ---------------------------------------------------------------------------
// Verzeichnis rekursiv scannen
// ---------------------------------------------------------------------------
QList<FunctionScanner::FunctionInfo> FunctionScanner::scanDirectory(
const QString &rootPath,
const QStringList &extensions) const
{
QList<FunctionInfo> 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::FunctionInfo> FunctionScanner::scanFile(const QString &filePath) const
{
QList<FunctionInfo> 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<int>(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
}

View File

@@ -0,0 +1,47 @@
#pragma once
#include <QObject>
#include <QString>
#include <QStringList>
#include <QList>
// ---------------------------------------------------------------------------
// 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<FunctionInfo> scanDirectory(const QString &rootPath,
const QStringList &extensions = {"*.php"}) const;
// Scannt eine einzelne Datei
QList<FunctionInfo> scanFile(const QString &filePath) const;
private:
static QString extractParameters(const QString &line, int parenPos);
static QString detectClassContext(const QStringList &lines, int functionLineIndex);
};