You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
437 lines
13 KiB
437 lines
13 KiB
#!/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 re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
|
|
# Infer imports
|
|
import utils
|
|
import inferlib
|
|
|
|
BASE_INDENT = 2
|
|
# how many lines of context around each report
|
|
SOURCE_CONTEXT = 2
|
|
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<project>.*)')
|
|
|
|
|
|
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.')
|
|
base_parser.add_argument('--html',
|
|
action='store_true',
|
|
help='Generate HTML report.')
|
|
|
|
|
|
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):
|
|
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 __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([
|
|
'<html>',
|
|
'<head>',
|
|
'<title>Infer found {num-bugs} bugs</title>',
|
|
'</head>',
|
|
'<body>',
|
|
'<h2>List of bugs found</h2>',
|
|
'{list-of-bugs}',
|
|
'</body>',
|
|
'</html>',
|
|
])
|
|
|
|
report_template = '\n'.join([
|
|
'<li>',
|
|
'{description}',
|
|
'({source-uri}<a href="{trace-url}">trace</a>)',
|
|
'</li>',
|
|
])
|
|
|
|
def source_uri(report):
|
|
d = {
|
|
'file-name': report['file'],
|
|
'line-number': report['line'],
|
|
}
|
|
if remote_source_template is not None:
|
|
link = remote_source_template.format(**d)
|
|
return '<a href="%s">source</a> | ' % link
|
|
return ''
|
|
|
|
i = 0
|
|
list_of_bugs = '<ol>'
|
|
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 += '</ol>'
|
|
|
|
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)
|
|
inferlib.mkdir_if_not_exists(html_dir)
|
|
|
|
traces_dir = os.path.join(html_dir, TRACES_REPORT_DIR)
|
|
inferlib.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 open(bug_trace_path, 'w') 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 open(bug_list_path, 'w') 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 open(report_filename) 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()
|