1b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang# Copyright 2013 The Chromium Authors. All rights reserved.
2b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang# Use of this source code is governed by a BSD-style license that can be
3b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang# found in the LICENSE file.
4b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
5b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wangimport collections
6b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wangimport os
7b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wangimport cStringIO
8b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
9b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wangfrom py_vulcanize import resource_loader
10b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
11b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
12b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wangdef _FindAllFilesRecursive(source_paths):
13b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  all_filenames = set()
14b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  for source_path in source_paths:
15b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    for dirpath, _, filenames in os.walk(source_path):
16b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang      for f in filenames:
17b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang        if f.startswith('.'):
18b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang          continue
19b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang        x = os.path.abspath(os.path.join(dirpath, f))
20b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang        all_filenames.add(x)
21b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  return all_filenames
22b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
23b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
24b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wangclass AbsFilenameList(object):
25b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
26b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def __init__(self, willDirtyCallback):
27b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    self._willDirtyCallback = willDirtyCallback
28b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    self._filenames = []
29b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    self._filenames_set = set()
30b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
31b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def _WillBecomeDirty(self):
32b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    if self._willDirtyCallback:
33b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang      self._willDirtyCallback()
34b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
35b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def append(self, filename):
36b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    assert os.path.isabs(filename)
37b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    self._WillBecomeDirty()
38b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    self._filenames.append(filename)
39b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    self._filenames_set.add(filename)
40b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
41b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def extend(self, iterable):
42b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    self._WillBecomeDirty()
43b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    for filename in iterable:
44b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang      assert os.path.isabs(filename)
45b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang      self._filenames.append(filename)
46b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang      self._filenames_set.add(filename)
47b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
48b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def appendRel(self, basedir, filename):
49b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    assert os.path.isabs(basedir)
50b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    self._WillBecomeDirty()
51b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    n = os.path.abspath(os.path.join(basedir, filename))
52b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    self._filenames.append(n)
53b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    self._filenames_set.add(n)
54b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
55b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def extendRel(self, basedir, iterable):
56b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    self._WillBecomeDirty()
57b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    assert os.path.isabs(basedir)
58b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    for filename in iterable:
59b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang      n = os.path.abspath(os.path.join(basedir, filename))
60b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang      self._filenames.append(n)
61b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang      self._filenames_set.add(n)
62b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
63b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def __contains__(self, x):
64b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    return x in self._filenames_set
65b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
66b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def __len__(self):
67b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    return self._filenames.__len__()
68b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
69b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def __iter__(self):
70b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    return iter(self._filenames)
71b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
72b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def __repr__(self):
73b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    return repr(self._filenames)
74b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
75b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def __str__(self):
76b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    return str(self._filenames)
77b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
78b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
79b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wangclass Project(object):
80b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
81b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  py_vulcanize_path = os.path.abspath(os.path.join(
82b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang      os.path.dirname(__file__), '..'))
83b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
84b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def __init__(self, source_paths=None):
85b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    """
86b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    source_paths: A list of top-level directories in which modules and raw
87b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang        scripts can be found. Module paths are relative to these directories.
88b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    """
89b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    self._loader = None
90b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    self._frozen = False
91b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    self.source_paths = AbsFilenameList(self._WillPartOfPathChange)
92b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
93b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    if source_paths is not None:
94b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang      self.source_paths.extend(source_paths)
95b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
96b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def Freeze(self):
97b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    self._frozen = True
98b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
99b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def _WillPartOfPathChange(self):
100b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    if self._frozen:
101b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang      raise Exception('The project is frozen. You cannot edit it now')
102b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    self._loader = None
103b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
104b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  @staticmethod
105b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def FromDict(d):
106b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    return Project(d['source_paths'])
107b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
108b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def AsDict(self):
109b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    return {
110b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang        'source_paths': list(self.source_paths)
111b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    }
112b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
113b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def __repr__(self):
114b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    return "Project(%s)" % repr(self.source_paths)
115b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
116b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def AddSourcePath(self, path):
117b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    self.source_paths.append(path)
118b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
119b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  @property
120b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def loader(self):
121b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    if self._loader is None:
122b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang      self._loader = resource_loader.ResourceLoader(self)
123b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    return self._loader
124b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
125b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def ResetLoader(self):
126b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    self._loader = None
127b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
128b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def _Load(self, filenames):
129b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    return [self.loader.LoadModule(module_filename=filename) for
130b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang            filename in filenames]
131b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
132b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def LoadModule(self, module_name=None, module_filename=None):
133b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    return self.loader.LoadModule(module_name=module_name,
134b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang                                  module_filename=module_filename)
135b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
136b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def CalcLoadSequenceForModuleNames(self, module_names,
137b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang                                     excluded_scripts=None):
138b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    modules = [self.loader.LoadModule(module_name=name,
139b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang                                      excluded_scripts=excluded_scripts) for
140b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang               name in module_names]
141b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    return self.CalcLoadSequenceForModules(modules)
142b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
143b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def CalcLoadSequenceForModules(self, modules):
144b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    already_loaded_set = set()
145b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    load_sequence = []
146b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    for m in modules:
147b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang      m.ComputeLoadSequenceRecursive(load_sequence, already_loaded_set)
148b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    return load_sequence
149b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
150b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def GetDepsGraphFromModuleNames(self, module_names):
151b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    modules = [self.loader.LoadModule(module_name=name) for
152b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang               name in module_names]
153b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    return self.GetDepsGraphFromModules(modules)
154b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
155b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def GetDepsGraphFromModules(self, modules):
156b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    load_sequence = self.CalcLoadSequenceForModules(modules)
157b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    g = _Graph()
158b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    for m in load_sequence:
159b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang      g.AddModule(m)
160b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
161b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang      for dep in m.dependent_modules:
162b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang        g.AddEdge(m, dep.id)
163b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
164b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    # FIXME: _GetGraph is not defined. Maybe `return g` is intended?
165b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    return _GetGraph(load_sequence)
166b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
167b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def GetDominatorGraphForModulesNamed(self, module_names, load_sequence):
168b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    modules = [self.loader.LoadModule(module_name=name)
169b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang               for name in module_names]
170b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    return self.GetDominatorGraphForModules(modules, load_sequence)
171b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
172b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def GetDominatorGraphForModules(self, start_modules, load_sequence):
173b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    modules_by_id = {}
174b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    for m in load_sequence:
175b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang      modules_by_id[m.id] = m
176b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
177b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    module_referrers = collections.defaultdict(list)
178b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    for m in load_sequence:
179b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang      for dep in m.dependent_modules:
180b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang        module_referrers[dep].append(m)
181b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
182b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    # Now start at the top module and reverse.
183b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    visited = set()
184b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    g = _Graph()
185b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
186b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    pending = collections.deque()
187b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    pending.extend(start_modules)
188b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    while len(pending):
189b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang      cur = pending.pop()
190b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
191b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang      g.AddModule(cur)
192b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang      visited.add(cur)
193b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
194b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang      for out_dep in module_referrers[cur]:
195b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang        if out_dep in visited:
196b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang          continue
197b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang        g.AddEdge(out_dep, cur)
198b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang        visited.add(out_dep)
199b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang        pending.append(out_dep)
200b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
201b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    # Visited -> Dot
202b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    return g.GetDot()
203b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
204b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
205b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wangclass _Graph(object):
206b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
207b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def __init__(self):
208b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    self.nodes = []
209b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    self.edges = []
210b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
211b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def AddModule(self, m):
212b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    f = cStringIO.StringIO()
213b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    m.AppendJSContentsToFile(f, False, None)
214b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
215b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    attrs = {
216b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang        'label': '%s (%i)' % (m.name, f.tell())
217b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    }
218b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
219b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    f.close()
220b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
221b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    attr_items = ['%s="%s"' % (x, y) for x, y in attrs.iteritems()]
222b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    node = 'M%i [%s];' % (m.id, ','.join(attr_items))
223b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    self.nodes.append(node)
224b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
225b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def AddEdge(self, mFrom, mTo):
226b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    edge = 'M%i -> M%i;' % (mFrom.id, mTo.id)
227b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    self.edges.append(edge)
228b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang
229b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang  def GetDot(self):
230b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang    return 'digraph deps {\n\n%s\n\n%s\n}\n' % (
231b2cf025c7d5cebd43084f38c6c7ff9cc17da428aWei Wang        '\n'.join(self.nodes), '\n'.join(self.edges))
232