Skip to content

Commit

Permalink
[analyzers] Deprecate --all and --details for analyzers
Browse files Browse the repository at this point in the history
* Deprecate both `--all` and `--details`, because they were more
  confusing than useful
* We list all supported (not available) analyzers by default, but print
  a warning message when its not available.
* We used to query the version of each analyzer binary in a uniform way,
  but clang, cppcheck, gcc prints their version using different flags,
  and in different formats. I changed this to call the analyzer plugins'
  get_binary_version method (which I also implemented in this patch).
  • Loading branch information
Szelethus committed Oct 24, 2023
1 parent fcd5f23 commit 7af389a
Show file tree
Hide file tree
Showing 8 changed files with 91 additions and 101 deletions.
3 changes: 2 additions & 1 deletion analyzer/codechecker_analyzer/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,8 @@ def perform_analysis(args, skip_handlers, actions, metadata_tool,

# TODO: cppcheck may require a different environment than clang.
version = analyzer_types.supported_analyzers[analyzer] \
.get_version(context.analyzer_env)
.get_binary_version(context.analyzer_binaries[analyzer],
context.analyzer_env)
metadata_info['analyzer_statistics']['version'] = version

metadata_tool['analyzers'][analyzer] = metadata_info
Expand Down
11 changes: 11 additions & 0 deletions analyzer/codechecker_analyzer/analyzers/analyzer_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ def resolve_missing_binary(cls, configured_binary, environ):
"""
raise NotImplementedError("Subclasses should implement this!")

@abstractmethod
def get_binary_version(self, configured_binary, environ, details=False) \
-> str:
"""
Return the version number of the binary that CodeChecker found, even
if its incompatible. If details is true, additional version information
is provided. If details is false, the return value should be
convertible to a distutils.version.StrictVersion type.
"""
raise NotImplementedError("Subclasses should implement this!")

@classmethod
def is_binary_version_incompatible(cls, configured_binary, environ) \
-> Optional[str]:
Expand Down
3 changes: 1 addition & 2 deletions analyzer/codechecker_analyzer/analyzers/analyzer_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,7 @@ def construct_analyzer(buildaction,
LOG.error('Unsupported analyzer type: %s', analyzer_type)
return analyzer

except Exception as ex:
LOG.debug_analyzer(ex)
except Exception:
# We should've detected well before this point that something is off
# with the analyzer. We can't recover here.
raise
Expand Down
13 changes: 8 additions & 5 deletions analyzer/codechecker_analyzer/analyzers/clangsa/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,16 +171,19 @@ def __add_plugin_load_flags(cls, analyzer_cmd: List[str]):
analyzer_cmd.extend(["-load", plugin])

@classmethod
def get_version(cls, env=None):
""" Get analyzer version information. """
version = [cls.analyzer_binary(), '--version']
def get_binary_version(self, configured_binary, environ, details=False) \
-> str:
if details:
version = [configured_binary, '--version']
else:
version = [configured_binary, '-dumpversion']
try:
output = subprocess.check_output(version,
env=env,
env=environ,
universal_newlines=True,
encoding="utf-8",
errors="ignore")
return output
return output.strip()
except (subprocess.CalledProcessError, OSError) as oerr:
LOG.warning("Failed to get analyzer version: %s",
' '.join(version))
Expand Down
22 changes: 17 additions & 5 deletions analyzer/codechecker_analyzer/analyzers/clangtidy/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,16 @@ def need_asterisk(checker: str) -> bool:
return result


def parse_version(tidy_output):
"""
Parse clang-tidy version output and return the version number.
"""
version_re = re.compile(r'.*version (?P<version>[\d\.]+)', re.S)
match = version_re.match(tidy_output)
if match:
return match.group('version')


class ClangTidy(analyzer_base.SourceAnalyzer):
"""
Constructs the clang tidy analyzer commands.
Expand All @@ -220,16 +230,18 @@ def analyzer_binary(cls):
.analyzer_binaries[cls.ANALYZER_NAME]

@classmethod
def get_version(cls, env=None):
""" Get analyzer version information. """
version = [cls.analyzer_binary(), '--version']
def get_binary_version(self, configured_binary, environ, details=False) \
-> str:
version = [configured_binary, '--version']
try:
output = subprocess.check_output(version,
env=env,
env=environ,
universal_newlines=True,
encoding="utf-8",
errors="ignore")
return output
if details:
return output.strip()
return parse_version(output)
except (subprocess.CalledProcessError, OSError) as oerr:
LOG.warning("Failed to get analyzer version: %s",
' '.join(version))
Expand Down
34 changes: 10 additions & 24 deletions analyzer/codechecker_analyzer/analyzers/cppcheck/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def parse_version(cppcheck_output):
version_re = re.compile(r'^Cppcheck (?P<version>[\d\.]+)')
match = version_re.match(cppcheck_output)
if match:
return StrictVersion(match.group('version'))
return match.group('version')


class Cppcheck(analyzer_base.SourceAnalyzer):
Expand All @@ -83,16 +83,19 @@ def analyzer_binary(cls):
.analyzer_binaries[cls.ANALYZER_NAME]

@classmethod
def get_version(cls, env=None):
def get_binary_version(self, configured_binary, environ, details=False) \
-> str:
""" Get analyzer version information. """
version = [cls.analyzer_binary(), '--version']
version = [configured_binary, '--version']
try:
output = subprocess.check_output(version,
env=env,
env=environ,
universal_newlines=True,
encoding="utf-8",
errors="ignore")
return output
if details:
return output.strip()
return parse_version(output)
except (subprocess.CalledProcessError, OSError) as oerr:
LOG.warning("Failed to get analyzer version: %s",
' '.join(version))
Expand Down Expand Up @@ -333,34 +336,17 @@ def resolve_missing_binary(cls, configured_binary, env):
LOG.debug("Using '%s' for Cppcheck!", cppcheck)
return cppcheck

@classmethod
def __get_analyzer_version(cls, analyzer_binary, env):
"""
Return the analyzer version.
"""
command = [analyzer_binary, "--version"]

try:
result = subprocess.check_output(
command,
env=env,
encoding="utf-8",
errors="ignore")
return parse_version(result)
except (subprocess.CalledProcessError, OSError):
return []

@classmethod
def is_binary_version_incompatible(cls, configured_binary, environ):
"""
Check the version compatibility of the given analyzer binary.
"""
analyzer_version = \
cls.__get_analyzer_version(configured_binary, environ)
cls.get_binary_version(configured_binary, environ)

# The analyzer version should be above 1.80 because '--plist-output'
# argument was introduced in this release.
if analyzer_version >= StrictVersion("1.80"):
if StrictVersion(analyzer_version) >= StrictVersion("1.80"):
return None

return "CppCheck binary found is too old at " \
Expand Down
19 changes: 8 additions & 11 deletions analyzer/codechecker_analyzer/analyzers/gcc/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,6 @@ def analyzer_binary(cls):
return analyzer_context.get_context() \
.analyzer_binaries[cls.ANALYZER_NAME]

@classmethod
def get_version(cls, env=None):
""" Get analyzer version information. """
return cls.__get_analyzer_version(cls.analyzer_binary(), env)

def add_checker_config(self, checker_cfg):
# TODO
pass
Expand Down Expand Up @@ -174,19 +169,21 @@ def resolve_missing_binary(cls, configured_binary, env):
pass

@classmethod
def __get_analyzer_version(cls, analyzer_binary, env):
def get_binary_version(self, configured_binary, env, details=False) \
-> str:
"""
Return the analyzer version.
"""
# --version outputs a lot of garbage as well (like copyright info),
# this only contains the version info.
version = [analyzer_binary, '-dumpfullversion']
if details:
version = [configured_binary, '--version']
else:
version = [configured_binary, '-dumpfullversion']
try:
output = subprocess.check_output(version,
env=env,
encoding="utf-8",
errors="ignore")
return output
return output.strip()
except (subprocess.CalledProcessError, OSError) as oerr:
LOG.warning("Failed to get analyzer version: %s",
' '.join(version))
Expand All @@ -200,7 +197,7 @@ def is_binary_version_incompatible(cls, configured_binary, environ):
Check the version compatibility of the given analyzer binary.
"""
analyzer_version = \
cls.__get_analyzer_version(configured_binary, environ)
cls.get_binary_version(configured_binary, environ)

# The analyzer version should be above 13.0.0 because the
# '-fdiagnostics-format=sarif-file' argument was introduced in this
Expand Down
87 changes: 34 additions & 53 deletions analyzer/codechecker_analyzer/cmd/analyzers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

import argparse
import subprocess
import sys

from codechecker_report_converter import twodim

Expand Down Expand Up @@ -51,29 +50,24 @@ def add_arguments_to_parser(parser):
Add the subcommand's arguments to the given argparse.ArgumentParser.
"""

working_analyzers, _ = analyzer_types.check_supported_analyzers(
analyzer_types.supported_analyzers)

parser.add_argument('--all',
dest="all",
action='store_true',
default=argparse.SUPPRESS,
required=False,
help="Show all supported analyzers, not just the "
"available ones.")
help="DEPRECATED.")

