Skip to content

Commit

Permalink
[analyzer] config file option for the parse subcommand
Browse files Browse the repository at this point in the history
Add support to use a config file by the parse subcommand.
The subcommand arguments can be saved and used from the
CodeChecker config file.

Common config file checking and parsing parts were
moved to the codechecker_common module because
the analyzer and the server parts use the same code.

The config file 'analyze' command related config
key was 'analyzer' to keep it in sync with the subcommands
it is renamed to 'analyze' in the documentation and examples.
The code was made backward compatible to accept both cases
and give a warning if both sections are available
in the configuration file.

The subocommand configuration examples were updated
and extended.
  • Loading branch information
Gyorgy Orban committed Aug 20, 2020
1 parent 5e5214f commit 929ea81
Show file tree
Hide file tree
Showing 10 changed files with 251 additions and 86 deletions.
48 changes: 8 additions & 40 deletions analyzer/codechecker_analyzer/cmd/analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from codechecker_analyzer.arg import OrderedCheckersAction
from codechecker_analyzer.buildlog import log_parser

from codechecker_common import arg, logger, skiplist_handler
from codechecker_common import arg, logger, skiplist_handler, cmd_config
from codechecker_common.util import load_json_or_empty


Expand Down Expand Up @@ -647,44 +647,8 @@ def add_arguments_to_parser(parser):
"OWN RISK!")

logger.add_verbose_arguments(parser)
parser.set_defaults(func=main,
func_process_config_file=process_config_file)


def process_config_file(args):
"""
Handler to get config file options.
"""
if args.config_file and os.path.exists(args.config_file):
cfg = load_json_or_empty(args.config_file, default={})
return cfg.get('analyzer', [])


def check_config_file(args):
"""
LOG and check about the config file usage.
If a config file is set but does not exist the program will
exit.
LOG is not initialized in the process_config_file function yet
so we can not log the usage there. Using print will
always print out the config file data which can mess up the
tests depending on the output.
"""

if args.config_file and not os.path.exists(args.config_file):
LOG.error("Configuration file '%s' does not exist.",
args.config_file)
sys.exit(1)
elif not args.config_file:
return

cfg = load_json_or_empty(args.config_file, default={})
if cfg.get("enabled"):
LOG.debug("Using config file: '%s'.", args.config_file)
return cfg.get('analyzer', [])

LOG.debug("Config file '%s' is available but disabled.", args.config_file)
parser.set_defaults(
func=main, func_process_config_file=cmd_config.process_config_file)


