10 февраля 2016 г.

CLang API: рисуем диаграмму наследования (clang-3.7)

Попробуем использовать CLang API для чего-нибудь полезного, например, для рисования диаграммы наследования.

В Clang API уже есть подобный функционал - функция CXXRecordDecl::viewInheritance(). Функция viewInheritance рисует диаграмму наследования для заданного класса используя библиотеку GraphViz. Но делает она это не совсем так, как мне бы хотелось. Поэтому я попробую написать свою собственную процедуру рисования диаграммы наследования, используя программу dot из пакета GraphViz.

Предыдущая статья


Итак, что необходимо сделать по пунктам:

  • сгенерировать список классов и список связей между классами
  • сгенерировать dot-файл
  • сгенерировать графическое отображение, например, в формате png

Список классов и связей

У меня уже есть готовая программа для обхода AST. Поэтому я просто возьму ее и добавлю новый функционал, необходимый для рисования диаграммы.

Я создам два списка - один для хранения имен классов (classes), а второй для хранения связей (inheritance). Чтобы не запутаться, я помещу функционал, связанный с рисованием, в пространство имен view:

namespace view
{
  typedef std::map<std::string, std::string> Classes; // Id-Name
  typedef std::multimap<std::string, std::string> Inheritance; // Id-BaseId

  Classes classes;
  Inheritance inheritance;

  std::string nameToId (const std::string &name)
  {
    std::string id;
    for (auto i : name)
    {
      id.push_back((':' == i) ? '_' : i);
    }
    return id;
  }
}

Функция nameToId() нужна для того, чтобы избавиться от символов двоеточия в имени класса, так как программа dot использует двоеточие для других целей.

Теперь необходимо обновлять эти списки при обходе узлов CXXRecordDecl. Ниже приведен обновленный метод MyVisitor::VisitCXXRecordDecl (добавленные строки выделены жирным курсивом):

bool VisitCXXRecordDecl (CXXRecordDecl *D)
{
  const std::string qName = D->getQualifiedNameAsString();

  llvm::outs() << "CXXRecord: " << D->getKindName();
  if (D->getIdentifier())
  {
    llvm::outs() << ", id: " << D->getIdentifier()->getName();
    llvm::outs() << " " << D;
  }
  llvm::outs() << " | " << qName;
  llvm::outs() << "\n";

  view::classes[view::nameToId(qName)] = qName;

  // List base classes
  {
    if (D->getNumBases())
    {
      llvm::outs() << "- Bases (" << D->getNumBases() << ")\n";
      for (const auto &base : D->bases())
      {
        llvm::outs() << "- - ";
        const QualType type = base.getType();
        const RecordType *recType = type->getAs<RecordType>();
        const CXXRecordDecl *cxxDecl = cast_or_null<CXXRecordDecl>(recType->getDecl()->getDefinition());
        assert(cxxDecl);

        const std::string qNameBase = cxxDecl->getQualifiedNameAsString();
        llvm::outs() << type.getAsString() << " | " << qNameBase;
        if (base.isVirtual())
        {
          llvm::outs() << " (virtual)";
        }
        llvm::outs() << "\n";

        view::inheritance.insert(view::Inheritance::value_type(view::nameToId(qName), view::nameToId(qNameBase)));
      }
    }

    llvm::outs() << "- Methods\n";
    for (auto i: D->methods())
    {
      llvm::outs() << "- - " << (*i).getQualifiedNameAsString() << "\n";
    }
  }   
  llvm::outs() << "------------------------------------------\n";

  return true;
}

В этом методе при каждом посещении узла CXXRecordDecl в список view::classes добавляется новая строка с именем текущего C++-класса. А при переборе базовых классов в список view::inheritance добавляется соответствующая запись вида Class1->Class2, где Class2 является базовым классом для класса Class1.

Dot-файл

Списки получены. Сгенерируем файл в формате, который понимает программа dot (подробнее о формате чертежей dot можно почитать в мануале).

Добавим в пространство имен view функцию generateDot(). Эта функция создает временный файл (по умолчанию в папке /tmp), и затем выводит оба сгенерированных списка в формате чертежей dot. Каждый класс будет изображен в виде прямоугольника с именем класса, а каждая связь между классами - в виде сплошной линии со стрелкой на конце.

namespace view
{
  void generateDot ()
  {
    int FD;
    SmallString<128> Filename;
    if (std::error_code EC = llvm::sys::fs::createTemporaryFile("my", "dot", FD, Filename))
    {
      llvm::errs() << "Error: " << EC.message() << "\n";
      return;
    }

    llvm::errs() << "Writing '" << Filename << "'... ";
    llvm::raw_fd_ostream O(FD, true);

    raw_ostream& Out = O;
    Out << "digraph \"" << llvm::DOT::EscapeString("MyDiagram") << "\" {\n";
    for (auto i : classes)
    {
      Out << "  ";
      Out << i.first;
      Out << " [ shape=\"box\", label=\"" << llvm::DOT::EscapeString(i.second);
      Out << "\"];\n";
    }
    for (auto i : inheritance)
    {
      Out << "  ";
      Out << llvm::DOT::EscapeString(i.first);
      Out << " -> ";
      Out << llvm::DOT::EscapeString(i.second);
      Out << ";\n";
    }
    Out << "}\n";

    llvm::errs() << " done. \n";
    O.close();
  }
}

Для примера я возьму код, который я "скормил" анализатору в прошлой статье, и еще добавлю вызов функции generateDot().

int main (int argc, char *argv[])
{
  const bool ret = clang::tooling::runToolOnCode(new MyAction,
    "struct MyStruct {};"
    "class MyClass {};"
    "namespace Nspace {"
    "class MyDerived : public MyStruct {};"
    "class MyDerived2 : public MyDerived, public virtual MyClass {};"
    "}");

  view::generateDot();

  return ret ? 0 : -1;
}

Скомпилируем и посмотрим на результаты.

> ./a.out
...
Writing '/tmp/my-ff62b3.dot'...  done.

> cat /tmp/my-ff62b3.dot
digraph "MyDiagram" {
  MyClass [ shape="box", label="MyClass"];
  MyStruct [ shape="box", label="MyStruct"];
  Nspace__MyDerived [ shape="box", label="Nspace::MyDerived"];
  Nspace__MyDerived2 [ shape="box", label="Nspace::MyDerived2"];
  Nspace__MyDerived -> MyStruct;
  Nspace__MyDerived2 -> Nspace__MyDerived;
  Nspace__MyDerived2 -> MyClass;
}

Диаграмма в png

Итак, моя программа создала файл /tmp/my-ff62b3.dot, который содержит описание чертежа в формате, понятном программе dot.

Самое время получить графический файл:

> dot -Tpng /tmp/my-8675f2.dot > f.png
> xdg-open f.png

Программа dot генерирует файл с именем f.png из исходного файла /tmp/my-8675f2.dot в формате png, что было задано ключом -Tpng.

Команда xdg-open специфична для Linux-систем - она должна проанализировать содержимое файла f.png и запустить соответствующую программу для просмотра (если такая есть в системе, разумеется). В моем случае был запущен Firefox.

Вот так выглядит диаграмма наследования, построенная на основе обхода Clang AST.

Заключение

На мой взгляд получилась очень красивая диаграмма наследования ))

Исходный код

Исходники можно взять на GitHub.

Комментариев нет:

Отправить комментарий