parser.add_argument('--details',
dest="details",
action='store_true',
default=argparse.SUPPRESS,
required=False,
help="Show details about the analyzers, not just "
"their names.")
help="DEPRECATED.")

parser.add_argument('--dump-config',
dest='dump_config',
required=False,
choices=working_analyzers,
choices=analyzer_types.supported_analyzers,
help="Dump the available checker options for the "
"given analyzer to the standard output. "
"Currently only clang-tidy supports this option. "
Expand All @@ -89,7 +83,7 @@ def add_arguments_to_parser(parser):
dest='analyzer_config',
required=False,
default=argparse.SUPPRESS,
choices=working_analyzers,
choices=analyzer_types.supported_analyzers,
help="Show analyzer configuration options. These can "
"be given to 'CodeChecker analyze "
"--analyzer-config'.")
Expand Down Expand Up @@ -118,7 +112,7 @@ def main(args):
logger.setup_logger(args.verbose if 'verbose' in args else None, stream)

context = analyzer_context.get_context()
working_analyzers, errored = \
_, errored = \
analyzer_types.check_supported_analyzers(
analyzer_types.supported_analyzers)

Expand Down Expand Up @@ -158,63 +152,50 @@ def uglify(text):
return text.lower().replace(' ', '_')

if 'analyzer_config' in args:
if 'details' in args:
header = ['Option', 'Description']
else:
header = ['Option']
header = ['Option', 'Description']

