|
|
|
#!/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 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 = 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_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 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 = sel.prompt_report()
|
|
|
|
max_level = sel.prompt_level()
|
|
|
|
|
|
|
|
tracer = Tracer(args, max_level)
|
|
|
|
tracer.build_report(report)
|
|
|
|
print(tracer)
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|