OLD | NEW |
1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 import optparse | 5 import argparse |
| 6 import logging |
6 import os | 7 import os |
7 import re | 8 import re |
8 import shlex | 9 import shlex |
9 import subprocess | |
10 import sys | 10 import sys |
| 11 import xml.dom.minidom |
11 | 12 |
12 from pylib import cmd_helper | 13 from pylib import cmd_helper |
13 from pylib import constants | 14 from pylib import constants |
14 | 15 |
15 | 16 |
16 def _PrintMessage(warnings, title, action, known_bugs_file): | 17 _FINDBUGS_HOME = os.path.join(constants.DIR_SOURCE_ROOT, 'third_party', |
17 if warnings: | 18 'findbugs') |
18 print | 19 _FINDBUGS_JAR = os.path.join(_FINDBUGS_HOME, 'lib', 'findbugs.jar') |
19 print '*' * 80 | 20 _FINDBUGS_MAX_HEAP = 768 |
20 print '%s warnings.' % title | 21 _FINDBUGS_PLUGIN_PATH = os.path.join( |
21 print '%s %s' % (action, known_bugs_file) | 22 constants.DIR_SOURCE_ROOT, 'tools', 'android', 'findbugs_plugin', 'lib', |
22 print '-' * 80 | 23 'chromiumPlugin.jar') |
23 for warning in warnings: | |
24 print warning | |
25 print '-' * 80 | |
26 print | |
27 | 24 |
28 | 25 |
29 def _StripLineNumbers(current_warnings): | 26 def _ParseXmlResults(results_doc): |
30 re_line = r':\[line.*?\]$' | 27 warnings = set() |
31 return [re.sub(re_line, '', x) for x in current_warnings] | 28 for en in (n for n in results_doc.documentElement.childNodes |
| 29 if n.nodeType == xml.dom.Node.ELEMENT_NODE): |
| 30 if en.tagName == 'BugInstance': |
| 31 warnings.add(_ParseBugInstance(en)) |
| 32 return warnings |
32 | 33 |
33 | 34 |
34 def _DiffKnownWarnings(current_warnings_set, known_bugs_file): | 35 def _GetMessage(node): |
35 if os.path.exists(known_bugs_file): | 36 for c in (n for n in node.childNodes |
36 with open(known_bugs_file, 'r') as known_bugs: | 37 if n.nodeType == xml.dom.Node.ELEMENT_NODE): |
37 known_bugs_set = set(known_bugs.read().splitlines()) | 38 if c.tagName == 'Message': |
38 else: | 39 if (len(c.childNodes) == 1 |
39 known_bugs_set = set() | 40 and c.childNodes[0].nodeType == xml.dom.Node.TEXT_NODE): |
40 | 41 return c.childNodes[0].data |
41 new_warnings = current_warnings_set - known_bugs_set | 42 return None |
42 _PrintMessage(sorted(new_warnings), 'New', 'Please fix, or perhaps add to', | |
43 known_bugs_file) | |
44 | |
45 obsolete_warnings = known_bugs_set - current_warnings_set | |
46 _PrintMessage(sorted(obsolete_warnings), 'Obsolete', 'Please remove from', | |
47 known_bugs_file) | |
48 | |
49 count = len(new_warnings) + len(obsolete_warnings) | |
50 if count: | |
51 print '*** %d FindBugs warning%s! ***' % (count, 's' * (count > 1)) | |
52 if len(new_warnings): | |
53 print '*** %d: new ***' % len(new_warnings) | |
54 if len(obsolete_warnings): | |
55 print '*** %d: obsolete ***' % len(obsolete_warnings) | |
56 print | |
57 print 'Alternatively, rebaseline with --rebaseline command option' | |
58 print | |
59 else: | |
60 print 'No new FindBugs warnings.' | |
61 print | |
62 return count | |
63 | 43 |
64 | 44 |
65 def _Rebaseline(current_warnings_set, known_bugs_file): | 45 def _ParseBugInstance(node): |
66 with file(known_bugs_file, 'w') as known_bugs: | 46 bug = FindBugsWarning(node.getAttribute('type')) |
67 for warning in sorted(current_warnings_set): | 47 msg_parts = [] |
68 print >> known_bugs, warning | 48 for c in (n for n in node.childNodes |
69 return 0 | 49 if n.nodeType == xml.dom.Node.ELEMENT_NODE): |
| 50 if c.tagName == 'Class': |
| 51 msg_parts.append(_GetMessage(c)) |
| 52 elif c.tagName == 'Method': |
| 53 msg_parts.append(_GetMessage(c)) |
| 54 elif c.tagName == 'Field': |
| 55 msg_parts.append(_GetMessage(c)) |
| 56 elif c.tagName == 'SourceLine': |
| 57 bug.file_name = c.getAttribute('sourcefile') |
| 58 if c.hasAttribute('start'): |
| 59 bug.start_line = int(c.getAttribute('start')) |
| 60 if c.hasAttribute('end'): |
| 61 bug.end_line = int(c.getAttribute('end')) |
| 62 msg_parts.append(_GetMessage(c)) |
| 63 elif (c.tagName == 'ShortMessage' and len(c.childNodes) == 1 |
| 64 and c.childNodes[0].nodeType == xml.dom.Node.TEXT_NODE): |
| 65 msg_parts.append(c.childNodes[0].data) |
| 66 bug.message = tuple(m for m in msg_parts if m) |
| 67 return bug |
70 | 68 |
71 | 69 |
72 def _GetChromeJars(release_version): | 70 class FindBugsWarning(object): |
73 version = 'Debug' | 71 |
74 if release_version: | 72 def __init__(self, bug_type='', end_line=0, file_name='', message=None, |
75 version = 'Release' | 73 start_line=0): |
76 path = os.path.join(constants.DIR_SOURCE_ROOT, | 74 self.bug_type = bug_type |
77 os.environ.get('CHROMIUM_OUT_DIR', 'out'), | 75 self.end_line = end_line |
78 version, | 76 self.file_name = file_name |
79 'lib.java') | 77 if message is None: |
80 cmd = 'find %s -name "*.jar"' % path | 78 self.message = tuple() |
81 out = cmd_helper.GetCmdOutput(shlex.split(cmd)) | 79 else: |
82 out = [p for p in out.splitlines() if not p.endswith('.dex.jar')] | 80 self.message = message |
83 if not out: | 81 self.start_line = start_line |
84 print 'No classes found in %s' % path | 82 |
85 return ' '.join(out) | 83 def __cmp__(self, other): |
| 84 return (cmp(self.file_name, other.file_name) |
| 85 or cmp(self.start_line, other.start_line) |
| 86 or cmp(self.end_line, other.end_line) |
| 87 or cmp(self.bug_type, other.bug_type) |
| 88 or cmp(self.message, other.message)) |
| 89 |
| 90 def __eq__(self, other): |
| 91 return self.__dict__ == other.__dict__ |
| 92 |
| 93 def __hash__(self): |
| 94 return hash((self.bug_type, self.end_line, self.file_name, self.message, |
| 95 self.start_line)) |
| 96 |
| 97 def __ne__(self, other): |
| 98 return not self == other |
| 99 |
| 100 def __str__(self): |
| 101 return '%s: %s' % (self.bug_type, '\n '.join(self.message)) |
86 | 102 |
87 | 103 |
88 def _Run(exclude, known_bugs, classes_to_analyze, auxiliary_classes, | 104 def Run(exclude, classes_to_analyze, auxiliary_classes, output_file, |
89 rebaseline, release_version, findbug_args): | 105 findbug_args, jars): |
90 """Run the FindBugs. | 106 """Run FindBugs. |
91 | 107 |
92 Args: | 108 Args: |
93 exclude: the exclude xml file, refer to FindBugs's -exclude command option. | 109 exclude: the exclude xml file, refer to FindBugs's -exclude command option. |
94 known_bugs: the text file of known bugs. The bugs in it will not be | |
95 reported. | |
96 classes_to_analyze: the list of classes need to analyze, refer to FindBug's | 110 classes_to_analyze: the list of classes need to analyze, refer to FindBug's |
97 -onlyAnalyze command line option. | 111 -onlyAnalyze command line option. |
98 auxiliary_classes: the classes help to analyze, refer to FindBug's | 112 auxiliary_classes: the classes help to analyze, refer to FindBug's |
99 -auxclasspath command line option. | 113 -auxclasspath command line option. |
100 rebaseline: True if the known_bugs file needs rebaseline. | 114 output_file: An optional path to dump XML results to. |
101 release_version: True if the release version needs check, otherwise check | 115 findbug_args: A list of addtional command line options to pass to Findbugs. |
102 debug version. | |
103 findbug_args: addtional command line options needs pass to Findbugs. | |
104 """ | 116 """ |
| 117 # TODO(jbudorick): Get this from the build system. |
| 118 system_classes = [ |
| 119 os.path.join(constants.ANDROID_SDK_ROOT, 'platforms', |
| 120 'android-%s' % constants.ANDROID_SDK_VERSION, 'android.jar') |
| 121 ] |
| 122 system_classes.extend(os.path.abspath(classes) |
| 123 for classes in auxiliary_classes or []) |
105 | 124 |
106 chrome_src = constants.DIR_SOURCE_ROOT | 125 cmd = ['java', |
107 sdk_root = constants.ANDROID_SDK_ROOT | 126 '-classpath', '%s:' % _FINDBUGS_JAR, |
108 sdk_version = constants.ANDROID_SDK_VERSION | 127 '-Xmx%dm' % _FINDBUGS_MAX_HEAP, |
| 128 '-Dfindbugs.home="%s"' % _FINDBUGS_HOME, |
| 129 '-jar', _FINDBUGS_JAR, |
| 130 '-textui', '-sortByClass', |
| 131 '-pluginList', _FINDBUGS_PLUGIN_PATH, '-xml:withMessages'] |
| 132 if system_classes: |
| 133 cmd.extend(['-auxclasspath', ':'.join(system_classes)]) |
| 134 if classes_to_analyze: |
| 135 cmd.extend(['-onlyAnalyze', classes_to_analyze]) |
| 136 if exclude: |
| 137 cmd.extend(['-exclude', os.path.abspath(exclude)]) |
| 138 if output_file: |
| 139 cmd.extend(['-output', output_file]) |
| 140 if findbug_args: |
| 141 cmd.extend(findbug_args) |
| 142 cmd.extend(os.path.abspath(j) for j in jars or []) |
109 | 143 |
110 system_classes = [] | 144 if output_file: |
111 system_classes.append(os.path.join(sdk_root, 'platforms', | 145 cmd_helper.RunCmd(cmd) |
112 'android-%s' % sdk_version, 'android.jar')) | 146 results_doc = xml.dom.minidom.parse(output_file) |
113 if auxiliary_classes: | 147 else: |
114 for classes in auxiliary_classes: | 148 raw_out = cmd_helper.GetCmdOutput(cmd) |
115 system_classes.append(os.path.abspath(classes)) | 149 results_doc = xml.dom.minidom.parseString(raw_out) |
116 | 150 |
117 findbugs_javacmd = 'java' | 151 current_warnings_set = _ParseXmlResults(results_doc) |
118 findbugs_home = os.path.join(chrome_src, 'third_party', 'findbugs') | |
119 findbugs_jar = os.path.join(findbugs_home, 'lib', 'findbugs.jar') | |
120 findbugs_pathsep = ':' | |
121 findbugs_maxheap = '768' | |
122 | 152 |
123 cmd = '%s ' % findbugs_javacmd | 153 return (' '.join(cmd), current_warnings_set) |
124 cmd = '%s -classpath %s%s' % (cmd, findbugs_jar, findbugs_pathsep) | |
125 cmd = '%s -Xmx%sm ' % (cmd, findbugs_maxheap) | |
126 cmd = '%s -Dfindbugs.home="%s" ' % (cmd, findbugs_home) | |
127 cmd = '%s -jar %s ' % (cmd, findbugs_jar) | |
128 | 154 |
129 cmd = '%s -textui -sortByClass ' % cmd | |
130 cmd = '%s -pluginList %s' % (cmd, os.path.join(chrome_src, 'tools', 'android', | |
131 'findbugs_plugin', 'lib', | |
132 'chromiumPlugin.jar')) | |
133 if len(system_classes): | |
134 cmd = '%s -auxclasspath %s ' % (cmd, ':'.join(system_classes)) | |
135 | |
136 if classes_to_analyze: | |
137 cmd = '%s -onlyAnalyze %s ' % (cmd, classes_to_analyze) | |
138 | |
139 if exclude: | |
140 cmd = '%s -exclude %s ' % (cmd, os.path.abspath(exclude)) | |
141 | |
142 if findbug_args: | |
143 cmd = '%s %s ' % (cmd, findbug_args) | |
144 | |
145 chrome_classes = _GetChromeJars(release_version) | |
146 if not chrome_classes: | |
147 return 1 | |
148 cmd = '%s %s ' % (cmd, chrome_classes) | |
149 | |
150 print | |
151 print '*' * 80 | |
152 print 'Command used to run findbugs:' | |
153 print cmd | |
154 print '*' * 80 | |
155 print | |
156 | |
157 proc = subprocess.Popen(shlex.split(cmd), | |
158 stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
159 out, _err = proc.communicate() | |
160 current_warnings_set = set(_StripLineNumbers(filter(None, out.splitlines()))) | |
161 | |
162 if rebaseline: | |
163 return _Rebaseline(current_warnings_set, known_bugs) | |
164 else: | |
165 return _DiffKnownWarnings(current_warnings_set, known_bugs) | |
166 | |
167 def Run(options): | |
168 exclude_file = None | |
169 known_bugs_file = None | |
170 | |
171 if options.exclude: | |
172 exclude_file = options.exclude | |
173 elif options.base_dir: | |
174 exclude_file = os.path.join(options.base_dir, 'findbugs_exclude.xml') | |
175 | |
176 if options.known_bugs: | |
177 known_bugs_file = options.known_bugs | |
178 elif options.base_dir: | |
179 known_bugs_file = os.path.join(options.base_dir, 'findbugs_known_bugs.txt') | |
180 | |
181 auxclasspath = None | |
182 if options.auxclasspath: | |
183 auxclasspath = options.auxclasspath.split(':') | |
184 return _Run(exclude_file, known_bugs_file, options.only_analyze, auxclasspath, | |
185 options.rebaseline, options.release_build, options.findbug_args) | |
186 | |
187 | |
188 def GetCommonParser(): | |
189 parser = optparse.OptionParser() | |
190 parser.add_option('-r', | |
191 '--rebaseline', | |
192 action='store_true', | |
193 dest='rebaseline', | |
194 help='Rebaseline known findbugs issues.') | |
195 | |
196 parser.add_option('-a', | |
197 '--auxclasspath', | |
198 action='store', | |
199 default=None, | |
200 dest='auxclasspath', | |
201 help='Set aux classpath for analysis.') | |
202 | |
203 parser.add_option('-o', | |
204 '--only-analyze', | |
205 action='store', | |
206 default=None, | |
207 dest='only_analyze', | |
208 help='Only analyze the given classes and packages.') | |
209 | |
210 parser.add_option('-e', | |
211 '--exclude', | |
212 action='store', | |
213 default=None, | |
214 dest='exclude', | |
215 help='Exclude bugs matching given filter.') | |
216 | |
217 parser.add_option('-k', | |
218 '--known-bugs', | |
219 action='store', | |
220 default=None, | |
221 dest='known_bugs', | |
222 help='Not report the bugs in the given file.') | |
223 | |
224 parser.add_option('-l', | |
225 '--release-build', | |
226 action='store_true', | |
227 dest='release_build', | |
228 help='Analyze release build instead of debug.') | |
229 | |
230 parser.add_option('-f', | |
231 '--findbug-args', | |
232 action='store', | |
233 default=None, | |
234 dest='findbug_args', | |
235 help='Additional findbug arguments.') | |
236 | |
237 parser.add_option('-b', | |
238 '--base-dir', | |
239 action='store', | |
240 default=None, | |
241 dest='base_dir', | |
242 help='Base directory for configuration file.') | |
243 | |
244 return parser | |
245 | |
246 | |
247 def main(): | |
248 parser = GetCommonParser() | |
249 options, _ = parser.parse_args() | |
250 | |
251 return Run(options) | |
252 | |
253 | |
254 if __name__ == '__main__': | |
255 sys.exit(main()) | |
OLD | NEW |