#!/usr/bin/env python2.7 # Copyright (c) 2013 - present Facebook, Inc. # All rights reserved. # # This source code is licensed under the BSD style license found in the # LICENSE file in the root directory of this source tree. An additional grant # of patent rights can be found in the PATENTS file in the same directory. from __future__ import absolute_import from __future__ import division from __future__ import print_function from __future__ import unicode_literals import argparse import codecs import json import os import re import shutil import subprocess import sys from inferlib import analyze, config, source, utils HTML_REPORT_DIR = 'report.html' TRACES_REPORT_DIR = 'traces' SOURCE_REMOTE_GITHUB_URL_TEMPLATE = ('https://github.com/{project}/blob/' '{hash}/{relative-path}/' '{file-name}#L{line-number}') SOURCE_REMOTE_GITHUB_RE = re.compile('.*github.com[:/](?P.*)') base_parser = argparse.ArgumentParser( description='Explore the error traces in Infer reports.') base_parser.add_argument('-o', '--out', metavar='', default=utils.DEFAULT_INFER_OUT, dest='infer_out', action=utils.AbsolutePathAction, help='Set the Infer results directory') base_parser.add_argument('--only-show', action='store_true', help='Show the list of reports and exit') base_parser.add_argument('--no-source', action='store_true', help='Do not print code excerpts') base_parser.add_argument('--select', metavar='N', nargs=1, help='Select bug number N. ' 'If omitted, prompts you for input.') base_parser.add_argument('--max-level', metavar='N', nargs=1, help='Level of nested procedure calls to show. ' 'Can be "max", in which case all levels are shown. ' 'If omitted, prompts you for input.') base_parser.add_argument('--html', action='store_true', help='Generate HTML report.') def describe_report(report, indent=0): filename = report[utils.JSON_INDEX_FILENAME] kind = report[utils.JSON_INDEX_KIND] line = report[utils.JSON_INDEX_LINE] error_type = report[utils.JSON_INDEX_TYPE] msg = report[utils.JSON_INDEX_QUALIFIER] return '{0}:{1}: {2}: {3}\n {4}{5}\n'.format( filename, line, kind.lower(), error_type, ' ' * indent, msg, ) def show_error_and_exit(err, show_help): print(err) if show_help: print('') base_parser.print_help() exit(1) class Tracer(object): def __init__(self, args, level=sys.maxsize): self.args = args self.max_level = level self.indenter = source.Indenter() def build_node_tags(self, node): pass def build_node(self, node): if node[utils.JSON_INDEX_TRACE_LEVEL] > self.max_level: return report_line = node[utils.JSON_INDEX_TRACE_LINE] fname = node[utils.JSON_INDEX_TRACE_FILENAME] self.indenter.newline() self.indenter.add('%s:%d: %s' % ( fname, report_line, node[utils.JSON_INDEX_TRACE_DESCRIPTION], )) self.indenter.newline() if not self.args.no_source: self.indenter.indent_push(node[utils.JSON_INDEX_TRACE_LEVEL]) mode = source.TERMINAL_FORMATTER if self.args.html: mode = source.PLAIN_FORMATTER self.indenter.add(source.build_source_context(fname, mode, report_line)) self.indenter.indent_pop() self.indenter.newline() def build_trace(self, trace): total_nodes = len(trace) hidden_nodes = len( filter(lambda n: n[utils.JSON_INDEX_TRACE_LEVEL] > self.max_level, trace)) shown_nodes = total_nodes - hidden_nodes hidden_str = '' all_str = 'all ' if hidden_nodes > 0: hidden_str = ' (%d steps too deeply nested)' % hidden_nodes all_str = '' self.indenter.add('Showing %s%d steps of the trace%s\n\n' % (all_str, shown_nodes, hidden_str)) self.indenter.newline() for node in trace: self.build_node(node) def build_report(self, report): self.build_trace(report[utils.JSON_INDEX_TRACE]) def __str__(self): return str(self.indenter) class Selector(object): def __init__(self, args, reports): self.args = args def has_trace(report): return len(report[utils.JSON_INDEX_TRACE]) > 0 self.reports = [report for report in reports if has_trace(report)] def show_choices(self): n = 0 n_length = len(str(len(self))) for report in self.reports: print(str(n).rjust(n_length) + '. ' + describe_report(report, n_length + 2)) n += 1 def prompt_report(self): report_number = 0 if self.args.select is not None: report_number = self.parse_report_number(self.args.select[0], True) else: self.show_choices() if len(self) > 1: report_number_str = raw_input( 'Choose report to display (default=0): ') if report_number_str != '': report_number = self.parse_report_number(report_number_str) elif len(self) == 1: print('Auto-selecting the only report.') return self.reports[report_number] def prompt_level(self): if self.args.max_level is not None: return self.parse_max_level(self.args.max_level[0], True) max_level_str = raw_input( 'Choose maximum level of nested procedures calls (default=max): ') if max_level_str == '': max_level = sys.maxsize else: max_level = self.parse_max_level(max_level_str) print('') return max_level def parse_report_number(self, s, show_help=False): try: n = int(s) except ValueError: show_error_and_exit( 'ERROR: integer report number expected', show_help) if n >= len(self) or n < 0: show_error_and_exit('ERROR: invalid report number.', show_help) return n def parse_max_level(self, s, show_help=False): if s == 'max': return sys.maxsize try: n = int(s) except ValueError: show_error_and_exit( 'ERROR: integer max level or "max" expected', show_help) if n < 0: show_error_and_exit('ERROR: invalid max level.', show_help) return n def __len__(self): return len(self.reports) def __iter__(self): return self.reports.__iter__() def __next__(self): return self.reports.__next__() def path_of_bug_number(traces_dir, i): return os.path.join(traces_dir, 'bug_%d.txt' % (i+1)) def url_of_bug_number(i): return '%s/bug_%d.txt' % (TRACES_REPORT_DIR, i+1) def get_remote_source_template(): """Return a template that given 'file-name' and 'line-number' entries gives a remote url to that source location. Return the empty template if no remote source has been detected. Currently only detects GitHub projects. """ # see if we are in a GitHub project clone try: git_remote = subprocess.check_output( ['git', 'config', '--get', 'remote.origin.url']).decode().strip() m = SOURCE_REMOTE_GITHUB_RE.match(git_remote) if m is not None: project = m.group('project') # some remotes end in .git, but the http urls don't have # these if project.endswith('.git'): project = project[:-len('.git')] print('Detected GitHub project %s' % project) hash = subprocess.check_output( ['git', 'rev-parse', 'HEAD']).decode().strip() root = subprocess.check_output( ['git', 'rev-parse', '--show-toplevel']).decode().strip() # FIXME(t8921813): we should have a way to get absolute # paths in traces. In the meantime, trust that we run from # the same directory from which infer was run. relative_path = os.path.relpath(os.getcwd(), root) d = { 'project': project, 'hash': hash, 'relative-path': relative_path, 'file-name': '{file-name}', 'line-number': '{line-number}', } return SOURCE_REMOTE_GITHUB_URL_TEMPLATE.format(**d) except subprocess.CalledProcessError: pass return None def html_bug_trace(args, report, bug_id): bug_trace = '' bug_trace += '%s\n' % describe_report(report) tracer = Tracer(args) tracer.build_report(report) bug_trace += str(tracer) return bug_trace def html_list_of_bugs(args, remote_source_template, selector): template = '\n'.join([ '', '', 'Infer found {num-bugs} bugs', '', '', '

