1#!/usr/bin/env python
2#
3# Copyright (c) 2013 The Chromium Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Runs Android's lint tool."""
8
9
10import optparse
11import os
12import sys
13from xml.dom import minidom
14
15from util import build_utils
16
17
18_SRC_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__),
19                                         '..', '..', '..'))
20
21
22def _RunLint(lint_path, config_path, processed_config_path, manifest_path,
23             result_path, product_dir, src_dirs, classes_dir):
24
25  def _RelativizePath(path):
26    """Returns relative path to top-level src dir.
27
28    Args:
29      path: A path relative to cwd.
30    """
31    return os.path.relpath(os.path.abspath(path), _SRC_ROOT)
32
33  def _ProcessConfigFile():
34    if not build_utils.IsTimeStale(processed_config_path, [config_path]):
35      return
36
37    with open(config_path, 'rb') as f:
38      content = f.read().replace(
39          'PRODUCT_DIR', _RelativizePath(product_dir))
40
41    with open(processed_config_path, 'wb') as f:
42      f.write(content)
43
44  def _ProcessResultFile():
45    with open(result_path, 'rb') as f:
46      content = f.read().replace(
47          _RelativizePath(product_dir), 'PRODUCT_DIR')
48
49    with open(result_path, 'wb') as f:
50      f.write(content)
51
52  def _ParseAndShowResultFile():
53    dom = minidom.parse(result_path)
54    issues = dom.getElementsByTagName('issue')
55    print >> sys.stderr
56    for issue in issues:
57      issue_id = issue.attributes['id'].value
58      severity = issue.attributes['severity'].value
59      message = issue.attributes['message'].value
60      location_elem = issue.getElementsByTagName('location')[0]
61      path = location_elem.attributes['file'].value
62      line = location_elem.getAttribute('line')
63      if line:
64        error = '%s:%s %s: %s [%s]' % (path, line, severity, message,
65                                       issue_id)
66      else:
67        # Issues in class files don't have a line number.
68        error = '%s %s: %s [%s]' % (path, severity, message, issue_id)
69      print >> sys.stderr, error
70      for attr in ['errorLine1', 'errorLine2']:
71        error_line = issue.getAttribute(attr)
72        if error_line:
73          print >> sys.stderr, error_line
74    return len(issues)
75
76  _ProcessConfigFile()
77
78  cmd = [
79      lint_path, '-Werror', '--exitcode', '--showall',
80      '--config', _RelativizePath(processed_config_path),
81      '--classpath', _RelativizePath(classes_dir),
82      '--xml', _RelativizePath(result_path),
83  ]
84  for src in src_dirs:
85    cmd.extend(['--sources', _RelativizePath(src)])
86  cmd.append(_RelativizePath(os.path.join(manifest_path, os.pardir)))
87
88  if os.path.exists(result_path):
89    os.remove(result_path)
90
91  try:
92    build_utils.CheckOutput(cmd, cwd=_SRC_ROOT)
93  except build_utils.CalledProcessError:
94    # There is a problem with lint usage
95    if not os.path.exists(result_path):
96      raise
97    # There are actual lint issues
98    else:
99      num_issues = _ParseAndShowResultFile()
100      _ProcessResultFile()
101      msg = ('\nLint found %d new issues.\n'
102             ' - For full explanation refer to %s\n'
103             ' - Wanna suppress these issues?\n'
104             '    1. Read comment in %s\n'
105             '    2. Run "python %s %s"\n' %
106             (num_issues,
107              _RelativizePath(result_path),
108              _RelativizePath(config_path),
109              _RelativizePath(os.path.join(_SRC_ROOT, 'build', 'android',
110                                           'lint', 'suppress.py')),
111              _RelativizePath(result_path)))
112      print >> sys.stderr, msg
113      return 1
114
115  return 0
116
117
118def main(argv):
119  parser = optparse.OptionParser()
120  parser.add_option('--lint-path', help='Path to lint executable.')
121  parser.add_option('--config-path', help='Path to lint suppressions file.')
122  parser.add_option('--processed-config-path',
123                    help='Path to processed lint suppressions file.')
124  parser.add_option('--manifest-path', help='Path to AndroidManifest.xml')
125  parser.add_option('--result-path', help='Path to XML lint result file.')
126  parser.add_option('--product-dir', help='Path to product dir.')
127  parser.add_option('--src-dirs', help='Directories containing java files.')
128  parser.add_option('--classes-dir', help='Directory containing class files.')
129  parser.add_option('--stamp', help='Path to touch on success.')
130  parser.add_option('--enable', action='store_true',
131                    help='Run lint instead of just touching stamp.')
132
133  options, _ = parser.parse_args()
134
135  build_utils.CheckOptions(
136      options, parser, required=['lint_path', 'config_path',
137                                 'processed_config_path', 'manifest_path',
138                                 'result_path', 'product_dir', 'src_dirs',
139                                 'classes_dir'])
140
141  src_dirs = build_utils.ParseGypList(options.src_dirs)
142
143  rc = 0
144
145  if options.enable:
146    rc = _RunLint(options.lint_path, options.config_path,
147                  options.processed_config_path,
148                  options.manifest_path, options.result_path,
149                  options.product_dir, src_dirs, options.classes_dir)
150
151  if options.stamp and not rc:
152    build_utils.Touch(options.stamp)
153
154  return rc
155
156
157if __name__ == '__main__':
158  sys.exit(main(sys.argv))
159