Clang Tool Refactoring

Contents

Introduction

Clang tools can help with global refactorings of Chromium code. Clang tools can take advantage of clang's AST to perform refactorings that would be impossible with a traditional find-and-replace regexp:

Caveats

Clang tools do not work with jumbo builds.

Invocations of a clang tool runs on on only one build config at a time. For example, running the tool across a target_os="win" build won't update code that is guarded by OS_POSIX. Performing a global refactoring will often require running the tool once for each build config.

Prerequisites

A Chromium checkout created with fetch should have everything needed.

For convenience, add third_party/llvm-build/Release+Asserts/bin to $PATH.

Writing the tool

LLVM uses C++11 and CMake. Source code for Chromium clang tools lives in //tools/clang. It is generally easiest to use one of the already-written tools as the base for writing a new tool.

Edit serialization format

The header and footer are required. Each line between the header and footer represents one edit. Fields are separated by :::, and the first field must be r (for replacement). In the future, this may be extended to handle header insertion/removal. A deletion is an edit with no replacement text.

The edits are applied by apply_edits.py, which understands certain conventions:

The clang tool should munge newlines in replacement text to \0. The script knows to translate \0 back to newlines when applying edits.

When removing an element from a ‘list’ (e.g. function parameters, initializers), the clang tool should emit a deletion for just the element. The script understands how to extend the deletion to remove commas, etc. as needed.

TODO: Document more about SourceLocation and how spelling loc differs from expansion loc, etc.

Why not RefactoringTool?

While clang has a clang::tooling::RefactoringTool to automatically apply the generated replacements and save the results, it doesn't work well for Chromium:

Clang tools run actions serially, so run time scales poorly to tens of thousands of files.

A parsing error in any file (quite common in NaCl source) prevents any of the generated replacements from being applied.

It is important to use --bootstrap as there appear to be bugs in the clang library this script produces if you build it with gcc, which is the default.

Once clang is bootsrapped, incremental builds can be done by invoking ninja in the third_party/llvm-build/Release+Asserts directory. In particular, recompiling solely the tool you are writing can be accomplished by executing ninja rewrite_to_chrome_style (replace rewrite_to_chrome_style with your tool's name).

Running

First, build all Chromium targets to avoid failures due to missing dependencies that are generated as part of the build:

--generate-compdb can be omitted if the compile DB was already generated and the list of build flags and source files has not changed since generation.

<path 1>, <path 2>, etc are optional arguments to filter the files to run the tool against. This is helpful when sharding global refactorings into smaller chunks. For example, the following command will run the empty_string tool against just the .c, .cc, .cpp, .m, .mm files in //net. Note that the filtering is not applied to the output of the tool - the tool can emit edits that apply to files outside of //cc (i.e. edits that apply to headers from //base that got included by source files in //cc).

Note that some header files might only be included from generated files (e.g. from only from some .cpp files under out/Debug/gen). To make sure that contents of such header files are processed by the clang tool, the clang tool needs to be run against the generated files. The only way to accomplish this today is to pass --all switch to run_tool.py - this will run the clang tool against all the sources from the compilation database.

The apply_edits.py tool will only apply edits to files actually under control of git. <path 1>, <path 2>, etc are optional arguments to further filter the files that the edits are applied to. Note that semantics of these filters is distinctly different from the arguments of run_tool.py filters - one set of filters controls which files are edited, the other set of filters controls which files the clang tool is run against.

Debugging

Dumping the AST for a file:

clang++-Xclang-ast-dump-std=c++14 foo.cc | less -R

Using clang-query to dynamically test matchers (requires checking out and building clang-tools-extra):

By default, the script hides the output of the tool. The easiest way to change that is to return 1 from the main() function of the clang tool.

Testing

Synposis:

tools/clang/scripts/test_tool.py <tool name>[--apply-edits]

The name of the tool binary and the subdirectory for the tool in //tools/clang must match. The test runner finds all files that match the pattern //tools/clang/<tool name>/tests/*-original.cc, and runs the tool across those files. If --apply-edits switch is presented, tool outputs are applied to respective files and compared to the *-expected.cc version. If there is a mismatch, the result is saved in *-actual.cc. When --apply-edits switch is not presented, tool outputs are compared to *-expected.txt and if different, the result is saved in *-actual.txt. Note that in this case, only one test file is expected.