Skip to content

Commit

Permalink
[cmd] Applying clang-tidy fixits
Browse files Browse the repository at this point in the history
CodeChecker can apply automatic fixes. For this clang-apply-replacements binary
is used, so it must be found on the computer.
  • Loading branch information
bruntib committed Jul 6, 2020
1 parent 77b02df commit 330fccf
Show file tree
Hide file tree
Showing 7 changed files with 283 additions and 1 deletion.
23 changes: 23 additions & 0 deletions analyzer/bin/codechecker-fixit
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env python3
# -------------------------------------------------------------------------
#
# 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
#
# -------------------------------------------------------------------------
"""
Entry point for the fixit command.
"""

from importlib.util import spec_from_file_location
import os

THIS_PATH = os.path.dirname(os.path.abspath(__file__))
CC = os.path.join(THIS_PATH, "CodeChecker")

# Load CodeChecker from the current folder (the wrapper script (without .py))
CodeChecker = spec_from_file_location('CodeChecker', CC).loader.load_module()

# Execute CC's main script with the current subcommand.
CodeChecker.main("fixit")
15 changes: 15 additions & 0 deletions analyzer/codechecker_analyzer/analyzer_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ def __init__(self, package_root, pckg_layout, cfg_dict):

self.__set_version()
self.__populate_analyzers()
self.__populate_replacer()

