view trunk/src/cmd/ImportGraph.d @ 427:e2bbc6406a14

Added a new option '-m' to the igraph command.
author Aziz K?ksal <aziz.koeksal@gmail.com>
date Mon, 01 Oct 2007 18:49:26 +0200
parents 33b566df6af4
children 7a6bfa569a52
line wrap: on
line source

/++
  Author: Aziz Köksal
  License: GPL3
+/
module cmd.ImportGraph;
import dil.SyntaxTree;
import dil.Declarations;
import dil.Token;
import dil.Parser, dil.Lexer;
import dil.File;
import dil.Module;
import dil.Settings;
import tango.text.Regex : RegExp = Regex;
import tango.io.FilePath;
import tango.io.FileConst;
import tango.text.Util;
import common;

alias FileConst.PathSeparatorChar dirSep;

enum IGraphOption
{
  None,
  IncludeUnlocatableModules = 1,
  PrintDot                  = 1<<1,
  HighlightCyclicEdges      = 1<<2,
  HighlightCyclicVertices   = 1<<3,
  GroupByPackageNames       = 1<<4,
  GroupByFullPackageName    = 1<<5,
  PrintPaths                = 1<<6,
  PrintList                 = 1<<7,
  MarkCyclicModules         = 1<<8,
}

string findModulePath(string moduleFQN, string[] importPaths)
{
  string modulePath;
  foreach (path; importPaths)
  {
    modulePath = path ~ (path[$-1] == dirSep ? "" : [dirSep]) ~ moduleFQN ~ ".d";
    // TODO: also check for *.di?
    if ((new FilePath(modulePath)).exists())
      return modulePath;
  }
  return null;
}

class Edge
{
  Vertex outgoing;
  Vertex incoming;
  bool isCyclic;
  this(Vertex o, Vertex i)
  {
    this.outgoing = o;
    this.incoming = i;
  }
}

class Vertex : Module
{
  uint id;
  Vertex[] incoming;
  bool isCyclic; /// Whether this vertex is in a cyclic relationship with other vertices.

  this(string filePath)
  {
    super(filePath, true);
  }

  Vertex[] outgoing()
  {
    return cast(Vertex[])super.modules;
  }
}

void execute(string filePath, string[] importPaths, string[] strRegexps, uint levels, IGraphOption options)
{
  // Init regular expressions.
  RegExp[] regexps;
  foreach (strRegexp; strRegexps)
    regexps ~= new RegExp(strRegexp);

  // Add directory of file and global directories to import paths.
  auto fileDir = (new FilePath(filePath)).folder();
  if (fileDir.length)
    importPaths ~= fileDir;
  importPaths ~= GlobalSettings.importPaths;

  Vertex[string] loadedModules;
  Vertex[] loadedModulesList; // Ordered list of loaded modules.
  Edge edges[];

  Vertex loadModule(string moduleFQNPath)
  {
    auto mod_ = moduleFQNPath in loadedModules;
    if (mod_ !is null)
      return *mod_; // Return already loaded module.

    // Ignore module names matching regular expressions.
    foreach (rx; regexps)
      if (rx.test(replace(moduleFQNPath, dirSep, '.')))
        return null;

    auto modulePath = findModulePath(moduleFQNPath, importPaths);
    Vertex mod;
    if (modulePath is null)
    {
      if (options & IGraphOption.IncludeUnlocatableModules)
      {
        mod = new Vertex(null);
        mod.setFQN(replace(moduleFQNPath, dirSep, '.'));
        loadedModules[moduleFQNPath] = mod;
        loadedModulesList ~= mod;
        mod.id = loadedModulesList.length -1;
      }
    }
    else
    {
// writefln(modulePath);
      mod = new Vertex(modulePath);
      mod.parse();

      loadedModules[moduleFQNPath] = mod;
      loadedModulesList ~= mod;
      mod.id = loadedModulesList.length -1;

      auto moduleFQNs = mod.getImports();

      foreach (moduleFQN_; moduleFQNs)
      {
        auto loaded_mod = loadModule(moduleFQN_);
        if (loaded_mod !is null)
        {
          edges ~= new Edge(mod, loaded_mod);
          mod.modules ~= loaded_mod;
          loaded_mod.incoming ~= mod;
        }
      }
    }
    return mod;
  }

  auto mod = new Vertex(filePath);
  mod.parse();

  auto moduleFQNs = mod.getImports();

  loadedModules[mod.getFQNPath()] = mod;
  loadedModulesList ~= mod;
  mod.id = 0; // loadedModulesList.length -1

  foreach (moduleFQN_; moduleFQNs)
  {
    auto loaded_mod = loadModule(moduleFQN_);
    if (loaded_mod !is null)
    {
      edges ~= new Edge(mod, loaded_mod);
      mod.modules ~= loaded_mod;
      loaded_mod.incoming ~= mod;
    }
  }
  // Finished loading modules.


  if (options & (IGraphOption.PrintList | IGraphOption.PrintPaths))
  {
    if (options & IGraphOption.MarkCyclicModules)
      analyzeGraph(loadedModulesList, edges.dup);

    if (options & IGraphOption.PrintPaths)
      printPaths(loadedModulesList, levels+1, "");
    else
      printList(loadedModulesList, levels+1, "");
  }
  else 
    printDot(loadedModulesList, edges, options);
}

