diff --git a/BUILD b/BUILD index eaec502..f6f578a 100644 --- a/BUILD +++ b/BUILD @@ -1,3 +1,5 @@ +load("//:defs.bzl", "clang_tidy_apply_fixes") + filegroup( name = "clang_tidy_config_default", srcs = [ @@ -23,6 +25,17 @@ label_flag( visibility = ["//visibility:public"], ) +filegroup( + name = "clang_apply_replacements_executable_default", + srcs = [], # empty list: system clang-apply-replacements +) + +label_flag( + name = "clang_apply_replacements_executable", + build_setting_default = ":clang_apply_replacements_executable_default", + visibility = ["//visibility:public"], +) + filegroup( name = "clang_tidy_additional_deps_default", srcs = [], @@ -33,3 +46,7 @@ label_flag( build_setting_default = ":clang_tidy_additional_deps_default", visibility = ["//visibility:public"], ) + +clang_tidy_apply_fixes( + name = "apply_fixes", +) diff --git a/README.md b/README.md index 7633a86..c211599 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,20 @@ If you have a hermetic build, you can use your own clang-tidy target like this: build:clang-tidy --@bazel_clang_tidy//:clang_tidy_executable=@local_config_cc//:clangtidy_bin ``` +### applying tidy fixes + +clang-tidy fixes can be applied with + +```sh +bazel run @bazel_clang_tidy//:apply_fixes +``` + +As with running clang-tidy, the binary target and config file can be specified with +`--@bazel_clang_tidy//:clang_tidy_executable` and +`--@bazel_clang_tidy//:clang_tidy_config`. Similarly, clang-apply-replacements +can be specified with +`--@bazel_clang_tidy//:clang_apply_replacements_executable`. + ## Features - Run clang-tidy on any C++ target diff --git a/WORKSPACE b/WORKSPACE index e69de29..adfd402 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -0,0 +1 @@ +workspace(name = "bazel_clang_tidy") diff --git a/clang_tidy/BUILD b/clang_tidy/BUILD index d1c4987..69dcf7f 100644 --- a/clang_tidy/BUILD +++ b/clang_tidy/BUILD @@ -4,3 +4,9 @@ sh_binary( data = ["//:clang_tidy_config"], visibility = ["//visibility:public"], ) + +filegroup( + name = "apply_fixes_template", + srcs = ["apply_fixes.template.sh"], + visibility = ["//visibility:public"], +) diff --git a/clang_tidy/apply_fixes.template.sh b/clang_tidy/apply_fixes.template.sh new file mode 100644 index 0000000..5cf22f1 --- /dev/null +++ b/clang_tidy/apply_fixes.template.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +set -euo pipefail + +bazel=$(readlink -f /proc/${PPID}/exe) + +args=$(printf " union %s" "${@}" | sed 's/^ union \(.*\)/\1/') +targets="${args:-//...}" + +bazel_tidy_config=(\ +"--aspects=@@WORKSPACE@//clang_tidy:clang_tidy.bzl%clang_tidy_aspect" \ +"--@@WORKSPACE@//:clang_tidy_executable=@TIDY_BINARY@" \ +"--@@WORKSPACE@//:clang_tidy_config=@TIDY_CONFIG@" \ +"--output_groups=report") + +cd $BUILD_WORKSPACE_DIRECTORY + +exported_fixes=$("$bazel" aquery \ + "mnemonic(\"ClangTidy\", kind(\"cc_.* rule\", $targets))" \ + --noshow_progress \ + --ui_event_filters=-info \ + "${bazel_tidy_config[@]}" \ + | grep 'Outputs:' \ + | sed 's:^\s\+Outputs\: \[\(.*\)\]$:\1:') + +"$bazel" build \ + --noshow_progress \ + --ui_event_filters=-info,-error,-stdout,-stderr \ + --keep_going \ + "${bazel_tidy_config[@]}" \ + "${@:-//...}" || true + +for file in $exported_fixes; do + # get the build directory which is probably some sandbox + build_dir=$(grep --max-count=1 'BuildDirectory:' "$file" \ + | sed "s:\s\+BuildDirectory\:\s\+'\(.*\)':\1:" || true) + + # if we didn't find BuildDirectory, it's probably an empty file + if [ -z "$build_dir" ]; then + continue + fi + + # relative path of a fix file copied to BUILD_WORKSPACE_DIRECTORY + suggested_fixes=$(basename "$file") + + # strip the build_dir prefix + # and set BuildDirectory to empty + # so clang-apply-replacements won't look for it + sed "s:$build_dir/::" "$file" \ + | sed "s:$build_dir::" \ + > "$suggested_fixes" + + # resolve symlinks and relative paths + while path=$(grep --max-count=1 '_virtual_includes\|\./' "$suggested_fixes" \ + | sed "s:\s\+FilePath\:\s\+'\(.*\)':\1:" || true); do + if [ -z "$path" ]; then + break + fi + + sed -i "s:$path:$(readlink -f $path):" "$suggested_fixes" + done + + # remove the original exported fixes, otherwise they are found by + # clang-apply-replacements + rm -f "$file" +done + +@APPLY_REPLACEMENTS_BINARY@ -remove-change-desc-files . diff --git a/defs.bzl b/defs.bzl new file mode 100644 index 0000000..ab43bdd --- /dev/null +++ b/defs.bzl @@ -0,0 +1,57 @@ +""" +clang-tidy fix rule +""" + +def _clang_tidy_apply_fixes_impl(ctx): + apply_fixes = ctx.actions.declare_file( + "clang_tidy.{}.sh".format(ctx.attr.name), + ) + + config = ctx.attr._tidy_config.files.to_list() + if len(config) != 1: + fail(":config ({}) must contain a single file".format(config)) + + apply_bin = ctx.attr._apply_replacements_binary.files_to_run.executable + apply_path = apply_bin.path if apply_bin else "clang-apply-replacements" + + ctx.actions.expand_template( + template = ctx.attr._template.files.to_list()[0], + output = apply_fixes, + substitutions = { + "@APPLY_REPLACEMENTS_BINARY@": apply_path, + "@TIDY_BINARY@": str(ctx.attr._tidy_binary.label), + "@TIDY_CONFIG@": str(ctx.attr._tidy_config.label), + "@WORKSPACE@": ctx.label.workspace_name, + }, + ) + + tidy_bin = ctx.attr._tidy_binary.files_to_run.executable + runfiles = ctx.runfiles( + ( + [apply_bin] if apply_bin else [] + + [tidy_bin] if tidy_bin else [] + + config + ), + ) + + return [ + DefaultInfo( + executable = apply_fixes, + runfiles = runfiles, + ), + ] + +clang_tidy_apply_fixes = rule( + implementation = _clang_tidy_apply_fixes_impl, + fragments = ["cpp"], + attrs = { + "_template": attr.label(default = Label("//clang_tidy:apply_fixes_template")), + "_tidy_config": attr.label(default = Label("//:clang_tidy_config")), + "_tidy_binary": attr.label(default = Label("//:clang_tidy_executable")), + "_apply_replacements_binary": attr.label( + default = Label("//:clang_apply_replacements_executable"), + ), + }, + toolchains = ["@bazel_tools//tools/cpp:toolchain_type"], + executable = True, +)