cppcheck/gui/resultstree.cpp

781 lines
20 KiB
C++

/*
* Cppcheck - A tool for static C/C++ code analysis
* Copyright (C) 2007-2009 Daniel Marjamäki and Cppcheck team.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/
*/
#include "resultstree.h"
#include <QApplication>
#include <QDebug>
#include <QMenu>
#include <QSignalMapper>
#include <QProcess>
#include <QDir>
#include <QMessageBox>
#include <QFileInfo>
#include <QClipboard>
ResultsTree::ResultsTree(QSettings &settings, ApplicationList &list) :
mSettings(settings),
mApplications(list),
mContextItem(0),
mCheckPath(""),
mVisibleErrors(false)
{
setModel(&mModel);
QStringList labels;
labels << tr("File") << tr("Severity") << tr("Line") << tr("Message");
mModel.setHorizontalHeaderLabels(labels);
setExpandsOnDoubleClick(false);
setSortingEnabled(true);
LoadSettings();
connect(this, SIGNAL(doubleClicked(const QModelIndex &)),
this, SLOT(QuickStartApplication(const QModelIndex &)));
}
ResultsTree::~ResultsTree()
{
SaveSettings();
}
QStandardItem *ResultsTree::CreateItem(const QString &name)
{
QStandardItem *item = new QStandardItem(name);
item->setEditable(false);
return item;
}
void ResultsTree::AddErrorItem(const QString &file,
const QString &severity,
const QString &message,
const QStringList &files,
const QVariantList &lines,
const QString &id)
{
Q_UNUSED(file);
if (files.isEmpty())
{
return;
}
QString realfile = StripPath(files[0], false);
if (realfile.isEmpty())
{
realfile = "Undefined file";
}
bool hide = !mShowTypes[SeverityToShowType(severity)];
//if there is at least on error that is not hidden, we have a visible error
if (!hide)
{
mVisibleErrors = true;
}
//Create the base item for the error and ensure it has a proper
//file item as a parent
QStandardItem *item = AddBacktraceFiles(EnsureFileItem(files[0], hide),
realfile,
lines[0].toInt(),
severity,
message,
hide,
SeverityToIcon(severity));
//Add user data to that item
QMap<QString, QVariant> data;
data["severity"] = SeverityToShowType(severity);
data["message"] = message;
data["files"] = files;
data["lines"] = lines;
data["id"] = id;
item->setData(QVariant(data));
//Add backtrace files as children
for (int i = 1;i < files.size() && i < lines.size();i++)
{
AddBacktraceFiles(item,
StripPath(files[i], false),
lines[i].toInt(),
severity,
message,
hide,
":images/go-down.png");
}
//TODO just hide/show current error and it's file
//since this does a lot of unnecessary work
if (!hide)
{
ShowFileItem(realfile);
}
}
QStandardItem *ResultsTree::AddBacktraceFiles(QStandardItem *parent,
const QString &file,
const int line,
const QString &severity,
const QString &message,
const bool hide,
const QString &icon)
{
if (!parent)
{
return 0;
}
QList<QStandardItem*> list;
list << CreateItem(file);
list << CreateItem(severity);
list << CreateItem(QString("%1").arg(line));
list << CreateItem(message);
QModelIndex index = QModelIndex();
parent->appendRow(list);
setRowHidden(parent->rowCount() - 1, parent->index(), hide);
if (!icon.isEmpty())
{
list[0]->setIcon(QIcon(icon));
}
//TODO Does this leak memory? Should items from list be deleted?
return list[0];
}
ShowTypes ResultsTree::VariantToShowType(const QVariant &data)
{
int value = data.toInt();
if (value < SHOW_ALL && value > SHOW_ERRORS)
{
return SHOW_NONE;
}
return (ShowTypes)value;
}
ShowTypes ResultsTree::SeverityToShowType(const QString & severity)
{
if (severity == "all")
return SHOW_ALL;
if (severity == "error")
return SHOW_ERRORS;
if (severity == "style")
return SHOW_STYLE;
if (severity == "security")
return SHOW_SECURITY;
return SHOW_NONE;
}
QStandardItem *ResultsTree::FindFileItem(const QString &name)
{
QList<QStandardItem *> list = mModel.findItems(name);
if (list.size() > 0)
{
return list[0];
}
return 0;
}
void ResultsTree::Clear()
{
mModel.removeRows(0, mModel.rowCount());
}
void ResultsTree::LoadSettings()
{
for (int i = 0;i < mModel.columnCount();i++)
{
//mFileTree.columnWidth(i);
QString temp = QString(tr("Result column %1 width")).arg(i);
setColumnWidth(i, mSettings.value(temp, 800 / mModel.columnCount()).toInt());
}
mSaveFullPath = mSettings.value(tr("Save full path"), false).toBool();
mSaveAllErrors = mSettings.value(tr("Save all errors"), false).toBool();
mShowFullPath = mSettings.value(tr("Show full path"), false).toBool();
}
void ResultsTree::SaveSettings()
{
for (int i = 0;i < mModel.columnCount();i++)
{
QString temp = QString(tr("Result column %1 width")).arg(i);
mSettings.setValue(temp, columnWidth(i));
}
}
void ResultsTree::ShowResults(ShowTypes type, bool show)
{
if (type != SHOW_NONE && mShowTypes[type] != show)
{
mShowTypes[type] = show;
RefreshTree();
}
}
void ResultsTree::RefreshTree()
{
mVisibleErrors = false;
//Get the amount of files in the tree
int filecount = mModel.rowCount();
for (int i = 0;i < filecount;i++)
{
//Get file i
QStandardItem *file = mModel.item(i, 0);
if (!file)
{
continue;
}
//Get the amount of errors this file contains
int errorcount = file->rowCount();
//By default it shouldn't be visible
bool show = false;
for (int j = 0;j < errorcount;j++)
{
//Get the error itself
QStandardItem *child = file->child(j, 0);
if (!child)
{
continue;
}
//Get error's user data
QVariant userdata = child->data();
//Convert it to QVariantMap
QVariantMap data = userdata.toMap();
//Check if this error should be hidden
bool hide = !mShowTypes[VariantToShowType(data["severity"])];
if (!hide)
{
mVisibleErrors = true;
}
//Hide/show accordingly
setRowHidden(j, file->index(), hide);
//If it was shown then the file itself has to be shown aswell
if (!hide)
{
show = true;
}
}
//Show the file if any of it's errors are visible
setRowHidden(i, QModelIndex(), !show);
}
}
QStandardItem *ResultsTree::EnsureFileItem(const QString &fullpath, bool hide)
{
QString name = StripPath(fullpath, false);
QStandardItem *item = FindFileItem(name);
if (item)
{
return item;
}
item = CreateItem(name);
item->setIcon(QIcon(":images/text-x-generic.png"));
//Add user data to that item
QMap<QString, QVariant> data;
data["files"] = fullpath;
item->setData(QVariant(data));
mModel.appendRow(item);
setRowHidden(mModel.rowCount() - 1, QModelIndex(), hide);
return item;
}
void ResultsTree::ShowFileItem(const QString &name)
{
QStandardItem *item = FindFileItem(name);
if (item)
{
setRowHidden(0, mModel.indexFromItem(item), false);
}
}
void ResultsTree::contextMenuEvent(QContextMenuEvent * e)
{
QModelIndex index = indexAt(e->pos());
if (index.isValid())
{
mContextItem = mModel.itemFromIndex(index);
//Create a new context menu
QMenu menu(this);
//Store all applications in a list
QList<QAction*> actions;
//Create a signal mapper so we don't have to store data to class
//member variables
QSignalMapper *signalMapper = new QSignalMapper(this);
if (mContextItem && mApplications.GetApplicationCount() > 0 && mContextItem->parent())
{
//Go through all applications and add them to the context menu
for (int i = 0;i < mApplications.GetApplicationCount();i++)
{
//Create an action for the application
QAction *start = new QAction(mApplications.GetApplicationName(i), &menu);
//Add it to our list so we can disconnect later on
actions << start;
//Add it to context menu
menu.addAction(start);
//Connect the signal to signal mapper
connect(start, SIGNAL(triggered()), signalMapper, SLOT(map()));
//Add a new mapping
signalMapper->setMapping(start, i);
}
connect(signalMapper, SIGNAL(mapped(int)),
this, SLOT(Context(int)));
}
// Add menuitems to copy full path/filename to clipboard
if (mContextItem)
{
if (mApplications.GetApplicationCount() > 0)
{
menu.addSeparator();
}
//Create an action for the application
QAction *copyfilename = new QAction(tr("Copy filename"), &menu);
QAction *copypath = new QAction(tr("Copy full path"), &menu);
menu.addAction(copyfilename);
menu.addAction(copypath);
connect(copyfilename, SIGNAL(triggered()), this, SLOT(CopyFilename()));
connect(copypath, SIGNAL(triggered()), this, SLOT(CopyFullPath()));
}
//Start the menu
menu.exec(e->globalPos());
if (mContextItem && mApplications.GetApplicationCount() > 0 && mContextItem->parent())
{
//Disconnect all signals
for (int i = 0;i < actions.size();i++)
{
disconnect(actions[i], SIGNAL(triggered()), signalMapper, SLOT(map()));
}
disconnect(signalMapper, SIGNAL(mapped(int)),
this, SLOT(Context(int)));
//And remove the signal mapper
delete signalMapper;
}
}
}
void ResultsTree::StartApplication(QStandardItem *target, int application)
{
//If there are now application's specified, tell the user about it
if (mApplications.GetApplicationCount() == 0)
{
QMessageBox msg(QMessageBox::Warning,
tr("Cppcheck"),
tr("You can open this error by specifying applications in program's settings."),
QMessageBox::Ok,
this);
msg.exec();
return;
}
if (target && application >= 0 && application < mApplications.GetApplicationCount() && target->parent())
{
QVariantMap data = target->data().toMap();
QString program = mApplications.GetApplicationPath(application);
//TODO Check which line was actually right clicked, now defaults to 0
unsigned int index = 0;
//Replace (file) with filename
QStringList files = data["files"].toStringList();
if (files.size() > 0)
{
program.replace("(file)", files[index], Qt::CaseInsensitive);
}
else
{
qDebug("Failed to get filename!");
}
QVariantList lines = data["lines"].toList();
if (lines.size() > 0)
{
program.replace("(line)", QString("%1").arg(lines[index].toInt()), Qt::CaseInsensitive);
}
else
{
qDebug("Failed to get filenumber!");
}
program.replace("(message)", data["message"].toString(), Qt::CaseInsensitive);
program.replace("(severity)", data["severity"].toString(), Qt::CaseInsensitive);
bool success = QProcess::startDetached(program);
if (!success)
{
QString app = mApplications.GetApplicationName(application);
QString text = tr("Could not start ") + app + "\n\n";
text += tr("Please check the application path and parameters are correct.");
QMessageBox msgbox(this);
msgbox.setWindowTitle("Cppcheck");
msgbox.setText(text);
msgbox.setIcon(QMessageBox::Critical);
msgbox.exec();
}
}
}
void ResultsTree::CopyFilename()
{
CopyPath(mContextItem, false);
}
void ResultsTree::CopyFullPath()
{
CopyPath(mContextItem, true);
}
void ResultsTree::Context(int application)
{
StartApplication(mContextItem, application);
}
void ResultsTree::QuickStartApplication(const QModelIndex &index)
{
StartApplication(mModel.itemFromIndex(index), 0);
}
void ResultsTree::CopyPath(QStandardItem *target, bool fullPath)
{
if (target)
{
QVariantMap data = target->data().toMap();
QString pathStr;
//Replace (file) with filename
QStringList files = data["files"].toStringList();
if (files.size() > 0)
{
pathStr = files[0];
if (!fullPath)
{
QFileInfo fi(pathStr);
pathStr = fi.fileName();
}
}
else
{
qDebug("Failed to get filename!");
}
QClipboard *clipboard = QApplication::clipboard();
clipboard->setText(pathStr);
}
}
QString ResultsTree::SeverityToIcon(const QString &severity)
{
if (severity == "all")
return ":images/dialog-warning.png";
if (severity == "error")
return ":images/dialog-error.png";
if (severity == "style")
return ":images/dialog-information.png";
return "";
}
void ResultsTree::SaveResults(QTextStream &out, bool xml)
{
if (xml)
{
out << "<?xml version=\"1.0\"?>" << endl << "<results>" << endl;
}
for (int i = 0;i < mModel.rowCount();i++)
{
QStandardItem *item = mModel.item(i, 0);
SaveErrors(out, item, xml);
}
if (xml)
{
out << "</results>" << endl;
}
}
void ResultsTree::SaveErrors(QTextStream &out, QStandardItem *item, bool xml)
{
if (!item)
{
return;
}
//qDebug() << item->text() << "has" << item->rowCount() << "errors";
for (int i = 0;i < item->rowCount();i++)
{
QStandardItem *error = item->child(i, 0);
if (!error)
{
continue;
}
if (isRowHidden(i, item->index()) && !mSaveAllErrors)
{
continue;
}
//Get error's user data
QVariant userdata = error->data();
//Convert it to QVariantMap
QVariantMap data = userdata.toMap();
QString line;
QString severity = ShowTypeToString(VariantToShowType(data["severity"]));
QString message = data["message"].toString();
QString id = data["id"].toString();
QStringList files = data["files"].toStringList();
QVariantList lines = data["lines"].toList();
if (files.size() <= 0 || lines.size() <= 0 || lines.size() != files.size())
{
continue;
}
if (xml)
{
/*
Error example from the core program in xml
<error file="gui/test.cpp" line="14" id="mismatchAllocDealloc" severity="error" msg="Mismatching allocation and deallocation: k"/>
The callstack seems to be ignored here aswell, instead last item of the stack is used
*/
line = QString("<error file=\"%1\" line=\"%2\" id=\"%3\" severity=\"%4\" msg=\"%5\"/>").
arg(StripPath(files[files.size()-1], true)). //filename
arg(lines[lines.size()-1].toInt()). //line
arg(id). //ID
arg(severity). //severity
arg(message); //Message
}
else
{
/*
Error example from the core program in text
[gui/test.cpp:23] -> [gui/test.cpp:14]: (error) Mismatching allocation and deallocation: k
*/
for (int i = 0;i < lines.size();i++)
{
line += QString("[%1:%2]").arg(StripPath(files[i], true)).arg(lines[i].toInt());
if (i < lines.size() - 1 && lines.size() > 0)
{
line += " -> ";
}
if (i == lines.size() - 1)
{
line += ": ";
}
}
line += QString("(%1) %2").arg(severity).arg(message);
}
out << line << endl;
}
}
QString ResultsTree::ShowTypeToString(ShowTypes type)
{
switch (type)
{
case SHOW_ALL:
return "all";
break;
case SHOW_STYLE:
return "style";
break;
case SHOW_SECURITY:
return "security";
break;
case SHOW_UNUSED:
return "unused";
break;
case SHOW_ERRORS:
return "error";
break;
case SHOW_NONE:
return "";
break;
}
return "";
}
void ResultsTree::UpdateSettings(bool showFullPath,
bool saveFullPath,
bool saveAllErrors)
{
if (mShowFullPath != showFullPath)
{
mShowFullPath = showFullPath;
RefreshFilePaths();
}
mSaveFullPath = saveFullPath;
mSaveAllErrors = saveAllErrors;
}
void ResultsTree::SetCheckDirectory(const QString &dir)
{
mCheckPath = dir;
}
QString ResultsTree::StripPath(const QString &path, bool saving)
{
if ((!saving && mShowFullPath) || (saving && mSaveFullPath))
{
return QString(path);
}
QDir dir(mCheckPath);
return dir.relativeFilePath(path);
}
void ResultsTree::RefreshFilePaths(QStandardItem *item)
{
if (!item)
{
return;
}
//Mark that this file's path hasn't been updated yet
bool updated = false;
//Loop through all errors within this file
for (int i = 0;i < item->rowCount();i++)
{
//Get error i
QStandardItem *error = item->child(i, 0);
if (!error)
{
continue;
}
//Get error's user data
QVariant userdata = error->data();
//Convert it to QVariantMap
QVariantMap data = userdata.toMap();
//Get list of files
QStringList files = data["files"].toStringList();
//We should always have at least 1 file per error
if (files.size() == 0)
{
continue;
}
//Update this error's text
error->setText(StripPath(files[0], false));
//If this error has backtraces make sure the files list has enough filenames
if (error->rowCount() <= files.size() - 1)
{
//Loop through all files within the error
for (int j = 0;j < error->rowCount();j++)
{
//Get file
QStandardItem *file = error->child(j, 0);
if (!file)
{
continue;
}
//Update file's path
file->setText(StripPath(files[j+1], false));
}
}
//if the main file hasn't been updated yet, update it now
if (!updated)
{
updated = true;
item->setText(error->text());
}
}
}
void ResultsTree::RefreshFilePaths()
{
qDebug("Refreshing file paths");
//Go through all file items (these are parent items that contain the errors)
for (int i = 0;i < mModel.rowCount();i++)
{
RefreshFilePaths(mModel.item(i, 0));
}
}
bool ResultsTree::VisibleErrors()
{
return mVisibleErrors;
}