List of bugs found

', '{list-of-bugs}', '', '', ]) report_template = '\n'.join([ '
  • ', '{description}', '({source-uri}trace)', '
  • ', ]) def source_uri(report): d = { 'file-name': report[utils.JSON_INDEX_FILENAME], 'line-number': report[utils.JSON_INDEX_LINE], } if remote_source_template is not None: link = remote_source_template.format(**d) return 'source | ' % link return '' i = 0 list_of_bugs = '
      ' for report in selector: d = { 'description': describe_report(report, 2), 'trace-url': url_of_bug_number(i), 'source-uri': source_uri(report), } list_of_bugs += report_template.format(**d) i += 1 list_of_bugs += '
    ' d = { 'num-bugs': len(selector), 'list-of-bugs': list_of_bugs, } return template.format(**d) def generate_html_report(args, reports): html_dir = os.path.join(args.infer_out, HTML_REPORT_DIR) shutil.rmtree(html_dir, True) analyze.mkdir_if_not_exists(html_dir) traces_dir = os.path.join(html_dir, TRACES_REPORT_DIR) analyze.mkdir_if_not_exists(traces_dir) sel = Selector(args, reports) i = 0 for bug in sel: bug_trace_path = path_of_bug_number(traces_dir, i) with codecs.open(bug_trace_path, 'w', encoding=config.LOCALE) as bug_trace_file: bug_trace_file.write(html_bug_trace(args, bug, i)) i += 1 remote_source_template = get_remote_source_template() bug_list_path = os.path.join(html_dir, 'index.html') with codecs.open(bug_list_path, 'w', encoding=config.LOCALE) as bug_list_file: bug_list_file.write(html_list_of_bugs(args, remote_source_template, sel)) print('Saved html report in:\n%s' % bug_list_path) def main(): args = base_parser.parse_args() report_filename = os.path.join(args.infer_out, utils.JSON_REPORT_FILENAME) with codecs.open(report_filename, 'r', encoding=config.LOCALE) as report_file: reports = json.load(report_file) if args.html: generate_html_report(args, reports) exit(0) sel = Selector(args, reports) if len(sel) == 0: print('No issues found') exit(0) if args.only_show: sel.show_choices() exit(0) report = sel.prompt_report() max_level = sel.prompt_level() print(describe_report(report)) tracer = Tracer(args, max_level) tracer.build_report(report) print(tracer) if __name__ == '__main__': main()