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.
397 lines
12 KiB
397 lines
12 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 codecs
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
|
|
from inferlib import colorize, config, issues, source, utils
|
|
|
|
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=config.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 show_error_and_exit(err, show_help):
|
|
utils.stderr(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.indenter = source.Indenter()
|
|
|
|
def build_node_tags(self, node):
|
|
pass
|
|
|
|
def build_node(self, node):
|
|
if node[issues.JSON_INDEX_TRACE_LEVEL] > self.max_level:
|
|
return
|
|
|
|
report_line = node[issues.JSON_INDEX_TRACE_LINE]
|
|
fname = node[issues.JSON_INDEX_TRACE_FILENAME]
|
|
|
|
self.indenter.newline()
|
|
self.indenter.add('%s:%d: %s' % (
|
|
fname,
|
|
report_line,
|
|
node[issues.JSON_INDEX_TRACE_DESCRIPTION],
|
|
))
|
|
self.indenter.newline()
|
|
|
|
if not self.args.no_source:
|
|
self.indenter.indent_push(node[issues.JSON_INDEX_TRACE_LEVEL])
|
|
mode = colorize.TERMINAL_FORMATTER
|
|
if self.args.html:
|
|
mode = colorize.PLAIN_FORMATTER
|
|
self.indenter.add(source.build_source_context(fname,
|
|
mode,
|
|
report_line))
|
|
self.indenter.indent_pop()
|
|
self.indenter.newline()
|
|
|
|
def build_trace(self, trace):
|
|
total_nodes = len(trace)
|
|
hidden_nodes = len(
|
|
filter(lambda n: n[issues.JSON_INDEX_TRACE_LEVEL] > self.max_level,
|
|
trace))
|
|
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.indenter.add('Showing %s%d steps of the trace%s\n\n'
|
|
% (all_str, shown_nodes, hidden_str))
|
|
self.indenter.newline()
|
|
|
|
for node in trace:
|
|
self.build_node(node)
|
|
|
|
def build_report(self, report):
|
|
self.build_trace(report[issues.JSON_INDEX_TRACE])
|
|
|
|
def __str__(self):
|
|
return str(self.indenter)
|
|
|
|
|
|
class Selector(object):
|
|
def __init__(self, args, reports):
|
|
self.args = args
|
|
|
|
def has_trace(report):
|
|
return len(report[issues.JSON_INDEX_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:
|
|
# the goal is to get the following output for each report:
|
|
# 1234. <first line of report #1234 goes here>
|
|
# <second line of report goes here>
|
|
msg = issues.text_of_report(report) \
|
|
.replace('\n', '\n%s' % ((n_length + 2) * ' '))
|
|
utils.stdout('%s. %s\n' % (str(n).rjust(n_length), msg))
|
|
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')]
|
|
utils.stdout('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' % issues.text_of_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[issues.JSON_INDEX_FILENAME],
|
|
'line-number': report[issues.JSON_INDEX_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': issues.text_of_report(report),
|
|
'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)
|
|
utils.mkdir_if_not_exists(html_dir)
|
|
|
|
traces_dir = os.path.join(html_dir, TRACES_REPORT_DIR)
|
|
utils.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 codecs.open(bug_trace_path, 'w',
|
|
encoding=config.LOCALE,
|
|
errors='xmlcharrefreplace') 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 codecs.open(bug_list_path, 'w',
|
|
encoding=config.LOCALE,
|
|
errors='xmlcharrefreplace') as bug_list_file:
|
|
bug_list_file.write(html_list_of_bugs(args,
|
|
remote_source_template,
|
|
sel))
|
|
|
|
utils.stdout('Saved html report in:\n%s' % bug_list_path)
|
|
|
|
|
|
def main():
|
|
args = base_parser.parse_args()
|
|
|
|
report_filename = os.path.join(args.infer_out, config.JSON_REPORT_FILENAME)
|
|
reports = utils.load_json_from_path(report_filename)
|
|
|
|
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()
|
|
|
|
utils.stdout(issues.text_of_report(report))
|
|
|
|
tracer = Tracer(args, max_level)
|
|
tracer.build_report(report)
|
|
utils.stdout(str(tracer))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|