diff --git a/FILES.md b/FILES.md index 9466f4307..11422884c 100644 --- a/FILES.md +++ b/FILES.md @@ -7,6 +7,8 @@ *inferTest* : Shell script for running Infer's tests. Uses Buck for running the tests. Usage: inferTest {c, objc, java} for the tests about the analysis of C, Objective-C, or Java files. +*inferTraceBugs* : Python script to explore the error traces in Infer reports + ## Helper commands The rest of the commands in infer/bin/ are not meant to be called directly, but are used by the top-level commands above. diff --git a/infer/bin/infer b/infer/bin/infer index e8a1bf570..b4b8bbeea 100755 --- a/infer/bin/infer +++ b/infer/bin/infer @@ -138,4 +138,5 @@ def main(): analysis.analyze_and_report() analysis.save_stats() -main() +if __name__ == '__main__': + main() diff --git a/infer/bin/inferTraceBugs b/infer/bin/inferTraceBugs new file mode 100755 index 000000000..dcbc6bfc9 --- /dev/null +++ b/infer/bin/inferTraceBugs @@ -0,0 +1,273 @@ +#!/usr/bin/env python +# +# 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() diff --git a/infer/bin/inferlib.py b/infer/bin/inferlib.py index 5b0124a38..783b84f06 100644 --- a/infer/bin/inferlib.py +++ b/infer/bin/inferlib.py @@ -14,7 +14,6 @@ import json import logging import multiprocessing import os -import re import shutil import subprocess import sys @@ -45,11 +44,6 @@ ERROR = 'ERROR' WARNING = 'WARNING' INFO = 'INFO' -class AbsolutePathAction(argparse.Action): - """Convert a path from relative to absolute in the arg parser""" - def __call__(self, parser, namespace, values, option_string=None): - setattr(namespace, self.dest, os.path.abspath(values)) - # https://github.com/python/cpython/blob/aa8ea3a6be22c92e774df90c6a6ee697915ca8ec/Lib/argparse.py class VersionAction(argparse._VersionAction): @@ -75,7 +69,7 @@ base_parser = argparse.ArgumentParser(add_help=False) base_group = base_parser.add_argument_group('global arguments') base_group.add_argument('-o', '--out', metavar='', default=utils.DEFAULT_INFER_OUT, dest='infer_out', - action=AbsolutePathAction, + action=utils.AbsolutePathAction, help='Set the Infer results directory') base_group.add_argument('-i', '--incremental', action='store_true', help='''Do not delete the results directory across @@ -271,12 +265,6 @@ def clean_csv(args, csv_report): shutil.move(temporary_file, csv_report) -def remove_bucket(bug_message): - """ Remove anything from the beginning if the message that - looks like a bucket """ - return re.sub(r'(^\[[a-zA-Z0-9]*\])', '', bug_message, 1) - - def print_and_write(file_out, message): print(message) file_out.write(message + '\n') @@ -301,7 +289,7 @@ def print_errors(csv_report, bugs_out): kind = row[utils.CSV_INDEX_KIND] line = row[utils.CSV_INDEX_LINE] error_type = row[utils.CSV_INDEX_TYPE] - msg = remove_bucket(row[utils.CSV_INDEX_QUALIFIER]) + msg = utils.remove_bucket(row[utils.CSV_INDEX_QUALIFIER]) print_and_write( file_out, '{0}:{1}: {2}: {3}\n {4}\n'.format( diff --git a/infer/bin/utils.py b/infer/bin/utils.py index 82d3fcd12..8753ec685 100644 --- a/infer/bin/utils.py +++ b/infer/bin/utils.py @@ -7,12 +7,14 @@ from __future__ import division from __future__ import print_function from __future__ import unicode_literals +import argparse import csv import fnmatch import gzip import json import logging import os +import re import subprocess import sys import tempfile @@ -99,6 +101,12 @@ def error(msg): print(msg, file=sys.stderr) +def remove_bucket(bug_message): + """ Remove anything from the beginning if the message that + looks like a bucket """ + return re.sub(r'(^\[[a-zA-Z0-9]*\])', '', bug_message, 1) + + def get_cmd_in_bin_dir(binary_name): # this relies on the fact that utils.py is located in infer/bin return os.path.join( @@ -346,4 +354,10 @@ def create_json_report(out_dir): issues = rows[1:] json.dump([dict(zip(headers, row)) for row in issues], file_out) + +class AbsolutePathAction(argparse.Action): + """Convert a path from relative to absolute in the arg parser""" + def __call__(self, parser, namespace, values, option_string=None): + setattr(namespace, self.dest, os.path.abspath(values)) + # vim: set sw=4 ts=4 et: