# Copyright (c) 2012 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import logging import os import xml.dom.minidom from devil.utils import cmd_helper from pylib.constants import host_paths _FINDBUGS_HOME = os.path.join(host_paths.DIR_SOURCE_ROOT, 'third_party', 'findbugs') _FINDBUGS_JAR = os.path.join(_FINDBUGS_HOME, 'lib', 'findbugs.jar') _FINDBUGS_MAX_HEAP = 768 _FINDBUGS_PLUGIN_PATH = os.path.join( host_paths.DIR_SOURCE_ROOT, 'tools', 'android', 'findbugs_plugin', 'lib', 'chromiumPlugin.jar') def _ParseXmlResults(results_doc): errors = set() warnings = set() for en in (n for n in results_doc.documentElement.childNodes if n.nodeType == xml.dom.Node.ELEMENT_NODE): if en.tagName == 'Errors': errors.update(_ParseErrors(en)) if en.tagName == 'BugInstance': warnings.add(_ParseBugInstance(en)) return errors, warnings def _GetMessage(node): for c in (n for n in node.childNodes if n.nodeType == xml.dom.Node.ELEMENT_NODE): if c.tagName == 'Message': if (len(c.childNodes) == 1 and c.childNodes[0].nodeType == xml.dom.Node.TEXT_NODE): return c.childNodes[0].data return None def _GetTextContent(node): if (len(node.childNodes) == 1 and node.childNodes[0].nodeType == xml.dom.Node.TEXT_NODE): return node.childNodes[0].data return '' def _ParseErrors(node): errors = set() for error_node in (n for n in node.childNodes if n.nodeType == xml.dom.Node.ELEMENT_NODE): error_text = '(unable to determine error text)' if error_node.tagName == 'Error': error_message_nodes = ( n for n in error_node.childNodes if (n.nodeType == xml.dom.Node.ELEMENT_NODE and n.tagName == 'ErrorMessage')) text_pieces = [_GetTextContent(n) for n in error_message_nodes] if text_pieces: error_text = ', '.join(t for t in text_pieces if t) errors.add(FindBugsError(error_node.tagName, error_text)) return errors def _ParseBugInstance(node): bug = FindBugsWarning(node.getAttribute('type')) msg_parts = [] for c in (n for n in node.childNodes if n.nodeType == xml.dom.Node.ELEMENT_NODE): if c.tagName == 'Class': msg_parts.append(_GetMessage(c)) elif c.tagName == 'Method': msg_parts.append(_GetMessage(c)) elif c.tagName == 'Field': msg_parts.append(_GetMessage(c)) elif c.tagName == 'SourceLine': bug.file_name = c.getAttribute('sourcefile') if c.hasAttribute('start'): bug.start_line = int(c.getAttribute('start')) if c.hasAttribute('end'): bug.end_line = int(c.getAttribute('end')) msg_parts.append(_GetMessage(c)) elif c.tagName == 'ShortMessage': msg_parts.append(_GetTextContent(c)) bug.message = tuple(m for m in msg_parts if m) return bug class FindBugsError(object): def __init__(self, error_type='', error_val=''): self.error_type = error_type self.error_val = error_val def __cmp__(self, other): return (cmp(self.error_type, other.error_type) or cmp(self.error_val, other.error_val)) def __eq__(self, other): return self.__dict__ == other.__dict__ def __hash__(self): return hash((self.error_type, self.error_val)) def __ne__(self, other): return not self == other def __str__(self): return '%s: %s' % (self.error_type, self.error_val) class FindBugsWarning(object): def __init__(self, bug_type='', end_line=0, file_name='', message=None, start_line=0): self.bug_type = bug_type self.end_line = end_line self.file_name = file_name if message is None: self.message = tuple() else: self.message = message self.start_line = start_line def __cmp__(self, other): return (cmp(self.file_name, other.file_name) or cmp(self.start_line, other.start_line) or cmp(self.end_line, other.end_line) or cmp(self.bug_type, other.bug_type) or cmp(self.message, other.message)) def __eq__(self, other): return self.__dict__ == other.__dict__ def __hash__(self): return hash((self.bug_type, self.end_line, self.file_name, self.message, self.start_line)) def __ne__(self, other): return not self == other def __str__(self): return '%s: %s' % (self.bug_type, '\n '.join(self.message)) def Run(exclude, classes_to_analyze, system_classes, auxiliary_classes, output_file, findbug_args, jars): """Run FindBugs. Args: exclude: the exclude xml file, refer to FindBugs's -exclude command option. classes_to_analyze: the list of classes need to analyze, refer to FindBugs' -onlyAnalyze command line option. system_classes: system classes to help analysis. auxiliary_classes: other classes to help analysis. Refer to FindBugs' -auxclasspath command line option. output_file: An optional path to dump XML results to. findbug_args: A list of additional command line options to pass to Findbugs. """ # TODO(jbudorick): Get this from the build system. all_aux_classes = [] all_aux_classes.extend(system_classes or []) all_aux_classes.extend( os.path.abspath(classes) for classes in auxiliary_classes or []) cmd = ['java', '-classpath', '%s:' % _FINDBUGS_JAR, '-Xmx%dm' % _FINDBUGS_MAX_HEAP, '-Dfindbugs.home="%s"' % _FINDBUGS_HOME, '-jar', _FINDBUGS_JAR, '-textui', '-sortByClass', '-pluginList', _FINDBUGS_PLUGIN_PATH, '-xml:withMessages'] if system_classes: cmd.extend(['-auxclasspath', ':'.join(all_aux_classes)]) if classes_to_analyze: cmd.extend(['-onlyAnalyze', classes_to_analyze]) if exclude: cmd.extend(['-exclude', os.path.abspath(exclude)]) if output_file: cmd.extend(['-output', output_file]) if findbug_args: cmd.extend(findbug_args) cmd.extend(os.path.abspath(j) for j in jars or []) if output_file: _, _, stderr = cmd_helper.GetCmdStatusOutputAndError(cmd) results_doc = xml.dom.minidom.parse(output_file) else: _, raw_out, stderr = cmd_helper.GetCmdStatusOutputAndError(cmd) results_doc = xml.dom.minidom.parseString(raw_out) for line in stderr.splitlines(): logging.debug(' %s', line) current_errors_set, current_warnings_set = _ParseXmlResults(results_doc) return (' '.join(cmd), current_errors_set, current_warnings_set)