182 lines
5.5 KiB
C++
182 lines
5.5 KiB
C++
#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;
|
|
}
|