Browse Source

Merge topic 'clang-tidy-export-fixes-dir'

232467eb1c clang-tidy: add <LANG>_CLANG_TIDY_EXPORT_FIXES_DIR property

Acked-by: Kitware Robot <[email protected]>
Acked-by: buildbot <[email protected]>
Merge-request: !7982
Brad King 2 years ago
parent
commit
3b4337adc7

+ 1 - 0
Help/manual/cmake-properties.7.rst

@@ -301,6 +301,7 @@ Properties on Targets
    /prop_tgt/JOB_POOL_PRECOMPILE_HEADER
    /prop_tgt/LABELS
    /prop_tgt/LANG_CLANG_TIDY
+   /prop_tgt/LANG_CLANG_TIDY_EXPORT_FIXES_DIR
    /prop_tgt/LANG_COMPILER_LAUNCHER
    /prop_tgt/LANG_CPPCHECK
    /prop_tgt/LANG_CPPLINT

+ 1 - 0
Help/manual/cmake-variables.7.rst

@@ -450,6 +450,7 @@ Variables that Control the Build
    /variable/CMAKE_INTERPROCEDURAL_OPTIMIZATION_CONFIG
    /variable/CMAKE_IOS_INSTALL_COMBINED
    /variable/CMAKE_LANG_CLANG_TIDY
+   /variable/CMAKE_LANG_CLANG_TIDY_EXPORT_FIXES_DIR
    /variable/CMAKE_LANG_COMPILER_LAUNCHER
    /variable/CMAKE_LANG_CPPCHECK
    /variable/CMAKE_LANG_CPPLINT

+ 29 - 0
Help/prop_tgt/LANG_CLANG_TIDY_EXPORT_FIXES_DIR.rst

@@ -0,0 +1,29 @@
+<LANG>_CLANG_TIDY_EXPORT_FIXES_DIR
+----------------------------------
+
+.. versionadded:: 3.26
+
+This property is implemented only when ``<LANG>`` is ``C``, ``CXX``, ``OBJC``
+or ``OBJCXX``, and only has an effect when :prop_tgt:`<LANG>_CLANG_TIDY` is
+set.
+
+Specify a directory for the ``clang-tidy`` tool to put ``.yaml`` files
+containing its suggested changes in. This can be used for automated mass
+refactoring by ``clang-tidy``. Each object file that gets compiled will have a
+corresponding ``.yaml`` file in this directory. After the build is completed,
+you can run ``clang-apply-replacements`` on this directory to simultaneously
+apply all suggested changes to the code base. If this property is not an
+absolute directory, it is assumed to be relative to the target's binary
+directory. This property should be preferred over adding an ``--export-fixes``
+or ``--fix`` argument directly to the :prop_tgt:`<LANG>_CLANG_TIDY` property.
+
+At generate-time, in order to avoid passing stale fixes from old code to
+``clang-apply-replacements``, CMake will search the directory for any ``.yaml``
+files that won't be generated by ``clang-tidy`` during the build, and delete
+them. In addition, just before running ``clang-tidy`` on a file, CMake will
+delete that file's corresponding ``.yaml`` file in case ``clang-tidy`` doesn't
+produce any fixes.
+
+This property is initialized by the value of
+the :variable:`CMAKE_<LANG>_CLANG_TIDY_EXPORT_FIXES_DIR` variable if it is set
+when a target is created.

+ 8 - 0
Help/release/dev/clang-tidy-export-fixes-dir.rst

@@ -0,0 +1,8 @@
+clang-tidy-export-fixes-dir
+---------------------------
+
+* A new :prop_tgt:`<LANG>_CLANG_TIDY_EXPORT_FIXES_DIR` target property was
+  created to allow the ``clang-tidy`` tool to export its suggested fixes to a
+  set of ``.yaml`` files. A new
+  :variable:`CMAKE_<LANG>_CLANG_TIDY_EXPORT_FIXES_DIR` variable was created to
+  initialize this property.

+ 15 - 0
Help/variable/CMAKE_LANG_CLANG_TIDY_EXPORT_FIXES_DIR.rst

@@ -0,0 +1,15 @@
+CMAKE_<LANG>_CLANG_TIDY_EXPORT_FIXES_DIR
+----------------------------------------
+
+.. versionadded:: 3.26
+
+Default value for :prop_tgt:`<LANG>_CLANG_TIDY_EXPORT_FIXES_DIR` target
+property when ``<LANG>`` is ``C``, ``CXX``, ``OBJC`` or ``OBJCXX``.
+
+This variable is used to initialize the property on each target as it is
+created.  For example:
+
+.. code-block:: cmake
+
+  set(CMAKE_CXX_CLANG_TIDY_EXPORT_FIXES_DIR clang-tidy-fixes)
+  add_executable(foo foo.cxx)