def __get_skip_handler(args):
Expand Down Expand Up @@ -769,7 +733,11 @@ def main(args):
"""
logger.setup_logger(args.verbose if 'verbose' in args else None)

check_config_file(args)
try:
cmd_config.check_config_file(args)
except FileNotFoundError as fnerr:
LOG.error(fnerr)
sys.exit(1)

if not os.path.exists(args.logfile):
LOG.error("The specified logfile '%s' does not exist!", args.logfile)
Expand Down
27 changes: 25 additions & 2 deletions analyzer/codechecker_analyzer/cmd/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

from codechecker_analyzer import analyzer_context, suppress_handler

from codechecker_common import arg, logger, plist_parser, util
from codechecker_common import arg, logger, plist_parser, util, cmd_config
from codechecker_common.skiplist_handler import SkipListHandler
from codechecker_common.source_code_comment_handler import \
REVIEW_STATUS_VALUES, SourceCodeCommentHandler, SpellException
Expand Down Expand Up @@ -398,6 +398,22 @@ def add_arguments_to_parser(parser):
"containing analysis results which should be "
"parsed and printed.")

parser.add_argument('--config',
dest='config_file',
required=False,
help="R|Allow the configuration from an "
"explicit JSON based configuration file. "
"The value of the 'parse' key in the "
"config file will be emplaced as command "
"line arguments. The format of "
"configuration file is:\n"
"{\n"
" \"parse\": [\n"
" \"--trim-path-prefix\",\n"
" \"$HOME/workspace\"\n"
" ]\n"
"}")

parser.add_argument('-t', '--type', '--input-format',
dest="input_format",
required=False,
Expand Down Expand Up @@ -492,7 +508,8 @@ def add_arguments_to_parser(parser):
', '.join(REVIEW_STATUS_VALUES)))

logger.add_verbose_arguments(parser)
parser.set_defaults(func=main)
parser.set_defaults(
func=main, func_process_config_file=cmd_config.process_config_file)


def parse(plist_file, metadata_dict, rh, file_report_map):
Expand Down Expand Up @@ -604,6 +621,12 @@ def main(args):

logger.setup_logger(args.verbose if 'verbose' in args else None)

try:
cmd_config.check_config_file(args)
except FileNotFoundError as fnerr:
LOG.error(fnerr)
sys.exit(1)

export = args.export if 'export' in args else None
if export == 'html' and 'output_path' not in args:
LOG.error("Argument --export not allowed without argument --output "
Expand Down
77 changes: 77 additions & 0 deletions analyzer/tests/functional/config/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,65 @@ def __run_analyze(self, extra_options=None):
out, _ = process.communicate()
return out, process.returncode

def __run_parse(self):
"""
Run the CodeChecker analyze command with a configuration file.
"""
# Create analyze command.
analyze_cmd = [self._codechecker_cmd, "parse", self.reports_dir,
"--config", self.config_file]

# Run analyze.
process = subprocess.Popen(
analyze_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8",
errors="ignore")
out, _ = process.communicate()
return out, process.returncode

def test_only_clangsa_config(self):
"""
Run analyze command with a config file which enables the clangsa
analyzer only.
"""
with open(self.config_file, 'w+',
encoding="utf-8", errors="ignore") as config_f:
json.dump({
'analyze': ['--analyzers', 'clangsa']}, config_f)

out, returncode = self.__run_analyze()

self.assertEqual(returncode, 0)
self.assertIn("clangsa analyzed simple.cpp", out)
self.assertNotIn("clang-tidy analyzed simple.cpp", out)

def test_only_clangsa_config_backward_compatible_mixed(self):
"""
Test the 'analyzer' configuration option backward compatibility.
The config name should be 'analyze' to be in sync with the
subcommand names.
"""
with open(self.config_file, 'w+',
encoding="utf-8", errors="ignore") as config_f:
json.dump({
'analyze': ['--analyzers', 'clangsa'],
'analyzer': ['--analyzers', 'clang-tidy']},
config_f)

out, returncode = self.__run_analyze()

self.assertEqual(returncode, 0)
self.assertIn("clangsa analyzed simple.cpp", out)
self.assertNotIn("clang-tidy analyzed simple.cpp", out)

def test_only_clangsa_config_backward_compatibility(self):
"""
Test the 'analyzer' configuration option backward compatibility.
The config name should be 'analyze' to be in sync with the
subcommand names.
"""
with open(self.config_file, 'w+',
encoding="utf-8", errors="ignore") as config_f:
json.dump({
Expand Down Expand Up @@ -127,3 +181,26 @@ def test_empty_config(self):
self.assertEqual(returncode, 0)
self.assertIn("clangsa analyzed simple.cpp", out)
self.assertIn("clang-tidy analyzed simple.cpp", out)

def test_parse_config(self):
"""
Run analyze command with a config file which enables the clangsa
analyzer only and parse the results with a parse command
config.
"""
with open(self.config_file, 'w+',
encoding="utf-8", errors="ignore") as config_f:
json.dump({
'analyzer': ['--analyzers', 'clangsa'],
'parse': ['--trim-path-prefix', '/workspace']},
config_f)

out, returncode = self.__run_analyze()

self.assertEqual(returncode, 0)
self.assertIn("clangsa analyzed simple.cpp", out)
self.assertNotIn("clang-tidy analyzed simple.cpp", out)

out, returncode = self.__run_parse()
print(out)
self.assertEqual(returncode, 0)
5 changes: 4 additions & 1 deletion bin/CodeChecker.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,10 @@ def signal_handler(signum, frame):
# extend the system argument list with these options and try to parse
# the argument list again to validate it.
if 'func_process_config_file' in args:
cfg_args = args.func_process_config_file(args)
if len(sys.argv) > 1:
called_sub_command = sys.argv[1]

cfg_args = args.func_process_config_file(args, called_sub_command)
if cfg_args:
# Expand environment variables in the arguments.
cfg_args = [os.path.expandvars(cfg) for cfg in cfg_args]
Expand Down
58 changes: 58 additions & 0 deletions codechecker_common/cmd_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# -------------------------------------------------------------------------
#
# Part of the CodeChecker project, under the Apache License v2.0 with
# LLVM Exceptions. See LICENSE for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#
# -------------------------------------------------------------------------
import os

from codechecker_common.util import load_json_or_empty
from codechecker_common import logger

LOG = logger.get_logger('system')


def process_config_file(args, subcommand_name):
"""
Handler to get config file options.
"""
if 'config_file' not in args:
return {}
if args.config_file and os.path.exists(args.config_file):
cfg = load_json_or_empty(args.config_file, default={})

# The subcommand name is analyze but the
# configuration section name is analyzer.
if subcommand_name == 'analyze':
# The config value can be 'analyze' or 'analyzer'
# for backward compatibility.
analyze_cfg = cfg.get("analyze", [])
analyzer_cfg = cfg.get("analyzer", [])
if analyze_cfg:
if analyzer_cfg:
LOG.warning("There is an 'analyze' and an 'analyzer' "
"config configuration option in the config "
"file. Please use the 'analyze' value to be "
"in sync with the subcommands.\n"
"Using the 'analyze' configuration.")
return analyze_cfg
if analyzer_cfg:
return analyzer_cfg

return cfg.get(subcommand_name, [])


def check_config_file(args):
"""Check if a config file is set in the arguments and if the file exists.
returns - None if not set or the file exists or
FileNotFoundError exception if the set config file is missing.
"""
if 'config_file' not in args:
return

if 'config_file' in args and args.config_file \
and not os.path.exists(args.config_file):
raise FileNotFoundError(
f"Configuration file '{args.config_file}' does not exist.")
31 changes: 23 additions & 8 deletions config/codechecker.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
{
"analyzer": [
"--enable", "core.DivideZero",
"--enable", "core.CallAndMessage",
"--disable", "alpha",
"--analyzers", "clangsa", "clang-tidy",
"--clean"
]
}
"analyze": [
"--enable=core.DivideZero",
"--enable=core.CallAndMessage",
"--analyzer-config",
"clangsa:unroll-loops=true",
"--checker-config",
"clang-tidy:google-readability-function-size.StatementThreshold=100"
"--report-hash", "context-free-v2"
"--verbose=debug",
"--clean"
],
"parse": [
"--trim-path-prefix",
"/$HOME/workspace"
],
"server": [
"--workspace=$HOME/workspace",
"--port=9090"
],
"store": [
"--url", "localhost:9090/Default"
]
}
Loading

0 comments on commit 929ea81

Please sign in to comment.