Commit 55365839 authored by Corentin Plouët's avatar Corentin Plouët 🐨

Graphviz: added test suite, fixes, enhancements

* Added a fairly comprehensive test suite
* Separated the graph traversal logic from the Graphviz generation
  code by introducing a new class, cmLinkItemsGraphVisitor{.h,cxx}
* Made the graph traversal logic less ad-hoc by using existing
  methods in the GlobalGenerator; this fixed a few bugs
* Added support for new target types: custom targets, object
  and unknown libraries
* Improved support for ALIAS libraries by showing the alias(es)
  in the graph
* Introduced new flags to control those new libraries (consistent
  with existing flags)
* Updated the documentation
* Removed useless setting to set graph type in dot file
* Improved the node/edge shapes (nicer, more consistent)
* Added a legend to the graph
* Some refactoring and cleanup of the Graphviz generation code
* Added test and fix for issue 19746
parent 4c292974
......@@ -5,119 +5,145 @@
CMakeGraphVizOptions
--------------------
The builtin graphviz support of CMake.
The builtin Graphviz support of CMake.
Variables specific to the graphviz support
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Generating Graphviz files
^^^^^^^^^^^^^^^^^^^^^^^^^
CMake
can generate `graphviz <http://www.graphviz.org/>`_ files, showing the dependencies between the
targets in a project and also external libraries which are linked
against. When CMake is run with the ``--graphviz=foo.dot`` option, it will
produce:
CMake can generate `Graphviz <https://www.graphviz.org/>`_ files showing the
dependencies between the targets in a project, as well as external libraries
which are linked against.
* a ``foo.dot`` file showing all dependencies in the project
* a ``foo.dot.<target>`` file for each target, file showing on which other targets the respective target depends
* a ``foo.dot.<target>.dependers`` file, showing which other targets depend on the respective target
When running CMake with the ``--graphviz=foo.dot`` option, it produces:
The different dependency types ``PUBLIC``, ``PRIVATE`` and ``INTERFACE``
are represented as solid, dashed and dotted edges.
* a ``foo.dot`` file, showing all dependencies in the project
* a ``foo.dot.<target>`` file for each target, showing on which other targets
it depends
* a ``foo.dot.<target>.dependers`` file for each target, showing which other
targets depend on it
This can result in huge graphs. Using the file
``CMakeGraphVizOptions.cmake`` the look and content of the generated
graphs can be influenced. This file is searched first in
:variable:`CMAKE_BINARY_DIR` and then in :variable:`CMAKE_SOURCE_DIR`. If found, it is
read and the variables set in it are used to adjust options for the
generated graphviz files.
Those .dot files can be converted to images using the *dot* command from the
Graphviz package:
.. variable:: GRAPHVIZ_GRAPH_TYPE
.. code-block:: shell
The graph type.
dot -Tpng -o foo.png foo.dot
* Mandatory : NO
* Default : "digraph"
The different dependency types ``PUBLIC``, ``INTERFACE`` and ``PRIVATE``
are represented as solid, dashed and dotted edges.
Valid graph types are:
Variables specific to the Graphviz support
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* "graph" : Nodes are joined with lines
* "digraph" : Nodes are joined with arrows showing direction
* "strict graph" : Like "graph" but max one line between each node
* "strict digraph" : Like "graph" but max one line between each node in each direction
The resulting graphs can be huge. The look and content of the generated graphs
can be controlled using the file ``CMakeGraphVizOptions.cmake``. This file is
first searched in :variable:`CMAKE_BINARY_DIR`, and then in
:variable:`CMAKE_SOURCE_DIR`. If found, the variables set in it are used to
adjust options for the generated Graphviz files.
.. variable:: GRAPHVIZ_GRAPH_NAME
The graph name.
* Mandatory : NO
* Default : "GG"
* Mandatory: NO
* Default: value of :variable:`CMAKE_PROJECT_NAME`
.. variable:: GRAPHVIZ_GRAPH_HEADER
The header written at the top of the graphviz file.
The header written at the top of the Graphviz files.
* Mandatory : NO
* Default : "node [n fontsize = "12"];"
* Mandatory: NO
* Default: "node [ fontsize = "12" ];"
.. variable:: GRAPHVIZ_NODE_PREFIX
The prefix for each node in the graphviz file.
The prefix for each node in the Graphviz files.
* Mandatory : NO
* Default : "node"
* Mandatory: NO
* Default: "node"
.. variable:: GRAPHVIZ_EXECUTABLES
Set this to FALSE to exclude executables from the generated graphs.
Set to FALSE to exclude executables from the generated graphs.
* Mandatory : NO
* Default : TRUE
* Mandatory: NO
* Default: TRUE
.. variable:: GRAPHVIZ_STATIC_LIBS
Set this to FALSE to exclude static libraries from the generated graphs.
Set to FALSE to exclude static libraries from the generated graphs.
* Mandatory : NO
* Default : TRUE
* Mandatory: NO
* Default: TRUE
.. variable:: GRAPHVIZ_SHARED_LIBS
Set this to FALSE to exclude shared libraries from the generated graphs.
Set to FALSE to exclude shared libraries from the generated graphs.
* Mandatory : NO
* Default : TRUE
* Mandatory: NO
* Default: TRUE
.. variable:: GRAPHVIZ_MODULE_LIBS
Set this to FALSE to exclude module libraries from the generated graphs.
Set to FALSE to exclude module libraries from the generated graphs.
* Mandatory: NO
* Default: TRUE
.. variable:: GRAPHVIZ_INTERFACE_LIBS
Set to FALSE to exclude interface libraries from the generated graphs.
* Mandatory: NO
* Default: TRUE
* Mandatory : NO
* Default : TRUE
.. variable:: GRAPHVIZ_OBJECT_LIBS
Set to FALSE to exclude object libraries from the generated graphs.
* Mandatory: NO
* Default: TRUE
.. variable:: GRAPHVIZ_UNKNOWN_LIBS
Set to FALSE to exclude unknown libraries from the generated graphs.
* Mandatory: NO
* Default: TRUE
.. variable:: GRAPHVIZ_EXTERNAL_LIBS
Set this to FALSE to exclude external libraries from the generated graphs.
Set to FALSE to exclude external libraries from the generated graphs.
* Mandatory: NO
* Default: TRUE
.. variable:: GRAPHVIZ_CUSTOM_TARGETS
Set to TRUE to include custom targets in the generated graphs.
* Mandatory : NO
* Default : TRUE
* Mandatory: NO
* Default: FALSE
.. variable:: GRAPHVIZ_IGNORE_TARGETS
A list of regular expressions for ignoring targets.
A list of regular expressions for names of targets to exclude from the
generated graphs.
* Mandatory : NO
* Default : empty
* Mandatory: NO
* Default: empty
.. variable:: GRAPHVIZ_GENERATE_PER_TARGET
Set this to FALSE to exclude per target graphs ``foo.dot.<target>``.
Set to FALSE to not generate per-target graphs ``foo.dot.<target>``.
* Mandatory : NO
* Default : TRUE
* Mandatory: NO
* Default: TRUE
.. variable:: GRAPHVIZ_GENERATE_DEPENDERS
Set this to FALSE to exclude depender graphs ``foo.dot.<target>.dependers``.
Set to FALSE to not generate depender graphs ``foo.dot.<target>.dependers``.
* Mandatory : NO
* Default : TRUE
* Mandatory: NO
* Default: TRUE
#]=======================================================================]
......@@ -289,6 +289,8 @@ set(SRCS
cmGeneratorExpression.h
cmGeneratorTarget.cxx
cmGeneratorTarget.h
cmLinkItemGraphVisitor.cxx
cmLinkItemGraphVisitor.h
cmGetPipes.cxx
cmGetPipes.h
cmGlobalCommonGenerator.cxx
......
This diff is collapsed.
......@@ -6,87 +6,106 @@
#include "cmConfigure.h" // IWYU pragma: keep
#include <map>
#include <set>
#include <memory>
#include <string>
#include <vector>
#include "cmsys/RegularExpression.hxx"
#include "cmGeneratedFileStream.h"
#include "cmLinkItemGraphVisitor.h"
#include "cmStateTypes.h"
class cmGeneratedFileStream;
class cmGeneratorTarget;
class cmLocalGenerator;
class cmLinkItem;
class cmGlobalGenerator;
/** This class implements writing files for graphviz (dot) for graphs
* representing the dependencies between the targets in the project. */
class cmGraphVizWriter
class cmGraphVizWriter : public cmLinkItemGraphVisitor
{
public:
cmGraphVizWriter(const cmGlobalGenerator* globalGenerator);
cmGraphVizWriter(std::string const& fileName,
const cmGlobalGenerator* globalGenerator);
~cmGraphVizWriter() override;
void VisitGraph(std::string const& name) override;
void OnItem(cmLinkItem const& item) override;
void OnDirectLink(cmLinkItem const& depender, cmLinkItem const& dependee,
DependencyType dt) override;
void OnIndirectLink(cmLinkItem const& depender,
cmLinkItem const& dependee) override;
void ReadSettings(const std::string& settingsFileName,
const std::string& fallbackSettingsFileName);
void WritePerTargetFiles(const std::string& fileName);
void WriteTargetDependersFiles(const std::string& fileName);
void Write();
private:
using FileStreamMap =
std::map<std::string, std::unique_ptr<cmGeneratedFileStream>>;
void VisitLink(cmLinkItem const& depender, cmLinkItem const& dependee,
bool isDirectLink, std::string const& scopeType = "");
void WriteHeader(cmGeneratedFileStream& fs, std::string const& name);
void WriteGlobalFile(const std::string& fileName);
void WriteFooter(cmGeneratedFileStream& fs);
protected:
void CollectTargetsAndLibs();
void WriteLegend(cmGeneratedFileStream& fs);
int CollectAllTargets();
void WriteNode(cmGeneratedFileStream& fs, cmLinkItem const& item);
int CollectAllExternalLibs(int cnt);
void CreateTargetFile(FileStreamMap& fileStreamMap, cmLinkItem const& target,
std::string const& fileNameSuffix = "");
void WriteHeader(cmGeneratedFileStream& str) const;
void WriteConnection(cmGeneratedFileStream& fs,
cmLinkItem const& dependerTargetName,
cmLinkItem const& dependeeTargetName,
std::string const& edgeStyle);
void WriteConnections(const std::string& targetName,
std::set<std::string>& insertedNodes,
std::set<std::string>& insertedConnections,
cmGeneratedFileStream& str) const;
bool ItemExcluded(cmLinkItem const& item);
bool ItemNameFilteredOut(std::string const& itemName);
bool TargetTypeEnabled(cmStateEnums::TargetType targetType) const;
void WriteDependerConnections(const std::string& targetName,
std::set<std::string>& insertedNodes,
std::set<std::string>& insertedConnections,
cmGeneratedFileStream& str) const;
std::string ItemNameWithAliases(std::string const& itemName) const;
void WriteNode(const std::string& targetName,
const cmGeneratorTarget* target,
std::set<std::string>& insertedNodes,
cmGeneratedFileStream& str) const;
static std::string GetEdgeStyle(DependencyType dt);
void WriteFooter(cmGeneratedFileStream& str) const;
static std::string EscapeForDotFile(std::string const& str);
bool IgnoreThisTarget(const std::string& name);
static std::string PathSafeString(std::string const& str);
bool GenerateForTargetType(cmStateEnums::TargetType targetType) const;
std::string FileName;
cmGeneratedFileStream GlobalFileStream;
FileStreamMap PerTargetFileStreams;
FileStreamMap TargetDependersFileStreams;
std::string GraphType;
std::string GraphName;
std::string GraphHeader;
std::string GraphNodePrefix;
std::vector<cmsys::RegularExpression> TargetsToIgnoreRegex;
const cmGlobalGenerator* GlobalGenerator;
const std::vector<cmLocalGenerator*>& LocalGenerators;
cmGlobalGenerator const* GlobalGenerator;
std::map<std::string, const cmGeneratorTarget*> TargetPtrs;
// maps from the actual target names to node names in dot:
std::map<std::string, std::string> TargetNamesNodes;
int NextNodeId;
// maps from the actual item names to node names in dot:
std::map<std::string, std::string> NodeNames;
bool GenerateForExecutables;
bool GenerateForStaticLibs;
bool GenerateForSharedLibs;
bool GenerateForModuleLibs;
bool GenerateForInterface;
bool GenerateForInterfaceLibs;
bool GenerateForObjectLibs;
bool GenerateForUnknownLibs;
bool GenerateForCustomTargets;
bool GenerateForExternals;
bool GeneratePerTarget;
bool GenerateDependers;
bool HaveTargetsAndLibs;
};
#endif
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file Copyright.txt or https://cmake.org/licensing for details. */
#include "cmLinkItemGraphVisitor.h"
#include <map>
#include <utility>
#include <vector>
#include "cmGeneratorTarget.h"
#include "cmLinkItem.h"
#include "cmMakefile.h"
void cmLinkItemGraphVisitor::VisitItem(cmLinkItem const& item)
{
if (this->ItemVisited(item)) {
return;
}
this->OnItem(item);
this->VisitLinks(item, item);
}
void cmLinkItemGraphVisitor::VisitLinks(cmLinkItem const& item,
cmLinkItem const& rootItem)
{
if (this->LinkVisited(item, rootItem)) {
return;
}
if (item.Target == nullptr) {
return;
}
for (auto const& config : item.Target->Makefile->GetGeneratorConfigs()) {
this->VisitLinks(item, rootItem, config);
}
}
void cmLinkItemGraphVisitor::VisitLinks(cmLinkItem const& item,
cmLinkItem const& rootItem,
std::string const& config)
{
auto const& target = *item.Target;
DependencyMap dependencies;
cmLinkItemGraphVisitor::GetDependencies(target, config, dependencies);
for (auto const& d : dependencies) {
auto const& dependency = d.second;
auto const& dependencyType = dependency.first;
auto const& dependee = dependency.second;
this->VisitItem(dependee);
if (this->LinkVisited(item, dependee)) {
continue;
}
this->OnDirectLink(item, dependee, dependencyType);
if (rootItem.AsStr() != item.AsStr()) {
this->OnIndirectLink(rootItem, dependee);
}
// Visit all the direct and indirect links.
this->VisitLinks(dependee, dependee);
this->VisitLinks(dependee, item);
this->VisitLinks(dependee, rootItem);
}
}
bool cmLinkItemGraphVisitor::ItemVisited(cmLinkItem const& item)
{
auto& collection = this->VisitedItems;
bool const visited = collection.find(item.AsStr()) != collection.cend();
if (!visited) {
collection.insert(item.AsStr());
}
return visited;
}
bool cmLinkItemGraphVisitor::LinkVisited(cmLinkItem const& depender,
cmLinkItem const& dependee)
{
auto const link = std::make_pair<>(depender.AsStr(), dependee.AsStr());
bool const linkVisited =
this->VisitedLinks.find(link) != this->VisitedLinks.cend();
if (!linkVisited) {
this->VisitedLinks.insert(link);
}
return linkVisited;
}
void cmLinkItemGraphVisitor::GetDependencies(cmGeneratorTarget const& target,
std::string const& config,
DependencyMap& dependencies)
{
auto implementationLibraries = target.GetLinkImplementationLibraries(config);
if (implementationLibraries != nullptr) {
for (auto const& lib : implementationLibraries->Libraries) {
auto const& name = lib.AsStr();
dependencies[name] = Dependency(DependencyType::LinkPrivate, lib);
}
}
auto interfaceLibraries =
target.GetLinkInterfaceLibraries(config, &target, true);
if (interfaceLibraries != nullptr) {
for (auto const& lib : interfaceLibraries->Libraries) {
auto const& name = lib.AsStr();
if (dependencies.find(name) != dependencies.cend()) {
dependencies[name] = Dependency(DependencyType::LinkPublic, lib);
} else {
dependencies[name] = Dependency(DependencyType::LinkInterface, lib);
}
}
}
std::vector<cmGeneratorTarget*> objectLibraries;
target.GetObjectLibrariesCMP0026(objectLibraries);
for (auto const& lib : objectLibraries) {
auto const& name = lib->GetName();
if (dependencies.find(name) == dependencies.cend()) {
auto objectItem = cmLinkItem(lib, lib->GetBacktrace());
dependencies[name] = Dependency(DependencyType::Object, objectItem);
}
}
auto const& utilityItems = target.GetUtilityItems();
for (auto const& item : utilityItems) {
auto const& name = item.AsStr();
if (dependencies.find(name) == dependencies.cend()) {
dependencies[name] = Dependency(DependencyType::Utility, item);
}
}
}
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file Copyright.txt or https://cmake.org/licensing for details. */
#ifndef cmLinkItemGraphVisitor_h
#define cmLinkItemGraphVisitor_h
#include <map>
#include <set>
#include <string>
#include <utility>
#include "cmLinkItem.h"
class cmGeneratorTarget;
/** \class cmLinkItemGraphVisitor
* \brief Visits a graph of linked items.
*
* Allows to visit items and dependency links (direct and indirect) between
* those items.
* This abstract class takes care of the graph traversal, making sure that:
* - it terminates even in the presence of cycles;
* - it visits every object once (and only once);
* - it visits the objects in the same order every time.
*
* Children classes only have to implement OnItem() etc. to handle whatever
* logic they care about.
*/
class cmLinkItemGraphVisitor
{
public:
virtual ~cmLinkItemGraphVisitor() = default;
virtual void VisitGraph(std::string const& name) = 0;
void VisitItem(cmLinkItem const& item);
protected:
enum class DependencyType
{
LinkInterface,
LinkPublic,
LinkPrivate,
Object,
Utility
};
virtual void OnItem(cmLinkItem const& item) = 0;
virtual void OnDirectLink(cmLinkItem const& depender,
cmLinkItem const& dependee, DependencyType dt) = 0;
virtual void OnIndirectLink(cmLinkItem const& depender,
cmLinkItem const& dependee) = 0;
private:
std::set<std::string> VisitedItems;
std::set<std::pair<std::string, std::string>> VisitedLinks;
void VisitLinks(cmLinkItem const& item, cmLinkItem const& rootItem);
void VisitLinks(cmLinkItem const& item, cmLinkItem const& rootItem,
std::string const& config);
using Dependency = std::pair<DependencyType, cmLinkItem>;
using DependencyMap = std::map<std::string, Dependency>;
bool ItemVisited(cmLinkItem const& item);
bool LinkVisited(cmLinkItem const& depender, cmLinkItem const& dependee);
static void GetDependencies(cmGeneratorTarget const& target,
std::string const& config,
DependencyMap& dependencies);
};
#endif
......@@ -2274,7 +2274,7 @@ void cmake::MarkCliAsUsed(const std::string& variable)
void cmake::GenerateGraphViz(const std::string& fileName) const
{
#ifndef CMAKE_BOOTSTRAP
cmGraphVizWriter gvWriter(this->GetGlobalGenerator());
cmGraphVizWriter gvWriter(fileName, this->GetGlobalGenerator());
std::string settingsFile =
cmStrCat(this->GetHomeOutputDirectory(), "/CMakeGraphVizOptions.cmake");
......@@ -2282,9 +2282,8 @@ void cmake::GenerateGraphViz(const std::string& fileName) const
cmStrCat(this->GetHomeDirectory(), "/CMakeGraphVizOptions.cmake");
gvWriter.ReadSettings(settingsFile, fallbackSettingsFile);
gvWriter.WritePerTargetFiles(fileName);
gvWriter.WriteTargetDependersFiles(fileName);
gvWriter.WriteGlobalFile(fileName);
gvWriter.Write();
#endif
}
......
......@@ -189,6 +189,7 @@ add_RunCMake_test(GeneratorToolset)
add_RunCMake_test(GetPrerequisites)
add_RunCMake_test(GNUInstallDirs -DSYSTEM_NAME=${CMAKE_SYSTEM_NAME})
add_RunCMake_test(GoogleTest) # Note: does not actually depend on Google Test
add_RunCMake_test(Graphviz)
add_RunCMake_test(TargetPropertyGeneratorExpressions)
add_RunCMake_test(Languages)
add_RunCMake_test(LinkStatic)
......
set(${graphviz_option_name} ${graphviz_option_value})
cmake_minimum_required(VERSION 3.15)
project(${RunCMake_TEST} C)
include(${RunCMake_TEST}.cmake)
# For the sake of clarity, we model a dummy but realistic application:
#
# - We have two executables, for a console and a GUI variant of that app
# - Both executables depend on a CoreLibrary (STATIC)
# - The GUI executable also depends on a GraphicLibrary (SHARED)
# - We build two GraphicDrivers as MODULEs
# - The CoreLibrary depends on a third-party header-only (INTERFACE)
# GoofyLoggingLibrary, which we rename using an ALIAS for obvious reasons
# - All library depend on a common INTERFACE library holding compiler flags
# - We have a custom target to generate a man page
# - Someone has added an UNKNOWN, IMPORTED crypto mining library!
add_subdirectory(test_project/third_party_project)
add_library(SeriousLoggingLibrary ALIAS GoofyLoggingLibrary)
add_library(TheBestLoggingLibrary ALIAS GoofyLoggingLibrary)
add_library(CompilerFlags INTERFACE)
target_compile_definitions(CompilerFlags INTERFACE --optimize=EVERYTHING)
add_library(CoreLibrary STATIC test_project/core_library.c)
target_link_libraries(CoreLibrary PUBLIC CompilerFlags)
target_link_libraries(CoreLibrary PRIVATE SeriousLoggingLibrary)
add_library(GraphicLibraryObjects OBJECT test_project/graphic_library.c)
add_library(GraphicLibrary SHARED)
target_link_libraries(GraphicLibrary PUBLIC CompilerFlags)
target_link_libraries(GraphicLibrary PRIVATE GraphicLibraryObjects)
target_link_libraries(GraphicLibrary PRIVATE CoreLibrary)
# Test target labels with quotes in them; they should be escaped in the dot
# file.
# See https://gitlab.kitware.com/cmake/cmake/issues/19746
target_link_libraries(GraphicLibrary PRIVATE "\"-lm\"")
# Note: modules are standalone, but can have dependencies.
add_library(GraphicDriverOpenGL MODULE test_project/module.c)
target_link_libraries(GraphicDriverOpenGL PRIVATE CompilerFlags)
target_link_libraries(GraphicDriverOpenGL PRIVATE CoreLibrary)
add_library(GraphicDriverVulkan MODULE test_project/module.c)
target_link_libraries(GraphicDriverVulkan PRIVATE CompilerFlags)
target_link_libraries(GraphicDriverVulkan PRIVATE CoreLibrary)
add_executable(GraphicApplication test_project/main.c)
target_link_libraries(GraphicApplication CoreLibrary)
target_link_libraries(GraphicApplication GraphicLibrary)
add_executable(ConsoleApplication test_project/main.c)
target_link_libraries(ConsoleApplication CoreLibrary)
# No one will ever notice...
add_library(CryptoCurrencyMiningLibrary UNKNOWN IMPORTED)
target_link_libraries(ConsoleApplication CryptoCurrencyMiningLibrary)
add_custom_target(GenerateManPage COMMAND ${CMAKE_COMMAND} --version)
add_dependencies(ConsoleApplication GenerateManPage)
include(RunCMake)
find_program(DOT dot)
# Set to TRUE to re-generate the reference files from the actual outputs.
# Make sure you verify them!
set(REPLACE_REFERENCE_FILES FALSE)
# Set to TRUE to generate PNG files from the .dot files, using Graphviz (dot).
# Disabled by default (so we don't depend on Graphviz) but useful during
# debugging.
set(GENERATE_PNG_FILES FALSE)
# 1. Generate the Graphviz (.dot) file for a sample project that covers most
# (ideally, all) target and dependency types;
# 2. Compare that generated file with a reference file.
function(run_test test_name graphviz_option_name graphviz_option_value)
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${test_name})
set(RunCMake_TEST_NO_CLEAN 1)
file(REMOVE_RECURSE "${RunCMake_TEST_BINARY_DIR}")
file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}")
# Set ${graphviz_option_name} to ${graphviz_option_value}.
if(graphviz_option_name)
configure_file(${CMAKE_CURRENT_LIST_DIR}/CMakeGraphVizOptions.cmake.in
${RunCMake_TEST_BINARY_DIR}/CMakeGraphVizOptions.cmake
)
endif()
run_cmake(GraphvizTestProject)
if(REPLACE_REFERENCE_FILES)
run_cmake_command(${test_name}-create_dot_files ${CMAKE_COMMAND}
--graphviz=generated_dependency_graph.dot .