a simple text visualisation of error traces

Summary:
@public
This adds a script `inferTraceBugs` to `infer/bin/` that
1. shows the list of bugs found by Infer to the user
2. asks which one to display
3. asks what max level of nested procedure calls to display
4. shows the error trace of that bug with some lines of context in the source
   code

Also has some options to script more easily, for instance when calling it from
inside an editor to navigate the sources.

Test Plan:
  infer -o out -- gcc -c hello.c
  inferTraceBugs -o out

also tested on OpenSSL.

In emacs, run `M-x compile` from the directory where `infer-out` is, then enter custom compilation command:
   inferTraceBugs --select 0 --max-level max --no-source

Then navigate the trace with `M-g n`.
master
Jules Villard 10 years ago
parent c3a1e501bc
commit 044df14616

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

@ -138,4 +138,5 @@ def main():
analysis.analyze_and_report()
analysis.save_stats()
main()
if __name__ == '__main__':
main()

@ -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='<directory>',
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()

@ -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='<directory>',
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(

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

Loading…
Cancel
Save