#!/usr/bin/env python2.7 # # Copyright (c) 2013-present Facebook. All rights reserved. # from __future__ import absolute_import from __future__ import division from __future__ import print_function from __future__ import unicode_literals import argparse import json import os import sys # Infer imports import utils BASE_INDENT = 2 JSON_REPORT = 'report.json' # how many lines of context around each report SOURCE_CONTEXT = 2 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.') def describe_report(report, indent=0): filename = report['file'] kind = report['kind'] line = report['line'] error_type = report['type'] msg = utils.remove_bucket(report['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.text = '' self.indent = [] def indent_get(self): indent = '' for i in self.indent: indent += i return indent def indent_push(self, n=1): self.indent.append(n * BASE_INDENT * ' ') def indent_pop(self): return self.indent.pop() def add(self, s): self.text += self.indent_get() + s def newline(self): self.text += '\n' def build_node_tags(self, node): pass def build_source_context(self, source_name, report_line): start_line = max(1, report_line - SOURCE_CONTEXT) # could go beyond last line, checked in the loop end_line = report_line + SOURCE_CONTEXT n_length = len(str(end_line)) line_number = 1 with open(source_name) as source_file: for line in source_file: if start_line <= line_number <= end_line: num = str(line_number).zfill(n_length) caret = ' ' if line_number == report_line: caret = '> ' self.add(num + ' ' + caret + line) line_number += 1 def build_node(self, node): if node['level'] > self.max_level: return report_line = node['line_number'] fname = node['filename'] self.add('%s:%d: %s\n' % (fname, report_line, node['description'])) if not self.args.no_source: self.indent_push(node['level']) self.build_source_context(fname, report_line) self.indent_pop() self.newline() def build_trace(self, trace): total_nodes = len(trace) hidden_nodes = len([None for n in trace if n['level'] > self.max_level]) 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.add('Showing %s%d steps of the trace%s\n\n' % (all_str, shown_nodes, hidden_str)) for node in trace: self.build_node(node) def build_report(self, report): self.add(describe_report(report)) self.newline() traces = json.loads(report['trace']) self.build_trace(traces['trace']) def __str__(self): return self.text class Selector(object): def __init__(self, args, reports): self.args = args def has_trace(report): trace = json.loads(report['trace']) return len(trace['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_number(self): if self.args.select is not None: return self.parse_report_number(self.args.select[0], True) self.show_choices() report_number = 0 if len(self) > 1: report_number_str = raw_input( 'Choose report to display (default=0): ') if report_number_str == '': report_number = 0 else: report_number = self.parse_report_number(report_number_str) elif len(self) == 1: print('Auto-selecting the only report.') report_number = 0 return 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 main(): args = base_parser.parse_args() with open(os.path.join(args.infer_out, JSON_REPORT)) as report_file: reports = json.load(report_file) sel = Selector(args, reports) if len(sel) == 0: print('No issues found') exit(0) if args.only_show: sel.show_choices() exit(0) report_number = sel.prompt_number() max_level = sel.prompt_level() tracer = Tracer(args, max_level) tracer.build_report(reports[report_number]) print(tracer) if __name__ == '__main__': main()