+ 18 - 0
Source/cmGeneratorTarget.cxx

@@ -3726,6 +3726,24 @@ std::string cmGeneratorTarget::GetCreateRuleVariable(
   return "";
 }
 
+//----------------------------------------------------------------------------
+std::string cmGeneratorTarget::GetClangTidyExportFixesDirectory(
+  const std::string& lang) const
+{
+  cmValue val =
+    this->GetProperty(cmStrCat(lang, "_CLANG_TIDY_EXPORT_FIXES_DIR"));
+  if (!cmNonempty(val)) {
+    return {};
+  }
+
+  std::string path = *val;
+  if (!cmSystemTools::FileIsFullPath(path)) {
+    path =
+      cmStrCat(this->LocalGenerator->GetCurrentBinaryDirectory(), '/', path);
+  }
+  return cmSystemTools::CollapseFullPath(path);
+}
+
 namespace {
 void processIncludeDirectories(cmGeneratorTarget const* tgt,
                                EvaluatedTargetPropertyEntries& entries,

+ 2 - 0
Source/cmGeneratorTarget.h

@@ -490,6 +490,8 @@ public:
   std::string GetCreateRuleVariable(std::string const& lang,
                                     std::string const& config) const;
 
+  std::string GetClangTidyExportFixesDirectory(const std::string& lang) const;
+
 private:
   using ConfigAndLanguage = std::pair<std::string, std::string>;
   using ConfigAndLanguageToBTStrings =

+ 24 - 0
Source/cmGlobalCommonGenerator.cxx

@@ -2,11 +2,14 @@
    file Copyright.txt or https://cmake.org/licensing for details.  */
 #include "cmGlobalCommonGenerator.h"
 
+#include <algorithm>
 #include <memory>
 #include <utility>
 
 #include <cmext/algorithm>
 
+#include <cmsys/Glob.hxx>
+
 #include "cmGeneratorExpression.h"
 #include "cmGeneratorTarget.h"
 #include "cmLocalGenerator.h"
@@ -14,6 +17,7 @@
 #include "cmStateDirectory.h"
 #include "cmStateSnapshot.h"
 #include "cmStateTypes.h"
+#include "cmStringAlgorithms.h"
 #include "cmSystemTools.h"
 #include "cmValue.h"
 #include "cmake.h"
@@ -124,3 +128,23 @@ std::string cmGlobalCommonGenerator::GetEditCacheCommand() const
   cmValue edit_cmd = cm->GetCacheDefinition("CMAKE_EDIT_COMMAND");
   return edit_cmd ? *edit_cmd : std::string();
 }
+
+void cmGlobalCommonGenerator::RemoveUnknownClangTidyExportFixesFiles() const
+{
+  for (auto const& dir : this->ClangTidyExportFixesDirs) {
+    cmsys::Glob g;
+    g.SetRecurse(true);
+    g.SetListDirs(false);
+    g.FindFiles(cmStrCat(dir, "/*.yaml"));
+    for (auto const& file : g.GetFiles()) {
+      if (!this->ClangTidyExportFixesFiles.count(file) &&
+          !std::any_of(this->ClangTidyExportFixesFiles.begin(),
+                       this->ClangTidyExportFixesFiles.end(),
+                       [&file](const std::string& knownFile) -> bool {
+                         return cmSystemTools::SameFile(file, knownFile);
+                       })) {
+        cmSystemTools::RemoveFile(file);
+      }
+    }
+  }
+}

+ 13 - 0
Source/cmGlobalCommonGenerator.h

@@ -5,6 +5,7 @@
 #include "cmConfigure.h" // IWYU pragma: keep
 
 #include <map>
+#include <set>
 #include <string>
 #include <vector>
 
@@ -42,9 +43,21 @@ public:
   std::map<std::string, DirectoryTarget> ComputeDirectoryTargets() const;
   bool IsExcludedFromAllInConfig(const DirectoryTarget::Target& t,
                                  const std::string& config);
+  void AddClangTidyExportFixesDir(const std::string& dir)
+  {
+    this->ClangTidyExportFixesDirs.insert(dir);
+  }
+  void AddClangTidyExportFixesFile(const std::string& file)
+  {
+    this->ClangTidyExportFixesFiles.insert(file);
+  }
 
 protected:
   virtual bool SupportsDirectConsole() const { return true; }
   const char* GetEditCacheTargetName() const override { return "edit_cache"; }
   std::string GetEditCacheCommand() const override;
+
+  std::set<std::string> ClangTidyExportFixesDirs;
+  std::set<std::string> ClangTidyExportFixesFiles;
+  void RemoveUnknownClangTidyExportFixesFiles() const;
 };

