Monday, May 27, 2013

Simple tool to visualize connections between signals and slots in Qt

This very simple and stupid tool visualizes connections between signals and slots and signals and signals in Qt. It takes one command line parameter namely the path to the project which should get visualized. From this path it goes down recursively and parses every *.cxx, *.cpp, *.hxx and *.hpp file if it can find a connection with the following signature

connect\s*\((.+)\s*,\s*SIGNAL\((.+)\)\s*,\s*(.+)\s*,\s*(?:SLOT|SIGNAL)\((.+)\)\s*\)\s*\)

If it finds this signature (comments count as well) it generates a DOT (http://www.graphviz.org/) file. This graph can be visualized for example here or the command line tools generate an image. Because DOT tries to minimize the height of the graph a sightly better result can be achieved with the command line from here.
The this keyword will be replaced by the file name because otherwise DOT would connect all of them. The label of the edge is the signal in the first line and the slot (or signal) in the second one.

#include <QString>
#include <QDir>
#include <QDebug>
#include <QFileInfo>
#include <QFileInfoList>
#include <QIODevice>
#include <iostream>

void IterateDirs(QDir dir);

int main(int argc,char* argv[])
{
  bool foundSite = false;
  QString site;
  for(int i = 1; i < argc; ++i)
  {
    if(std::string(argv[i]) == "--path" || std::string(argv[i]) == "-p")
    {
      ++i;
      if (i < argc)
        site = argv[i];
      else
        break;
      foundSite = true;
      continue;
    }
  }
  if (argc < 3 || !foundSite)
  {
    std::cerr << "Usage: ConVis --path <projectpath>" << std::endl;
    return EXIT_FAILURE;
  }
  QDir dir(site);
  std::cout << "digraph connections {\nconstraint=\"false\" minlen=2\n";
  IterateDirs(dir);
  std::cout << "}\n";
  return EXIT_SUCCESS;
}

int i = 0;

void IterateDirs(QDir dir)
{
  QStringList filter;
  filter << "*.cxx" << "*.hxx" << "*.cpp" << "*.hpp";
  QFileInfoList files = dir.entryInfoList(filter, QDir::AllDirs |
                                          QDir::NoDotAndDotDot | QDir::Files);
  QFileInfoList::const_iterator it = files.constBegin();
  QFileInfoList::const_iterator en = files.constEnd();
  while (it != en)
  {
    QFileInfo info = *it;
    //qDebug() << info.isDir() << info.absoluteFilePath() << info.isFile();
    if (info.isDir())
      IterateDirs(QDir(info.absoluteFilePath()));
    else if (info.isFile())
    {
      QFile f(info.absoluteFilePath());
      f.open(QIODevice::ReadOnly);
      QString content = f.readAll();
      f.close();
      QRegExp r("connect\\s*\\((.+)\\s*,\\s*SIGNAL\\((.+)\\)\\s*,\\s*(.+)\\s*,"
                "\\s*(?:SLOT|SIGNAL)\\((.+)\\)\\s*\\)\\s*\\)");
      r.setMinimal(true);
      bool output = r.indexIn(content) != -1;
      if (output)
        std::cout << "subgraph cluster_" << i << "{\n";
      while (r.indexIn(content) != -1)
      {
        //qDebug() << r.capturedTexts();
        QStringList l = r.capturedTexts();
        if (l.at(1).trimmed() == "this")
          l.replace(1, info.fileName());
        if (l.at(3).trimmed() == "this")
          l.replace(3, info.fileName());
        std::cout << "\"" << l.at(1).trimmed().toStdString() << "\" -> \""
                          << l.at(3).trimmed().toStdString()
                          << "\" [color=\"green\"" << " label=\""
                          << l.at(2).trimmed().toStdString() << "\\n"
                          << l.at(4).trimmed().append(")").toStdString()
                          << "\" style=\"solid\" labeldistance=2];\n";
        content = content.mid(r.indexIn(content) + r.matchedLength());
      }
      if (output)
        std::cout << "label=\"" << info.fileName().toStdString() << "\"\n}\n";
      ++i;
    }
    ++it;
  }
}

3 comments:

  1. nice :)

    using
    while (r.indexIn(content) != -1 && r.capturedTexts().at(0).count(';')==0)

    avoids conflicts with randomly placed "connect"s

    still confuses equal named local slots between different compilation units :)

    ReplyDelete
  2. I will appreciate if you could attach some visual result
    Thanks

    ReplyDelete
  3. Excellent work!

    I had little trouble with some of my code because we include the connection type when needing UniqueConnections. This changes the regex a little bit:
    QRegExp r("connect\\s*\\((.+)\\s*,\\s*SIGNAL\\((.+)\\)\\s*,\\s*(.+)\\s*,"
    "\\s*(?:SLOT|SIGNAL)\\((.+)\\)\\s*\\)\\s*(?:, Qt::UniqueConnection|)\\)");

    The comment doesn't show up well, but you should be able to copy/paste.

    Thanks again.

    ReplyDelete