| 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 |