#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; }