Bug 1473278: Add infer to java-check, install, clear-cache, and print-checks subcommands. r=gps

MozReview-Commit-ID: 5ngZu6lh1wU

--HG--
extra : amend_source : b89243dd57777746febd180db7acadb77d3d3a49
This commit is contained in:
Robert Bartlensky 2018-07-26 14:45:44 +01:00
Родитель 582e2ce27b
Коммит 24a33b307e
3 изменённых файлов: 230 добавлений и 21 удалений

Просмотреть файл

@ -1,7 +0,0 @@
{
"infer-blacklist-path-regex": [
// This is full of issues, and is a dependency we need to discard
// sooner rather than later anyway:
"mobile/android/thirdparty/ch/boye/httpclientandroidlib"
]
}

Просмотреть файл

@ -1630,7 +1630,7 @@ class StaticAnalysis(MachCommandBase):
"""Utilities for running C++ static analysis checks and format.""" """Utilities for running C++ static analysis checks and format."""
# List of file extension to consider (should start with dot) # List of file extension to consider (should start with dot)
_format_include_extensions = ('.cpp', '.c', '.h') _format_include_extensions = ('.cpp', '.c', '.h', '.java')
# File contaning all paths to exclude from formatting # File contaning all paths to exclude from formatting
_format_ignore_file = '.clang-format-ignore' _format_ignore_file = '.clang-format-ignore'
@ -1712,7 +1712,141 @@ class StaticAnalysis(MachCommandBase):
self.log(logging.WARNING, 'warning_summary', self.log(logging.WARNING, 'warning_summary',
{'count': len(monitor.warnings_db)}, {'count': len(monitor.warnings_db)},
'{count} warnings present.') '{count} warnings present.')
if rc != 0:
return rc return rc
# if we are building firefox for android it might be nice to
# also analyze the java code base
if self.substs['MOZ_BUILD_APP'] == 'mobile/android':
rc = self.check_java(source, jobs, strip, verbose, skip_export=True)
return rc
@StaticAnalysisSubCommand('static-analysis', 'check-java',
'Run infer on the java codebase.')
@CommandArgument('source', nargs='*', default=['mobile'],
help='Source files to be analyzed. '
'Can be omitted, in which case the entire code base '
'is analyzed. The source argument is ignored if '
'there is anything fed through stdin, in which case '
'the analysis is only performed on the files changed '
'in the patch streamed through stdin. This is called '
'the diff mode.')
@CommandArgument('--checks', '-c', default=[], metavar='checks', nargs='*',
help='Static analysis checks to enable.')
@CommandArgument('--jobs', '-j', default='0', metavar='jobs', type=int,
help='Number of concurrent jobs to run.'
' Default is the number of CPUs.')
@CommandArgument('--task', '-t', type=str,
default='compileLocalWithGeckoBinariesNoMinApiDebugSources',
help='Which gradle tasks to use to compile the java codebase.')
def check_java(self, source=['mobile'], jobs=2, strip=1, verbose=False, checks=[],
task='compileLocalWithGeckoBinariesNoMinApiDebugSources',
skip_export=False):
self._set_log_level(verbose)
self.log_manager.enable_all_structured_loggers()
if self.substs['MOZ_BUILD_APP'] != 'mobile/android':
self.log(logging.WARNING, 'static-analysis', {},
'Cannot check java source code unless you are building for android!')
return 1
# if source contains the whole mobile folder, then we just have to
# analyze everything
check_all = any(i.rstrip(os.sep).split(os.sep)[-1] == 'mobile' for i in source)
# gather all java sources from the source variable
java_sources = []
if not check_all:
java_sources = self._get_java_files(source)
if not java_sources:
return 0
if not skip_export:
rc = self._build_export(jobs=jobs, verbose=verbose)
if rc != 0:
return rc
rc = self._get_infer(verbose=verbose)
if rc != 0:
self.log(logging.WARNING, 'static-analysis', {},
'This command is only available for linux64!')
return rc
# which checkers to use, and which folders to exclude
all_checkers, third_party_path = self._get_infer_config()
checkers, excludes = self._get_infer_args(
checks=checks or all_checkers,
third_party_path=third_party_path
)
gradlew = mozpath.join(self.topsrcdir, 'gradlew')
# infer capture command
capture_cmd = [self._infer_path, 'capture'] + excludes + \
['--', gradlew, task]
tmp_file, args = self._get_infer_source_args(java_sources)
# infer analyze command
analysis_cmd = [self._infer_path, 'analyze', '--keep-going'] + \
checkers + args
# capture, then analyze the sources
for args in [[gradlew, 'clean'], capture_cmd, analysis_cmd]:
rc = self.run_process(args=args, cwd=self.topsrcdir,
pass_thru=True)
# if a command fails, break and close the tmp file before returning
if rc != 0:
break
if tmp_file:
tmp_file.close()
return rc
def _get_java_files(self, sources):
java_sources = []
for i in sources:
f = mozpath.join(self.topsrcdir, i)
if os.path.isdir(f):
for root, dirs, files in os.walk(f):
dirs.sort()
for file in sorted(files):
if file.endswith('.java'):
java_sources.append(mozpath.join(root, file))
elif f.endswith('.java'):
java_sources.append(f)
return java_sources
def _get_infer_source_args(self, sources):
'''Return the arguments to only analyze <sources>'''
if not sources:
return (None, [])
# create a temporary file in which we place all sources
# this is used by the analysis command to only analyze certain files
f = tempfile.NamedTemporaryFile()
for source in sources:
f.write(source+'\n')
f.flush()
return (f, ['--changed-files-index', f.name])
def _get_infer_config(self):
'''Load the infer config file.'''
import yaml
checkers = []
tp_path = ''
with open(mozpath.join(self.topsrcdir, 'tools',
'infer', 'config.yaml')) as f:
try:
config = yaml.safe_load(f)
for item in config['infer_checkers']:
if item['publish']:
checkers.append(item['name'])
tp_path = mozpath.join(self.topsrcdir, config['third_party'])
except Exception as e:
print('Looks like config.yaml is not valid, so we are unable '
'to determine default checkers, and which folder to '
'exclude, using defaults provided by infer')
return checkers, tp_path
def _get_infer_args(self, checks, third_party_path):
'''Return the arguments which include the checkers <checks>, and
excludes all folder in <third_party_path>.'''
checkers = ['-a', 'checkers']
excludes = []
for checker in checks:
checkers.append('--' + checker)
with open(third_party_path) as f:
for line in f:
excludes.append('--skip-analysis-in-path')
excludes.append(line.strip('\n'))
return checkers, excludes
def _get_clang_tidy_command(self, checks, header_filter, sources, jobs, fix): def _get_clang_tidy_command(self, checks, header_filter, sources, jobs, fix):
@ -1746,7 +1880,8 @@ class StaticAnalysis(MachCommandBase):
' this baseline we will test future results.') ' this baseline we will test future results.')
@CommandArgument('--intree-tool', '-i', default=False, action='store_true', @CommandArgument('--intree-tool', '-i', default=False, action='store_true',
help='Use a pre-aquired in-tree clang-tidy package.') help='Use a pre-aquired in-tree clang-tidy package.')
@CommandArgument('checker_names', nargs='*', default=[], help='Checkers that are going to be auto-tested.') @CommandArgument('checker_names', nargs='*', default=[],
help='Checkers that are going to be auto-tested.')
def autotest(self, verbose=False, dump_results=False, intree_tool=False, checker_names=[]): def autotest(self, verbose=False, dump_results=False, intree_tool=False, checker_names=[]):
# If 'dump_results' is True than we just want to generate the issues files for each # If 'dump_results' is True than we just want to generate the issues files for each
# checker in particulat and thus 'force_download' becomes 'False' since we want to # checker in particulat and thus 'force_download' becomes 'False' since we want to
@ -1793,7 +1928,8 @@ class StaticAnalysis(MachCommandBase):
rc = self._get_clang_tools(force=force_download, verbose=verbose) rc = self._get_clang_tools(force=force_download, verbose=verbose)
if rc != 0: if rc != 0:
self.log(logging.ERROR, 'ERROR: static-analysis', {}, 'clang-tidy unable to locate package.') self.log(logging.ERROR, 'ERROR: static-analysis', {},
'clang-tidy unable to locate package.')
return self.TOOLS_FAILED_DOWNLOAD return self.TOOLS_FAILED_DOWNLOAD
self._clang_tidy_base_path = mozpath.join(self.topsrcdir, "tools", "clang-tidy") self._clang_tidy_base_path = mozpath.join(self.topsrcdir, "tools", "clang-tidy")
@ -1805,7 +1941,8 @@ class StaticAnalysis(MachCommandBase):
platform, _ = self.platform platform, _ = self.platform
if platform not in config['platforms']: if platform not in config['platforms']:
self.log(logging.ERROR, 'static-analysis', {},"RUNNING: clang-tidy autotest for platform {} not supported.".format(platform)) self.log(logging.ERROR, 'static-analysis', {},
"RUNNING: clang-tidy autotest for platform {} not supported.".format(platform))
return TOOLS_UNSUPORTED_PLATFORM return TOOLS_UNSUPORTED_PLATFORM
import concurrent.futures import concurrent.futures
@ -1875,11 +2012,11 @@ class StaticAnalysis(MachCommandBase):
@StaticAnalysisSubCommand('static-analysis', 'install', @StaticAnalysisSubCommand('static-analysis', 'install',
'Install the static analysis helper tool') 'Install the static analysis helper tool')
@CommandArgument('source', nargs='?', type=str, @CommandArgument('source', nargs='?', type=str,
help='Where to fetch a local archive containing the static-analysis and format helper tool.' help='Where to fetch a local archive containing the static-analysis and '
'It will be installed in ~/.mozbuild/clang-tools/.' 'format helper tool.'
'Can be omitted, in which case the latest clang-tools ' 'It will be installed in ~/.mozbuild/clang-tools and ~/.mozbuild/infer.'
' helper for the platform would be automatically ' 'Can be omitted, in which case the latest clang-tools and infer '
'detected and installed.') 'helper for the platform would be automatically detected and installed.')
@CommandArgument('--skip-cache', action='store_true', @CommandArgument('--skip-cache', action='store_true',
help='Skip all local caches to force re-fetching the helper tool.', help='Skip all local caches to force re-fetching the helper tool.',
default=False) default=False)
@ -1887,6 +2024,10 @@ class StaticAnalysis(MachCommandBase):
self._set_log_level(verbose) self._set_log_level(verbose)
rc = self._get_clang_tools(force=True, skip_cache=skip_cache, rc = self._get_clang_tools(force=True, skip_cache=skip_cache,
source=source, verbose=verbose) source=source, verbose=verbose)
if rc == 0:
# XXX ignore the return code because if it fails or not, infer is
# not mandatory, but clang-tidy is
self._get_infer(force=True, skip_cache=skip_cache, verbose=verbose)
return rc return rc
@StaticAnalysisSubCommand('static-analysis', 'clear-cache', @StaticAnalysisSubCommand('static-analysis', 'clear-cache',
@ -1895,20 +2036,31 @@ class StaticAnalysis(MachCommandBase):
self._set_log_level(verbose) self._set_log_level(verbose)
rc = self._get_clang_tools(force=True, download_if_needed=True, skip_cache=True, rc = self._get_clang_tools(force=True, download_if_needed=True, skip_cache=True,
verbose=verbose) verbose=verbose)
if rc == 0:
self._get_infer(force=True, download_if_needed=True, skip_cache=True,
verbose=verbose)
if rc != 0: if rc != 0:
return rc return rc
return self._artifact_manager.artifact_clear_cache()
self._artifact_manager.artifact_clear_cache()
@StaticAnalysisSubCommand('static-analysis', 'print-checks', @StaticAnalysisSubCommand('static-analysis', 'print-checks',
'Print a list of the static analysis checks performed by default') 'Print a list of the static analysis checks performed by default')
def print_checks(self, verbose=False): def print_checks(self, verbose=False):
self._set_log_level(verbose) self._set_log_level(verbose)
rc = self._get_clang_tools(verbose=verbose) rc = self._get_clang_tools(verbose=verbose)
if rc == 0:
rc = self._get_infer(verbose=verbose)
if rc != 0: if rc != 0:
return rc return rc
args = [self._clang_tidy_path, '-list-checks', '-checks=%s' % self._get_checks()] args = [self._clang_tidy_path, '-list-checks', '-checks=%s' % self._get_checks()]
return self._run_command_in_objdir(args=args, pass_thru=True) rc = self._run_command_in_objdir(args=args, pass_thru=True)
if rc != 0:
return rc
checkers, _ = self._get_infer_config()
print('Infer checks:')
for checker in checkers:
print(' '*4 + checker)
return 0
@Command('clang-format', category='misc', description='Run clang-format on current changes') @Command('clang-format', category='misc', description='Run clang-format on current changes')
@CommandArgument('--show', '-s', action='store_true', default=False, @CommandArgument('--show', '-s', action='store_true', default=False,
@ -1939,7 +2091,8 @@ class StaticAnalysis(MachCommandBase):
test_file_path_cpp = test_file_path + '.cpp' test_file_path_cpp = test_file_path + '.cpp'
test_file_path_json = test_file_path + '.json' test_file_path_json = test_file_path + '.json'
self.log(logging.INFO, 'static-analysis', {},"RUNNING: clang-tidy checker {}.".format(check)) self.log(logging.INFO, 'static-analysis', {},
"RUNNING: clang-tidy checker {}.".format(check))
# Verify if this checker actually exists # Verify if this checker actually exists
if not check in self._clang_tidy_checks: if not check in self._clang_tidy_checks:
@ -2231,6 +2384,47 @@ class StaticAnalysis(MachCommandBase):
args += [':({0}){1}'.format(','.join(magics), pattern)] args += [':({0}){1}'.format(','.join(magics), pattern)]
return args return args
def _get_infer(self, force=False, skip_cache=False,
download_if_needed=True, verbose=False):
rc, config, _ = self._get_config_environment()
if rc != 0:
return rc
infer_path = mozpath.join(self._mach_context.state_dir, 'infer')
self._infer_path = mozpath.join(infer_path, 'infer', 'bin',
'infer' +
config.substs.get('BIN_SUFFIX', ''))
if os.path.exists(self._infer_path) and not force:
return 0
else:
if os.path.isdir(infer_path) and download_if_needed:
# The directory exists, perhaps it's corrupted? Delete it
# and start from scratch.
import shutil
shutil.rmtree(infer_path)
return self._get_infer(force=force, skip_cache=skip_cache,
verbose=verbose,
download_if_needed=download_if_needed)
os.mkdir(infer_path)
self._artifact_manager = PackageFrontend(self._mach_context)
if not download_if_needed:
return 0
job, _ = self.platform
if job != 'linux64':
return -1
else:
job += '-infer'
# We want to unpack data in the infer mozbuild folder
currentWorkingDir = os.getcwd()
os.chdir(infer_path)
rc = self._artifact_manager.artifact_toolchain(verbose=verbose,
skip_cache=skip_cache,
from_build=[job],
no_unpack=False,
retry=0)
# Change back the cwd
os.chdir(currentWorkingDir)
return rc
def _run_clang_format_diff(self, clang_format_diff, clang_format, show): def _run_clang_format_diff(self, clang_format_diff, clang_format, show):
# Run clang-format on the diff # Run clang-format on the diff
# Note that this will potentially miss a lot things # Note that this will potentially miss a lot things
@ -2532,4 +2726,3 @@ class Analyze(MachCommandBase):
res = 'Please make sure you have a local tup db *or* specify the location with --path.' res = 'Please make sure you have a local tup db *or* specify the location with --path.'
print ('Could not find a valid tup db in ' + path, res, sep='\n') print ('Could not find a valid tup db in ' + path, res, sep='\n')
return 1 return 1

23
tools/infer/config.yaml Normal file
Просмотреть файл

@ -0,0 +1,23 @@
---
target: obj-x86_64-pc-linux-gnu
# It is used by 'mach static-analysis' and 'mozreview static-analysis bot'
# in order to have consistency across the used checkers.
platforms:
- linux64
infer_checkers:
- name: check-nullable
publish: !!bool yes
- name: eradicate
publish: !!bool no
- name: quandary
publish: !!bool yes
- name: starvation
publish: !!bool yes
- name: litho
publish: !!bool yes
- name: racerd
publish: !!bool yes
- name: liveness
publish: !!bool yes
# Third party files from mozilla-central
third_party: tools/rewriting/ThirdPartyPaths.txt