if args.output_format in ['csv', 'json']:
header = list(map(uglify, header))

analyzer = args.analyzer_config
analyzer_class = analyzer_types.supported_analyzers[analyzer]
analyzer_name = args.analyzer_config
analyzer_class = analyzer_types.supported_analyzers[analyzer_name]

configs = analyzer_class.get_analyzer_config()
if not configs:
LOG.error("Failed to get analyzer configuration options for '%s' "
"analyzer! Please try to upgrade your analyzer version "
"to use this feature.", analyzer)
sys.exit(1)
LOG.warning("No analyzer configurations found for "
f"'{analyzer_name}'. If you suspsect this shouldn't "
"be the case, try to update your analyzer or check "
"whether CodeChecker found the intended binary.")

rows = [(':'.join((analyzer, c[0])), c[1]) if 'details' in args
else (':'.join((analyzer, c[0])),) for c in configs]
rows = [(':'.join((analyzer_name, c[0])), c[1]) for c in configs]

print(twodim.to_str(args.output_format, header, rows))

for err_analyzer_name, err_reason in errored:
if analyzer_name == err_analyzer_name:
LOG.warning(
f"Can't analyze with '{analyzer_name}': {err_reason}")

return

if 'details' in args:
header = ['Name', 'Path', 'Version']
else:
header = ['Name']
header = ['Name', 'Path', 'Version']

if args.output_format in ['csv', 'json']:
header = list(map(uglify, header))

rows = []
for analyzer in working_analyzers:
if 'details' not in args:
rows.append([analyzer])
else:
binary = context.analyzer_binaries.get(analyzer)
try:
version = subprocess.check_output(
[binary, '--version'], encoding="utf-8", errors="ignore")
except (subprocess.CalledProcessError, OSError):
version = 'ERROR'

rows.append([analyzer,
binary,
version])

if 'all' in args:
for analyzer, err_reason in errored:
if 'details' not in args:
rows.append([analyzer])
else:
rows.append([analyzer,
context.analyzer_binaries.get(analyzer),
err_reason])

if rows:
print(twodim.to_str(args.output_format, header, rows))
for analyzer_name in analyzer_types.supported_analyzers:
analyzer_class = analyzer_types.supported_analyzers[analyzer_name]
binary = context.analyzer_binaries.get(analyzer_name)
check_env = context.analyzer_env
version = analyzer_class.get_binary_version(binary, check_env)
if not version:
version = 'ERROR'

rows.append([analyzer_name, binary, version])

assert rows
print(twodim.to_str(args.output_format, header, rows))

for analyzer_name, err_reason in errored:
LOG.warning(f"Can't analyze with '{analyzer_name}': {err_reason}")

0 comments on commit 7af389a

Please sign in to comment.