void printPaths(Vertex[] vertices, uint level, char[] indent)
{
  if (!level)
    return;
  foreach (vertex; vertices)
  {
    Stdout(indent)((vertex.isCyclic?"*":"")~vertex.filePath).newline;
    if (vertex.outgoing.length)
      printPaths(vertex.outgoing, level-1, indent~"  ");
  }
}

void printList(Vertex[] vertices, uint level, char[] indent)
{
  if (!level)
    return;
  foreach (vertex; vertices)
  {
    Stdout(indent)((vertex.isCyclic?"*":"")~vertex.getFQN()).newline;
    if (vertex.outgoing.length)
      printList(vertex.outgoing, level-1, indent~"  ");
  }
}

void printDot(Vertex[] loadedModulesList, Edge[] edges, IGraphOption options)
{
  Vertex[][string] verticesByPckgName;
  if (options & IGraphOption.GroupByFullPackageName)
    foreach (module_; loadedModulesList)
      verticesByPckgName[module_.packageName] ~= module_;

  if (options & (IGraphOption.HighlightCyclicVertices |
                 IGraphOption.HighlightCyclicEdges))
    analyzeGraph(loadedModulesList, edges.dup);

  Stdout("Digraph ModuleDependencies\n{\n");

  if (options & IGraphOption.HighlightCyclicVertices)
    foreach (i, module_; loadedModulesList)
      Stdout.format(`  n{0} [label="{1}"{2}];`, i, module_.getFQN(), (module_.isCyclic ? ",style=filled,fillcolor=tomato" : "")).newline;
  else
    foreach (i, module_; loadedModulesList)
      Stdout.format(`  n{0} [label="{1}"];`, i, module_.getFQN()).newline;

  foreach (edge; edges)
    Stdout.format(`  n{0} -> n{1}{2};`, edge.outgoing.id, edge.incoming.id, (edge.isCyclic ? "[color=red]" : "")).newline;

  if (options & IGraphOption.GroupByFullPackageName)
    foreach (packageName, vertices; verticesByPckgName)
    {
      Stdout.format(`  subgraph "cluster_{0}" {`\n`    label="{1}";color=blue;`"\n    ", packageName, packageName);
      foreach (module_; vertices)
        Stdout.format(`n{0};`, module_.id);
      Stdout("\n  }\n");
    }

  Stdout("}\n");
}

void analyzeGraph(Vertex[] vertices, Edge[] edges)
{
  void recursive(Vertex[] modules)
  {
    foreach (idx, vertex; vertices)
    {
      uint outgoing, incoming;
      foreach (j, edge; edges)
      {
        if (edge.outgoing is vertex)
          outgoing++;
        if (edge.incoming is vertex)
          incoming++;
      }

      if (outgoing == 0)
      {
        if (incoming != 0)
        {
          // Vertex is a sink.
          alias outgoing i; // Reuse
          alias incoming j; // Reuse
          // Remove edges.
          for (i=j=0; i < edges.length; i++)
            if (edges[i].incoming !is vertex)
              edges[j++] = edges[i];
          edges.length = j;
          vertices = vertices[0..idx] ~ vertices[idx+1..$];
          return recursive(modules);
        }
        else
          assert(0, "orphaned module: "~vertex.getFQN()~" (has no edges in graph)"); // orphaned vertex (module) in graph
      }
      else if (incoming == 0)
      {
        // Vertex is a source
        alias outgoing i; // Reuse
        alias incoming j; // Reuse
        // Remove edges.
        for (i=j=0; i < edges.length; i++)
          if (edges[i].outgoing !is vertex)
            edges[j++] = edges[i];
        edges.length = j;
        vertices = vertices[0..idx] ~ vertices[idx+1..$];
        return recursive(modules);
      }
//       else
//       {
//         // source && sink
//       }
    }

    // When reaching this point it means only cylic edges and vertices are left.
    foreach (vertex; vertices)
      vertex.isCyclic = true;
    foreach (edge; edges)
      if (edge)
        edge.isCyclic = true;
  }
  recursive(vertices);
}