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.

406 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 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]
report_col = node[issues.JSON_INDEX_TRACE_COLUMN]
fname = node[issues.JSON_INDEX_TRACE_FILENAME]
self.indenter.newline()
self.indenter.add('%s:%d:%d: %s' % (
fname,
report_line,
report_col,
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
empty_desc = len(node[issues.JSON_INDEX_TRACE_DESCRIPTION]) == 0
self.indenter.add(source.build_source_context(fname,
mode,
report_line,
report_col,
empty_desc
))
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 __unicode__(self):
return unicode(self.indenter)
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 += unicode(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.CODESET,
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.CODESET,
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(unicode(tracer))
if __name__ == '__main__':
main()