def set_env(self, env_vars):
"""
Expand Down Expand Up @@ -151,6 +152,16 @@ def __populate_analyzers(self):

self.__analyzers[name] = os.path.realpath(compiler_binary)

def __populate_replacer(self):
""" Set clang-apply-replacements tool. """
replacer_binary = self.pckg_layout.get('clang-apply-replacements')

if os.path.dirname(replacer_binary):
# Check if it is a package relative path.
self.__replacer = os.path.join(self._package_root, replacer_binary)
else:
self.__replacer = find_executable(replacer_binary)

@property
def checker_config(self):
return self.__checker_config
Expand Down Expand Up @@ -242,6 +253,10 @@ def ld_lib_path_extra(self):
def analyzer_binaries(self):
return self.__analyzers

@property
def replacer_binary(self):
return self.__replacer

@property
def package_root(self):
return self._package_root
Expand Down
180 changes: 180 additions & 0 deletions analyzer/codechecker_analyzer/cmd/fixit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# -------------------------------------------------------------------------
#
# 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
#
# -------------------------------------------------------------------------
"""
Defines the CodeChecker action for applying fixits.
"""

import argparse
import os
import re
import subprocess
import tempfile
import yaml

from codechecker_analyzer import analyzer_context
from codechecker_common import arg, logger

LOG = logger.get_logger('system')


def get_argparser_ctor_args():
"""
This method returns a dict containing the kwargs for constructing an
argparse.ArgumentParser (either directly or as a subparser).
"""

return {
'prog': 'CodeChecker fixit',
'formatter_class': arg.RawDescriptionDefaultHelpFormatter,
'description': """
Some analyzers may suggest some automatic bugfixes. Most of the times these are
style issues which can be fixed easily. This command handles the listing and
application of these automatic fixes.""",
'help': "Apply automatic fixes based on the suggestions of the "
"analyzers"
}


def add_arguments_to_parser(parser):
"""
Add the subcommand's arguments to the given argparse.ArgumentParser.
"""

parser.add_argument('input',
type=str,
nargs='+',
metavar='folder',
help="The analysis result folder(s) containing "
"analysis results and fixits which should be "
"applied.")
parser.add_argument('-l', '--list',
action='store_true',
default=argparse.SUPPRESS,
help="List the available automatic fixes.")
parser.add_argument('--checker-name',
nargs='*',
help='Filter results by checker names. The checker '
'name can contain multiple * quantifiers which '
'matches any number of characters (zero or '
'more). So for example "*DeadStores" will '
'match "deadcode.DeadStores".')
parser.add_argument('--file',
metavar='FILE_PATH',
nargs='*',
help='Filter results by file path. The file path can '
'contain multiple * quantifiers which matches '
'any number of characters (zero or more). So if '
'you have /a/x.cpp and /a/y.cpp then "/a/*.cpp" '
'selects both.')

logger.add_verbose_arguments(parser)
parser.set_defaults(func=main)


def clang_tidy_fixit_filter(content, checker_names, file_paths):
"""
This function filters the content of a replacement .yaml file.
content -- The content of a replacement .yaml file parsed to an object by
yaml module.
checker_names -- A list of checker names possibly containing * joker
characters. The full checker name must match in order to
apply it.
file_paths -- A list of file paths to which the fixits will be applied.
A file path may possibly contain * joker characters. The
full path must match in order to apply it.
"""
def make_regex(parts):
if not parts:
return re.compile('.*')
parts = map(lambda part: re.escape(part).replace(r'\*', '.*'), parts)
return re.compile('|'.join(parts) + '$')

checker_names = make_regex(checker_names)
file_paths = make_regex(file_paths)

content['Diagnostics'] = list(filter(
lambda diag: checker_names.match(diag['DiagnosticName']) and
len(diag['DiagnosticMessage']['Replacements']) != 0 and
file_paths.match(diag['DiagnosticMessage']['FilePath']),
content['Diagnostics']))


def list_fixits(inputs, checker_names, file_paths):
"""
This function dumps the .yaml files to the standard output like a "dry run"
with the replacements. See clang_tidy_fixit_filter() for the documentation
of the filter options.
inputs -- A list of report directories which contains the fixit dumps in a
subdirectory named "fixit".
"""
for i in inputs:
fixit_dir = os.path.join(i, 'fixit')

if not os.path.isdir(fixit_dir):
continue

for fixit_file in os.listdir(fixit_dir):
with open(os.path.join(fixit_dir, fixit_file),
encoding='utf-8', errors='ignore') as f:
content = yaml.load(f, Loader=yaml.BaseLoader)
clang_tidy_fixit_filter(content, checker_names, file_paths)
print(yaml.dump(content))


def apply_fixits(inputs, checker_names, file_paths):
"""
This function applies the replacements from the .yaml files.
inputs -- A list of report directories which contains the fixit dumps in a
subdirectory named "fixit".
"""
for i in inputs:
fixit_dir = os.path.join(i, 'fixit')

if not os.path.isdir(fixit_dir):
continue

with tempfile.TemporaryDirectory() as out_dir:
for fixit_file in os.listdir(fixit_dir):
with open(os.path.join(fixit_dir, fixit_file),
encoding='utf-8', errors='ignore') as f:
content = yaml.load(f, Loader=yaml.BaseLoader)
clang_tidy_fixit_filter(content, checker_names, file_paths)

if len(content['Diagnostics']) != 0:
with open(os.path.join(out_dir, fixit_file), 'w',
encoding='utf-8', errors='ignore') as out:
yaml.dump(content, out)

proc = subprocess.Popen([
analyzer_context.get_context().replacer_binary,
out_dir])
proc.communicate()


def main(args):
"""
Entry point for the command handling automatic fixes.
TODO: Currently clang-tidy is the only tool which supports the dumping of
fixit replacements. In this script we assume that the replacement dump
.yaml files are in the format so clang-apply-replacement Clang tool can
consume them.
"""

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

context = analyzer_context.get_context()

if not context.replacer_binary:
LOG.error("clang-apply-replacements tool is not found")
return

if 'list' in args:
list_fixits(args.input, args.checker_name, args.file)
else:
apply_fixits(args.input, args.checker_name, args.file)
3 changes: 2 additions & 1 deletion config/package_layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"analyzers": {
"clangsa": "clang",
"clang-tidy": "clang-tidy"
}
},
"clang-apply-replacements": "clang-apply-replacements"
},
"compiler_resource_dir" : null,
"ld_lib_path_extra" : [],
Expand Down
42 changes: 42 additions & 0 deletions docs/analyzer/user_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Table of Contents
* [Statistical analysis mode](#statistical)
* [`parse`](#parse)
* [Exporting source code suppression to suppress file](#suppress-file)
* [`fixit`](#fixit)
* [`checkers`](#checkers)
* [`analyzers`](#analyzers)
* [`Configuring Clang version`](#clang_version)
Expand Down Expand Up @@ -1378,6 +1379,47 @@ then the results of the analysis can be printed with
CodeChecker parse ./my_plists
```

## `fixit` <a name="fixit"></a>

ClangTidy is able to provide suggestions on automatic fixes of reported issues.
For example there is a ClangTidy checker which suggests using
`collection.empty()` instead of `collection.size() != 0` expression. These
simple changes can be applied directy in the source code. `CodeChecker fixit`
command handles these automatic fixes.

```
usage: CodeChecker fixit [-h] [-l]
[--checker-name [CHECKER_NAME [CHECKER_NAME...]]]
[--file [FILE [FILE ...]]]
[--verbose {info,debug,debug_analyzer}]
folder [folder ...]
Some analyzers may suggest some automatic bugfixes. Most of the times these are
style issues which can be fixed easily. This command handles the listing and
application of these automatic fixes.
positional arguments:
folder The analysis result folder(s) containing analysis
results and fixits which should be applied.
optional arguments:
-h, --help show this help message and exit
-l, --list List the available automatic fixes.
--checker-name [CHECKER_NAME [CHECKER_NAME ...]]
Filter results by checker names. The checker name can
contain multiple * quantifiers which matches any number
of characters (zero or more). So for example
"*DeadStores" will match "deadcode.DeadStores".
(default: None)
--file [FILE_PATH [FILE_PATH ...]]
Filter results by file path. The file path can contain
multiple * quantifiers which matches any number of
characters (zero or more). So if you have /a/x.cpp and
/a/y.cpp then "/a/*.cpp" selects both. (default: None)
--verbose {info,debug,debug_analyzer}
Set verbosity level.
```

## `checkers`<a name="checkers"></a>

List the checkers available in the installed analyzers which can be used when
Expand Down
6 changes: 6 additions & 0 deletions docs/package_layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,9 @@ CodeChecker command to `yes` or `1` to enforce taking the analyzers from the
`PATH` instead of the given binaries. If this option is set you can also
configure the plugin directory of the Clang Static Analyzer by using the
`CC_CLANGSA_PLUGIN_DIR` environment variable.

### Replacer section
This section is a key-value component. The key is `clang-apply-replacements`
and the value is the same by default. This is the name of a Clang tool which
applies textual replacements in the source code directly. `CodeChecker fixit`
command uses this tool, see its documentation for further details.
15 changes: 15 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,21 @@ action).

![Analysis results in static HTML files](images/static_html.png)

Some CodeChecker reports are so easy to fix that even static analyzer tools may
provide automatic fixes on them. In CodeChecker you can list these with the
following command after having analysis reports in `reports` directory:
```sh
CodeChecker fixit --list reports
```

The listed automatic fixes can be applied by omitting `--list` flag. See
`--help` on details of filtering automatic fixes. You can also use the JSON
format output of `CodeChecker cmd diff` command if you want to fix only new
findings:
```sh
CodeChecker cmd diff -b reports1 -n reports2 --new -o json | CodeChecker fixit
```

## Step 4: Store analysis results in a CodeChecker DB and visualize results <a name="step-4"></a>

You can store the analysis results in a central database and view the results
Expand Down

0 comments on commit 330fccf

Please sign in to comment.