+ 4 - 0
Source/cmGlobalNinjaGenerator.cxx

@@ -592,6 +592,8 @@ void cmGlobalNinjaGenerator::Generate()
   this->CMakeCacheFile = this->NinjaOutputPath("CMakeCache.txt");
   this->DisableCleandead = false;
   this->DiagnosedCxxModuleNinjaSupport = false;
+  this->ClangTidyExportFixesDirs.clear();
+  this->ClangTidyExportFixesFiles.clear();
 
   this->PolicyCMP0058 =
     this->LocalGenerators[0]->GetMakefile()->GetPolicyStatus(
@@ -632,6 +634,8 @@ void cmGlobalNinjaGenerator::Generate()
   {
     this->CleanMetaData();
   }
+
+  this->RemoveUnknownClangTidyExportFixesFiles();
 }
 
 void cmGlobalNinjaGenerator::CleanMetaData()

+ 5 - 0
Source/cmGlobalUnixMakefileGenerator3.cxx

@@ -102,6 +102,9 @@ void cmGlobalUnixMakefileGenerator3::Configure()
 
 void cmGlobalUnixMakefileGenerator3::Generate()
 {
+  this->ClangTidyExportFixesDirs.clear();
+  this->ClangTidyExportFixesFiles.clear();
+
   // first do superclass method
   this->cmGlobalGenerator::Generate();
 
@@ -137,6 +140,8 @@ void cmGlobalUnixMakefileGenerator3::Generate()
     *this->CommandDatabase << "\n]";
     this->CommandDatabase.reset();
   }
