1#!/usr/bin/env python
2# Copyright 2014 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6import argparse
7from checker import Checker as Checker
8import os
9import sys
10
11try:
12  import json
13except:
14  import simplejson as json
15
16
17class Module(object):
18  def __init__(self, name, sources, depends=[], externs=[]):
19    self.name = name
20    self.sources = sources
21    # TODO(dbeam): support depending on other modules/dependency flattening.
22    self.depends = depends
23    self.externs = externs
24
25  @staticmethod
26  def from_dict(d):
27    keys = d.keys()
28
29    required = ["name", "sources"]
30    assert all(r in keys for r in required), "Module missing name or sources"
31
32    allowed = required + ["depends", "externs"]
33    assert all(k in allowed for k in keys), "Module has unknown key"
34
35    depends = d["depends"] if "depends" in d else []
36    externs = d["externs"] if "externs" in d else []
37    return Module(d["name"], d["sources"], depends=depends, externs=externs)
38
39
40# TODO(dbeam): should ModuleParser be internal to ModuleCompiler or should we
41# pass Modules into ModuleCompiler.compile()? Maybe this is fine?
42class ModuleParser(object):
43  _cache = {}
44
45  def __init__(self, verbose=False):
46    self._verbose = verbose
47
48  def parse(self, file_path):
49    if file_path in self._cache:
50      print "(INFO) Found module file %s in the cache" % file_path
51      return self._cache[file_path]
52
53    file = open(file_path, "r")
54    data = json.load(file)
55    file.close()
56
57    if self._verbose:
58      pretty_json = json.dumps(data, indent=2, separators=(',', ': ')).strip()
59      print "(INFO) Layout: " + os.linesep + pretty_json + os.linesep
60
61    self._cache[file_path] = [Module.from_dict(m) for m in data]
62    return self._cache[file_path]
63
64
65class ModuleCompiler(object):
66  _checker = None
67  _parser = None
68
69  def __init__(self, verbose=False):
70    self._verbose = verbose
71
72  def _debug(self, msg, prefix="(INFO) ", suffix=""):
73    if self._verbose:
74      print prefix + msg.strip() + suffix
75
76  def compile(self, module_file):
77    self._debug("MODULE FILE: " + module_file, prefix="")
78
79    # NOTE: It's possible but unlikely that |_checker| or |_parser|'s verbosity
80    # isn't the same as |self._verbose| due to this class being called with
81    # verbose=False then verbose=True in the same program.
82    self._parser = self._parser or ModuleParser(verbose=self._verbose)
83    self._checker = self._checker or Checker(verbose=self._verbose)
84
85    current_dir = os.getcwd()
86    module_dir = os.path.dirname(module_file)
87    rel_path = lambda f: f
88
89    if current_dir and module_dir:
90      here_to_module_dir = os.path.relpath(module_dir, current_dir)
91      if here_to_module_dir:
92        rel_path = lambda f: os.path.join(here_to_module_dir, f)
93
94    modules = self._parser.parse(module_file)
95
96    for m in modules:
97      self._debug("MODULE: " + m.name, prefix="", suffix=os.linesep)
98
99      for s in m.sources:
100        depends = [rel_path(d) for d in m.depends]
101        externs = [rel_path(e) for e in m.externs]
102        exit_code, _ = self._checker.check(rel_path(s), depends=depends,
103                                           externs=externs)
104        if exit_code:
105          sys.exit(exit_code)
106
107        if s != m.sources[-1]:
108          self._debug(os.linesep, prefix="")
109
110      if m != modules[-1]:
111        self._debug(os.linesep, prefix="")
112
113
114def main(opts):
115  module_compiler = ModuleCompiler(verbose=opts.verbose)
116  for module_file in opts.module_file:
117    module_compiler.compile(module_file)
118
119
120if __name__ == "__main__":
121  parser = argparse.ArgumentParser(
122      description="Typecheck JavaScript using Closure compiler")
123  parser.add_argument("-v", "--verbose", action="store_true",
124                      help="Show more information as this script runs")
125  parser.add_argument("module_file", nargs=argparse.ONE_OR_MORE,
126                      help="Path to a modules file to check")
127  main(parser.parse_args())
128