+
+  this->RemoveUnknownClangTidyExportFixesFiles();
 }
 
 void cmGlobalUnixMakefileGenerator3::AddCXXCompileCommand(

+ 23 - 1
Source/cmMakefileTargetGenerator.cxx

@@ -25,6 +25,7 @@
 #include "cmGeneratedFileStream.h"
 #include "cmGeneratorExpression.h"
 #include "cmGeneratorTarget.h"
+#include "cmGlobalCommonGenerator.h"
 #include "cmGlobalUnixMakefileGenerator3.h"
 #include "cmLinkLineComputer.h" // IWYU pragma: keep
 #include "cmLocalCommonGenerator.h"
@@ -1107,8 +1108,29 @@ void cmMakefileTargetGenerator::WriteObjectRuleFiles(
           } else {
             driverMode = lang == "C" ? "gcc" : "g++";
           }
+          std::string d =
+            this->GeneratorTarget->GetClangTidyExportFixesDirectory(lang);
+          std::string exportFixes;
+          if (!d.empty()) {
+            this->GlobalCommonGenerator->AddClangTidyExportFixesDir(d);
+            std::string fixesFile = cmSystemTools::CollapseFullPath(cmStrCat(
+              d, '/',
+              this->LocalGenerator->MaybeRelativeToTopBinDir(cmStrCat(
+                this->LocalGenerator->GetCurrentBinaryDirectory(), '/',
+                this->LocalGenerator->GetTargetDirectory(
+                  this->GeneratorTarget),
+                '/', objectName, ".yaml"))));
+            this->GlobalCommonGenerator->AddClangTidyExportFixesFile(
+              fixesFile);
+            cmSystemTools::MakeDirectory(
+              cmSystemTools::GetFilenamePath(fixesFile));
+            fixesFile =
+              this->LocalGenerator->MaybeRelativeToCurBinDir(fixesFile);
+            exportFixes = cmStrCat(";--export-fixes=", fixesFile);
+          }
           run_iwyu += this->LocalGenerator->EscapeForShell(
-            cmStrCat(*tidy, ";--extra-arg-before=--driver-mode=", driverMode));
+            cmStrCat(*tidy, ";--extra-arg-before=--driver-mode=", driverMode,
+                     exportFixes));
         }
         if (cmNonempty(cpplint)) {
           run_iwyu += " --cpplint=";

+ 48 - 1
Source/cmNinjaTargetGenerator.cxx

@@ -27,6 +27,7 @@
 #include "cmGeneratedFileStream.h"
 #include "cmGeneratorExpression.h"
 #include "cmGeneratorTarget.h"
+#include "cmGlobalCommonGenerator.h"
 #include "cmGlobalNinjaGenerator.h"
 #include "cmLocalGenerator.h"
 #include "cmLocalNinjaGenerator.h"
@@ -394,6 +395,24 @@ std::string cmNinjaTargetGenerator::GetObjectFilePath(
   return path;
 }
 
+std::string cmNinjaTargetGenerator::GetClangTidyReplacementsFilePath(
+  const std::string& directory, cmSourceFile const* source,
+  const std::string& config) const
+{
+  std::string path = this->LocalGenerator->GetHomeRelativeOutputPath();
+  if (!path.empty()) {
+    path += '/';
+  }
+  path = cmStrCat(directory, '/', path);
+  std::string const& objectName = this->GeneratorTarget->GetObjectName(source);
+  path =
+    cmStrCat(std::move(path),
+             this->LocalGenerator->GetTargetDirectory(this->GeneratorTarget),
+             this->GetGlobalGenerator()->ConfigDirectory(config), '/',
+             objectName, ".yaml");
+  return path;
+}
+
 std::string cmNinjaTargetGenerator::GetPreprocessedFilePath(
   cmSourceFile const* source, const std::string& config) const
 {
@@ -935,8 +954,24 @@ void cmNinjaTargetGenerator::WriteCompileRule(const std::string& lang,
         } else {
           driverMode = lang == "C" ? "gcc" : "g++";
         }
+        const bool haveClangTidyExportFixesDir =
+          !this->GeneratorTarget->GetClangTidyExportFixesDirectory(lang)
+             .empty();
+        std::string exportFixes;
+        if (haveClangTidyExportFixesDir) {
+          exportFixes = ";--export-fixes=$CLANG_TIDY_EXPORT_FIXES";
+        }
         run_iwyu += this->GetLocalGenerator()->EscapeForShell(
-          cmStrCat(*tidy, ";--extra-arg-before=--driver-mode=", driverMode));
+          cmStrCat(*tidy, ";--extra-arg-before=--driver-mode=", driverMode,
+                   exportFixes));
+        if (haveClangTidyExportFixesDir) {
+          std::string search = cmStrCat(
+            this->GetLocalGenerator()->GetState()->UseWindowsShell() ? ""
+                                                                     : "\\",
+            "$$CLANG_TIDY_EXPORT_FIXES");
+          auto loc = run_iwyu.rfind(search);
+          run_iwyu.replace(loc, search.length(), "$CLANG_TIDY_EXPORT_FIXES");
+        }
       }
       if (cmNonempty(cpplint)) {
         run_iwyu += cmStrCat(
@@ -1317,6 +1352,18 @@ void cmNinjaTargetGenerator::WriteObjectBuildStatement(
     }
   }
 
+  std::string d =
+    this->GeneratorTarget->GetClangTidyExportFixesDirectory(language);
+  if (!d.empty()) {
+    this->GlobalCommonGenerator->AddClangTidyExportFixesDir(d);
+    std::string fixesFile =
+      this->GetClangTidyReplacementsFilePath(d, source, config);
+    this->GlobalCommonGenerator->AddClangTidyExportFixesFile(fixesFile);
+    cmSystemTools::MakeDirectory(cmSystemTools::GetFilenamePath(fixesFile));
+    fixesFile = this->ConvertToNinjaPath(fixesFile);
+    vars["CLANG_TIDY_EXPORT_FIXES"] = fixesFile;
+  }
+
   if (firstForConfig) {
     this->ExportObjectCompileCommand(
       language, sourceFilePath, objectDir, objectFileName, objectFileDir,

+ 5 - 0
Source/cmNinjaTargetGenerator.h

@@ -134,6 +134,11 @@ protected:
   std::string GetPreprocessedFilePath(cmSourceFile const* source,
                                       const std::string& config) const;
 
+  /// @return the clang-tidy replacements file path for the given @a source.
+  std::string GetClangTidyReplacementsFilePath(
+    const std::string& directory, cmSourceFile const* source,
+    const std::string& config) const;
+
   /// @return the dyndep file path for this target.
   std::string GetDyndepFilePath(std::string const& lang,
                                 const std::string& config) const;

+ 4 - 0
Source/cmTarget.cxx

@@ -573,12 +573,14 @@ cmTarget::cmTarget(std::string const& name, cmStateEnums::TargetType type,
     initProp("NO_SYSTEM_FROM_IMPORTED");
     initProp("BUILD_WITH_INSTALL_NAME_DIR");
     initProp("C_CLANG_TIDY");
+    initProp("C_CLANG_TIDY_EXPORT_FIXES_DIR");
     initProp("C_CPPLINT");
     initProp("C_CPPCHECK");
     initProp("C_INCLUDE_WHAT_YOU_USE");
     initProp("C_LINKER_LAUNCHER");
     initProp("LINK_WHAT_YOU_USE");
     initProp("CXX_CLANG_TIDY");
+    initProp("CXX_CLANG_TIDY_EXPORT_FIXES_DIR");
     initProp("CXX_CPPLINT");
     initProp("CXX_CPPCHECK");
     initProp("CXX_INCLUDE_WHAT_YOU_USE");
@@ -600,8 +602,10 @@ cmTarget::cmTarget(std::string const& name, cmStateEnums::TargetType type,
     initProp("LINK_SEARCH_START_STATIC");
     initProp("LINK_SEARCH_END_STATIC");
     initProp("OBJC_CLANG_TIDY");
+    initProp("OBJC_CLANG_TIDY_EXPORT_FIXES_DIR");
     initProp("OBJC_LINKER_LAUNCHER");
     initProp("OBJCXX_CLANG_TIDY");
+    initProp("OBJCXX_CLANG_TIDY_EXPORT_FIXES_DIR");
     initProp("OBJCXX_LINKER_LAUNCHER");
     initProp("Swift_LANGUAGE_VERSION");
     initProp("Swift_MODULE_DIRECTORY");

+ 6 - 0
Source/cmcmd.cxx

@@ -367,6 +367,12 @@ int HandleTidy(const std::string& runCmd, const std::string& sourceFile,
   std::vector<std::string> tidy_cmd = cmExpandedList(runCmd, true);
   tidy_cmd.push_back(sourceFile);
 
+  for (auto const& arg : tidy_cmd) {
+    if (cmHasLiteralPrefix(arg, "--export-fixes=")) {
+      cmSystemTools::RemoveFile(arg.substr(cmStrLen("--export-fixes=")));
+    }
+  }
+
   // clang-tidy supports working out the compile commands from a
   // compile_commands.json file in a directory given by a "-p" option, or by
   // passing the compiler command line arguments after --. When the latter

+ 35 - 0
Tests/RunCMake/ClangTidy/ExportFixesDir-Build-check.cmake

@@ -0,0 +1,35 @@
+if(RunCMake_GENERATOR_IS_MULTI_CONFIG)
+  assert_any_file_exists(
+    "${RunCMake_TEST_BINARY_DIR}/clang-tidy/CMakeFiles/main.dir/Debug/main.c.o.yaml"
+    "${RunCMake_TEST_BINARY_DIR}/clang-tidy/CMakeFiles/main.dir/Debug/main.c.obj.yaml"
+    )
+  assert_any_file_exists(
+    "${RunCMake_TEST_BINARY_DIR}/clang-tidy/CMakeFiles/main.dir/Debug/extra.c.o.yaml"
+    "${RunCMake_TEST_BINARY_DIR}/clang-tidy/CMakeFiles/main.dir/Debug/extra.c.obj.yaml"
+    )
+  assert_any_file_exists(
+    "${RunCMake_TEST_BINARY_DIR}/export_fixes_subdir/clang-tidy/export_fixes_subdir/CMakeFiles/subdir.dir/Debug/__/main.c.o.yaml"
+    "${RunCMake_TEST_BINARY_DIR}/export_fixes_subdir/clang-tidy/export_fixes_subdir/CMakeFiles/subdir.dir/Debug/__/main.c.obj.yaml"
+    )
+  assert_any_file_exists(
+    "${RunCMake_TEST_BINARY_DIR}/export_fixes_subdir/clang-tidy/export_fixes_subdir/CMakeFiles/subdir.dir/Debug/__/extra.c.o.yaml"
+    "${RunCMake_TEST_BINARY_DIR}/export_fixes_subdir/clang-tidy/export_fixes_subdir/CMakeFiles/subdir.dir/Debug/__/extra.c.obj.yaml"
+    )
+else()
+  assert_any_file_exists(
+    "${RunCMake_TEST_BINARY_DIR}/clang-tidy/CMakeFiles/main.dir/main.c.o.yaml"
+    "${RunCMake_TEST_BINARY_DIR}/clang-tidy/CMakeFiles/main.dir/main.c.obj.yaml"
+    )
+  assert_any_file_exists(
+    "${RunCMake_TEST_BINARY_DIR}/clang-tidy/CMakeFiles/main.dir/extra.c.o.yaml"
+    "${RunCMake_TEST_BINARY_DIR}/clang-tidy/CMakeFiles/main.dir/extra.c.obj.yaml"
+    )
+  assert_any_file_exists(
+    "${RunCMake_TEST_BINARY_DIR}/export_fixes_subdir/clang-tidy/export_fixes_subdir/CMakeFiles/subdir.dir/__/main.c.o.yaml"
+    "${RunCMake_TEST_BINARY_DIR}/export_fixes_subdir/clang-tidy/export_fixes_subdir/CMakeFiles/subdir.dir/__/main.c.obj.yaml"
+    )
+  assert_any_file_exists(
+    "${RunCMake_TEST_BINARY_DIR}/export_fixes_subdir/clang-tidy/export_fixes_subdir/CMakeFiles/subdir.dir/__/extra.c.o.yaml"
+    "${RunCMake_TEST_BINARY_DIR}/export_fixes_subdir/clang-tidy/export_fixes_subdir/CMakeFiles/subdir.dir/__/extra.c.obj.yaml"
+    )
+endif()

+ 6 - 0
Tests/RunCMake/ClangTidy/ExportFixesDir.cmake

@@ -0,0 +1,6 @@
+enable_language(C)
+set(CMAKE_C_CLANG_TIDY "${PSEUDO_TIDY}" -some -args)
+set(CMAKE_C_CLANG_TIDY_EXPORT_FIXES_DIR clang-tidy)
+set(files ${CMAKE_CURRENT_SOURCE_DIR}/main.c ${CMAKE_CURRENT_SOURCE_DIR}/extra.c)
+add_executable(main ${files})
+add_subdirectory(export_fixes_subdir)

+ 35 - 0
Tests/RunCMake/ClangTidy/ExportFixesDir2-Build-check.cmake

@@ -0,0 +1,35 @@
+if(RunCMake_GENERATOR_IS_MULTI_CONFIG)
+  assert_any_file_exists(
+    "${RunCMake_TEST_BINARY_DIR}/clang-tidy/CMakeFiles/main.dir/Debug/main.c.o.yaml"
+    "${RunCMake_TEST_BINARY_DIR}/clang-tidy/CMakeFiles/main.dir/Debug/main.c.obj.yaml"
+    )
+  assert_no_file_exists(
+    "${RunCMake_TEST_BINARY_DIR}/clang-tidy/CMakeFiles/main.dir/Debug/extra.c.o.yaml"
+    "${RunCMake_TEST_BINARY_DIR}/clang-tidy/CMakeFiles/main.dir/Debug/extra.c.obj.yaml"
+    )
+  assert_any_file_exists(
+    "${RunCMake_TEST_BINARY_DIR}/export_fixes_subdir/clang-tidy/export_fixes_subdir/CMakeFiles/subdir.dir/Debug/__/main.c.o.yaml"
+    "${RunCMake_TEST_BINARY_DIR}/export_fixes_subdir/clang-tidy/export_fixes_subdir/CMakeFiles/subdir.dir/Debug/__/main.c.obj.yaml"
+    )
+  assert_no_file_exists(
+    "${RunCMake_TEST_BINARY_DIR}/export_fixes_subdir/clang-tidy/export_fixes_subdir/CMakeFiles/subdir.dir/Debug/__/extra.c.o.yaml"
+    "${RunCMake_TEST_BINARY_DIR}/export_fixes_subdir/clang-tidy/export_fixes_subdir/CMakeFiles/subdir.dir/Debug/__/extra.c.obj.yaml"
+    )
+else()
+  assert_any_file_exists(
+    "${RunCMake_TEST_BINARY_DIR}/clang-tidy/CMakeFiles/main.dir/main.c.o.yaml"
+    "${RunCMake_TEST_BINARY_DIR}/clang-tidy/CMakeFiles/main.dir/main.c.obj.yaml"
+    )
+  assert_no_file_exists(
+    "${RunCMake_TEST_BINARY_DIR}/clang-tidy/CMakeFiles/main.dir/extra.c.o.yaml"
+    "${RunCMake_TEST_BINARY_DIR}/clang-tidy/CMakeFiles/main.dir/extra.c.obj.yaml"
+    )
+  assert_any_file_exists(
+    "${RunCMake_TEST_BINARY_DIR}/export_fixes_subdir/clang-tidy/export_fixes_subdir/CMakeFiles/subdir.dir/__/main.c.o.yaml"
+    "${RunCMake_TEST_BINARY_DIR}/export_fixes_subdir/clang-tidy/export_fixes_subdir/CMakeFiles/subdir.dir/__/main.c.obj.yaml"
+    )
+  assert_no_file_exists(
+    "${RunCMake_TEST_BINARY_DIR}/export_fixes_subdir/clang-tidy/export_fixes_subdir/CMakeFiles/subdir.dir/__/extra.c.o.yaml"
+    "${RunCMake_TEST_BINARY_DIR}/export_fixes_subdir/clang-tidy/export_fixes_subdir/CMakeFiles/subdir.dir/__/extra.c.obj.yaml"
+    )
+endif()

+ 35 - 0
Tests/RunCMake/ClangTidy/ExportFixesDir2-check.cmake

@@ -0,0 +1,35 @@
+if(RunCMake_GENERATOR_IS_MULTI_CONFIG)
+  assert_any_file_exists(
+    "${RunCMake_TEST_BINARY_DIR}/clang-tidy/CMakeFiles/main.dir/Debug/main.c.o.yaml"
+    "${RunCMake_TEST_BINARY_DIR}/clang-tidy/CMakeFiles/main.dir/Debug/main.c.obj.yaml"
+    )
+  assert_no_file_exists(
+    "${RunCMake_TEST_BINARY_DIR}/clang-tidy/CMakeFiles/main.dir/Debug/extra.c.o.yaml"
+    "${RunCMake_TEST_BINARY_DIR}/clang-tidy/CMakeFiles/main.dir/Debug/extra.c.obj.yaml"
+    )
+  assert_any_file_exists(
+    "${RunCMake_TEST_BINARY_DIR}/export_fixes_subdir/clang-tidy/export_fixes_subdir/CMakeFiles/subdir.dir/Debug/__/main.c.o.yaml"
+    "${RunCMake_TEST_BINARY_DIR}/export_fixes_subdir/clang-tidy/export_fixes_subdir/CMakeFiles/subdir.dir/Debug/__/main.c.obj.yaml"
+    )
+  assert_no_file_exists(
+    "${RunCMake_TEST_BINARY_DIR}/export_fixes_subdir/clang-tidy/export_fixes_subdir/CMakeFiles/subdir.dir/Debug/__/extra.c.o.yaml"
+    "${RunCMake_TEST_BINARY_DIR}/export_fixes_subdir/clang-tidy/export_fixes_subdir/CMakeFiles/subdir.dir/Debug/__/extra.c.obj.yaml"
+    )
+else()
+  assert_any_file_exists(
+    "${RunCMake_TEST_BINARY_DIR}/clang-tidy/CMakeFiles/main.dir/main.c.o.yaml"
+    "${RunCMake_TEST_BINARY_DIR}/clang-tidy/CMakeFiles/main.dir/main.c.obj.yaml"
+    )
+  assert_no_file_exists(
+    "${RunCMake_TEST_BINARY_DIR}/clang-tidy/CMakeFiles/main.dir/extra.c.o.yaml"
+    "${RunCMake_TEST_BINARY_DIR}/clang-tidy/CMakeFiles/main.dir/extra.c.obj.yaml"
+    )
+  assert_any_file_exists(
+    "${RunCMake_TEST_BINARY_DIR}/export_fixes_subdir/clang-tidy/export_fixes_subdir/CMakeFiles/subdir.dir/__/main.c.o.yaml"
+    "${RunCMake_TEST_BINARY_DIR}/export_fixes_subdir/clang-tidy/export_fixes_subdir/CMakeFiles/subdir.dir/__/main.c.obj.yaml"
+    )
+  assert_no_file_exists(
+    "${RunCMake_TEST_BINARY_DIR}/export_fixes_subdir/clang-tidy/export_fixes_subdir/CMakeFiles/subdir.dir/__/extra.c.o.yaml"
+    "${RunCMake_TEST_BINARY_DIR}/export_fixes_subdir/clang-tidy/export_fixes_subdir/CMakeFiles/subdir.dir/__/extra.c.obj.yaml"
+    )
+endif()

+ 6 - 0
Tests/RunCMake/ClangTidy/ExportFixesDir2.cmake

@@ -0,0 +1,6 @@
+enable_language(C)
+set(CMAKE_C_CLANG_TIDY "${PSEUDO_TIDY}" -some -args)
+set(CMAKE_C_CLANG_TIDY_EXPORT_FIXES_DIR clang-tidy)
+set(files ${CMAKE_CURRENT_SOURCE_DIR}/main.c)
+add_executable(main ${files})
+add_subdirectory(export_fixes_subdir)

+ 52 - 0
Tests/RunCMake/ClangTidy/RunCMakeTest.cmake

@@ -30,3 +30,55 @@ if (NOT RunCMake_GENERATOR STREQUAL "Watcom WMake")
 endif()
 run_tidy(C-bad)
 run_tidy(compdb)
+
+function(any_file_exists varname)
+  foreach(filename IN LISTS ARGN)
+    if(EXISTS "${filename}")
+      set("${varname}" 1 PARENT_SCOPE)
+      return()
+    endif()
+  endforeach()
+  set("${varname}" 0 PARENT_SCOPE)
+endfunction()
+
+function(assert_any_file_exists)
+  any_file_exists(exists ${ARGN})
+  if(NOT exists)
+    string(APPEND RunCMake_TEST_FAILED "Expected one of the following files to exist but they do not:\n")
+    foreach(filename IN LISTS ARGN)
+      string(APPEND RunCMake_TEST_FAILED "  ${filename}\n")
+    endforeach()
+    set(RunCMake_TEST_FAILED "${RunCMake_TEST_FAILED}" PARENT_SCOPE)
+  endif()
+endfunction()
+
+function(assert_no_file_exists)
+  any_file_exists(exists ${ARGN})
+  if(exists)
+    string(APPEND RunCMake_TEST_FAILED "Expected none of the following files to exist but one of them does:\n")
+    foreach(filename IN LISTS ARGN)
+      string(APPEND RunCMake_TEST_FAILED "  ${filename}\n")
+    endforeach()
+    set(RunCMake_TEST_FAILED "${RunCMake_TEST_FAILED}" PARENT_SCOPE)
+  endif()
+endfunction()
+
+function(run_tidy_export_fixes)
+  # Use a single build tree for tests without cleaning.
+  set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/ExportFixesDir-build)
+  set(RunCMake_TEST_NO_CLEAN 1)
+  file(REMOVE_RECURSE "${RunCMake_TEST_BINARY_DIR}")
+  file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}")
+  run_cmake(ExportFixesDir)
+
+  set(RunCMake_TEST_OUTPUT_MERGE 1)
+  run_cmake_command(ExportFixesDir-Build ${CMAKE_COMMAND} --build . --config Debug)
+  unset(RunCMake_TEST_OUTPUT_MERGE)
+
+  run_cmake(ExportFixesDir2)
+
+  set(RunCMake_TEST_OUTPUT_MERGE 1)
+  run_cmake_command(ExportFixesDir2-Build ${CMAKE_COMMAND} --build . --config Debug)
+  unset(RunCMake_TEST_OUTPUT_MERGE)
+endfunction()
+run_tidy_export_fixes()

+ 1 - 0
Tests/RunCMake/ClangTidy/export_fixes_subdir/CMakeLists.txt

@@ -0,0 +1 @@
+add_executable(subdir ${files})

+ 3 - 0
Tests/RunCMake/ClangTidy/extra.c

@@ -0,0 +1,3 @@
+void extra(void)
+{
+}

+ 3 - 0
Tests/RunCMake/CommandLine/E___run_co_compile-tidy-remove-fixes-check.cmake

@@ -0,0 +1,3 @@
+if(EXISTS "${RunCMake_BINARY_DIR}/tidy-fixes.yaml")
+  string(APPEND RunCMake_TEST_FAILED "Expected ${RunCMake_BINARY_DIR}/tidy-fixes.yaml not to exist but it does")
+endif()

+ 1 - 0
Tests/RunCMake/CommandLine/E___run_co_compile-tidy-remove-fixes-prep.cmake

@@ -0,0 +1 @@
+file(TOUCH "${RunCMake_BINARY_DIR}/tidy-fixes.yaml")

+ 1 - 0
Tests/RunCMake/CommandLine/RunCMakeTest.cmake

@@ -47,6 +47,7 @@ run_cmake_command(E___run_co_compile-no-iwyu ${CMAKE_COMMAND} -E __run_co_compil
 run_cmake_command(E___run_co_compile-bad-iwyu ${CMAKE_COMMAND} -E __run_co_compile --iwyu=iwyu-does-not-exist -- command-does-not-exist)
 run_cmake_command(E___run_co_compile-no--- ${CMAKE_COMMAND} -E __run_co_compile --iwyu=iwyu-does-not-exist command-does-not-exist)
 run_cmake_command(E___run_co_compile-no-cc ${CMAKE_COMMAND} -E __run_co_compile --iwyu=iwyu-does-not-exist --)
+run_cmake_command(E___run_co_compile-tidy-remove-fixes ${CMAKE_COMMAND} -E __run_co_compile "--tidy=${CMAKE_COMMAND}\\;-E\\;true\\;--export-fixes=${RunCMake_BINARY_DIR}/tidy-fixes.yaml" -- ${CMAKE_COMMAND} -E true)
 
 run_cmake_command(G_no-arg ${CMAKE_COMMAND} -B DummyBuildDir -G)
 run_cmake_command(G_bad-arg ${CMAKE_COMMAND} -B DummyBuildDir -G NoSuchGenerator)

+ 13 - 0
Tests/RunCMake/pseudo_tidy.c

@@ -1,8 +1,13 @@
+#ifndef _CRT_SECURE_NO_WARNINGS
+#  define _CRT_SECURE_NO_WARNINGS
+#endif
+
 #include <stdio.h>
 #include <string.h>
 
 int main(int argc, char* argv[])
 {
+  FILE* f;
   int i;
   for (i = 1; i < argc; ++i) {
     if (strcmp(argv[i], "-p") == 0) {
@@ -20,6 +25,14 @@ int main(int argc, char* argv[])
       fprintf(stderr, "stderr from bad command line arg '-bad'\n");
       return 1;
     }
+    if (strncmp(argv[i], "--export-fixes=", 15) == 0) {
+      f = fopen(argv[i] + 15, "w");
+      if (!f) {
+        fprintf(stderr, "Error opening %s for writing\n", argv[i] + 15);
+        return 1;
+      }
+      fclose(f);
+    }
     if (argv[i][0] != '-') {
       fprintf(stdout, "%s:0:0: warning: message [checker]\n", argv[i]);
       break;