From 232467eb1c0dab9156cd8c4af56aad3959cbee4b Mon Sep 17 00:00:00 2001
From: Kyle Edwards <kyle.edwards@kitware.com>
Date: Tue, 29 Nov 2022 13:39:10 -0500
Subject: clang-tidy: add <LANG>_CLANG_TIDY_EXPORT_FIXES_DIR property

Fixes: #21362
---
 Help/manual/cmake-properties.7.rst                 |  1 +
 Help/manual/cmake-variables.7.rst                  |  1 +
 Help/prop_tgt/LANG_CLANG_TIDY_EXPORT_FIXES_DIR.rst | 29 ++++++++++++
 Help/release/dev/clang-tidy-export-fixes-dir.rst   |  8 ++++
 .../CMAKE_LANG_CLANG_TIDY_EXPORT_FIXES_DIR.rst     | 15 +++++++
 Source/cmGeneratorTarget.cxx                       | 18 ++++++++
 Source/cmGeneratorTarget.h                         |  2 +
 Source/cmGlobalCommonGenerator.cxx                 | 24 ++++++++++
 Source/cmGlobalCommonGenerator.h                   | 13 ++++++
 Source/cmGlobalNinjaGenerator.cxx                  |  4 ++
 Source/cmGlobalUnixMakefileGenerator3.cxx          |  5 +++
 Source/cmMakefileTargetGenerator.cxx               | 24 +++++++++-
 Source/cmNinjaTargetGenerator.cxx                  | 49 +++++++++++++++++++-
 Source/cmNinjaTargetGenerator.h                    |  5 +++
 Source/cmTarget.cxx                                |  4 ++
 Source/cmcmd.cxx                                   |  6 +++
 .../ClangTidy/ExportFixesDir-Build-check.cmake     | 35 +++++++++++++++
 Tests/RunCMake/ClangTidy/ExportFixesDir.cmake      |  6 +++
 .../ClangTidy/ExportFixesDir2-Build-check.cmake    | 35 +++++++++++++++
 .../RunCMake/ClangTidy/ExportFixesDir2-check.cmake | 35 +++++++++++++++
 Tests/RunCMake/ClangTidy/ExportFixesDir2.cmake     |  6 +++
 Tests/RunCMake/ClangTidy/RunCMakeTest.cmake        | 52 ++++++++++++++++++++++
 .../ClangTidy/export_fixes_subdir/CMakeLists.txt   |  1 +
 Tests/RunCMake/ClangTidy/extra.c                   |  3 ++
 ...___run_co_compile-tidy-remove-fixes-check.cmake |  3 ++
 ...E___run_co_compile-tidy-remove-fixes-prep.cmake |  1 +
 Tests/RunCMake/CommandLine/RunCMakeTest.cmake      |  1 +
 Tests/RunCMake/pseudo_tidy.c                       | 13 ++++++
 28 files changed, 397 insertions(+), 2 deletions(-)
 create mode 100644 Help/prop_tgt/LANG_CLANG_TIDY_EXPORT_FIXES_DIR.rst
 create mode 100644 Help/release/dev/clang-tidy-export-fixes-dir.rst
 create mode 100644 Help/variable/CMAKE_LANG_CLANG_TIDY_EXPORT_FIXES_DIR.rst
 create mode 100644 Tests/RunCMake/ClangTidy/ExportFixesDir-Build-check.cmake
 create mode 100644 Tests/RunCMake/ClangTidy/ExportFixesDir.cmake
 create mode 100644 Tests/RunCMake/ClangTidy/ExportFixesDir2-Build-check.cmake
 create mode 100644 Tests/RunCMake/ClangTidy/ExportFixesDir2-check.cmake
 create mode 100644 Tests/RunCMake/ClangTidy/ExportFixesDir2.cmake
 create mode 100644 Tests/RunCMake/ClangTidy/export_fixes_subdir/CMakeLists.txt
 create mode 100644 Tests/RunCMake/ClangTidy/extra.c
 create mode 100644 Tests/RunCMake/CommandLine/E___run_co_compile-tidy-remove-fixes-check.cmake
 create mode 100644 Tests/RunCMake/CommandLine/E___run_co_compile-tidy-remove-fixes-prep.cmake

diff --git a/Help/manual/cmake-properties.7.rst b/Help/manual/cmake-properties.7.rst
index e9ee681..93c6d3e 100644
--- a/Help/manual/cmake-properties.7.rst
+++ b/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
diff --git a/Help/manual/cmake-variables.7.rst b/Help/manual/cmake-variables.7.rst
index d66bb2b..2a1e017 100644
--- a/Help/manual/cmake-variables.7.rst
+++ b/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
diff --git a/Help/prop_tgt/LANG_CLANG_TIDY_EXPORT_FIXES_DIR.rst b/Help/prop_tgt/LANG_CLANG_TIDY_EXPORT_FIXES_DIR.rst
new file mode 100644
index 0000000..265fade
--- /dev/null
+++ b/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.
diff --git a/Help/release/dev/clang-tidy-export-fixes-dir.rst b/Help/release/dev/clang-tidy-export-fixes-dir.rst
new file mode 100644
index 0000000..edb7ed5
--- /dev/null
+++ b/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.
diff --git a/Help/variable/CMAKE_LANG_CLANG_TIDY_EXPORT_FIXES_DIR.rst b/Help/variable/CMAKE_LANG_CLANG_TIDY_EXPORT_FIXES_DIR.rst
new file mode 100644
index 0000000..60b7f40
--- /dev/null
+++ b/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)
diff --git a/Source/cmGeneratorTarget.cxx b/Source/cmGeneratorTarget.cxx
index c80cdb9..123b5e6 100644
--- a/Source/cmGeneratorTarget.cxx
+++ b/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,
diff --git a/Source/cmGeneratorTarget.h b/Source/cmGeneratorTarget.h
index 3cd5e34..7fa662d 100644
--- a/Source/cmGeneratorTarget.h
+++ b/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 =
diff --git a/Source/cmGlobalCommonGenerator.cxx b/Source/cmGlobalCommonGenerator.cxx
index 3ae66f0..7a44452 100644
--- a/Source/cmGlobalCommonGenerator.cxx
+++ b/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);
+      }
+    }
+  }
+}
diff --git a/Source/cmGlobalCommonGenerator.h b/Source/cmGlobalCommonGenerator.h
index fed9ce8..fa42674 100644
--- a/Source/cmGlobalCommonGenerator.h
+++ b/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;
 };
diff --git a/Source/cmGlobalNinjaGenerator.cxx b/Source/cmGlobalNinjaGenerator.cxx
index 4500f33..75c347e 100644
--- a/Source/cmGlobalNinjaGenerator.cxx
+++ b/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()
diff --git a/Source/cmGlobalUnixMakefileGenerator3.cxx b/Source/cmGlobalUnixMakefileGenerator3.cxx
index 70a9d3e..30206b5 100644
--- a/Source/cmGlobalUnixMakefileGenerator3.cxx
+++ b/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(
diff --git a/Source/cmMakefileTargetGenerator.cxx b/Source/cmMakefileTargetGenerator.cxx
index 766e9f9..20cc2c3 100644
--- a/Source/cmMakefileTargetGenerator.cxx
+++ b/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=";
diff --git a/Source/cmNinjaTargetGenerator.cxx b/Source/cmNinjaTargetGenerator.cxx
index 85a6fc2..f2f719d 100644
--- a/Source/cmNinjaTargetGenerator.cxx
+++ b/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
 {
@@ -932,8 +951,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(
@@ -1314,6 +1349,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,
diff --git a/Source/cmNinjaTargetGenerator.h b/Source/cmNinjaTargetGenerator.h
index b5abb36..8bf7986 100644
--- a/Source/cmNinjaTargetGenerator.h
+++ b/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;
diff --git a/Source/cmTarget.cxx b/Source/cmTarget.cxx
index 70a624d..d73e7cf 100644
--- a/Source/cmTarget.cxx
+++ b/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");
diff --git a/Source/cmcmd.cxx b/Source/cmcmd.cxx
index 06bceb4..4303f96 100644
--- a/Source/cmcmd.cxx
+++ b/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
diff --git a/Tests/RunCMake/ClangTidy/ExportFixesDir-Build-check.cmake b/Tests/RunCMake/ClangTidy/ExportFixesDir-Build-check.cmake
new file mode 100644
index 0000000..97ec52b
--- /dev/null
+++ b/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()
diff --git a/Tests/RunCMake/ClangTidy/ExportFixesDir.cmake b/Tests/RunCMake/ClangTidy/ExportFixesDir.cmake
new file mode 100644
index 0000000..2b278da
--- /dev/null
+++ b/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)
diff --git a/Tests/RunCMake/ClangTidy/ExportFixesDir2-Build-check.cmake b/Tests/RunCMake/ClangTidy/ExportFixesDir2-Build-check.cmake
new file mode 100644
index 0000000..f65d33c
--- /dev/null
+++ b/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()
diff --git a/Tests/RunCMake/ClangTidy/ExportFixesDir2-check.cmake b/Tests/RunCMake/ClangTidy/ExportFixesDir2-check.cmake
new file mode 100644
index 0000000..f65d33c
--- /dev/null
+++ b/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()
diff --git a/Tests/RunCMake/ClangTidy/ExportFixesDir2.cmake b/Tests/RunCMake/ClangTidy/ExportFixesDir2.cmake
new file mode 100644
index 0000000..c81c49a
--- /dev/null
+++ b/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)
diff --git a/Tests/RunCMake/ClangTidy/RunCMakeTest.cmake b/Tests/RunCMake/ClangTidy/RunCMakeTest.cmake
index 5e3fbc4..01dbb61 100644
--- a/Tests/RunCMake/ClangTidy/RunCMakeTest.cmake
+++ b/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()
diff --git a/Tests/RunCMake/ClangTidy/export_fixes_subdir/CMakeLists.txt b/Tests/RunCMake/ClangTidy/export_fixes_subdir/CMakeLists.txt
new file mode 100644
index 0000000..af2db88
--- /dev/null
+++ b/Tests/RunCMake/ClangTidy/export_fixes_subdir/CMakeLists.txt
@@ -0,0 +1 @@
+add_executable(subdir ${files})
diff --git a/Tests/RunCMake/ClangTidy/extra.c b/Tests/RunCMake/ClangTidy/extra.c
new file mode 100644
index 0000000..d235550
--- /dev/null
+++ b/Tests/RunCMake/ClangTidy/extra.c
@@ -0,0 +1,3 @@
+void extra(void)
+{
+}
diff --git a/Tests/RunCMake/CommandLine/E___run_co_compile-tidy-remove-fixes-check.cmake b/Tests/RunCMake/CommandLine/E___run_co_compile-tidy-remove-fixes-check.cmake
new file mode 100644
index 0000000..d638fda
--- /dev/null
+++ b/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()
diff --git a/Tests/RunCMake/CommandLine/E___run_co_compile-tidy-remove-fixes-prep.cmake b/Tests/RunCMake/CommandLine/E___run_co_compile-tidy-remove-fixes-prep.cmake
new file mode 100644
index 0000000..1e89ffe
--- /dev/null
+++ b/Tests/RunCMake/CommandLine/E___run_co_compile-tidy-remove-fixes-prep.cmake
@@ -0,0 +1 @@
+file(TOUCH "${RunCMake_BINARY_DIR}/tidy-fixes.yaml")
diff --git a/Tests/RunCMake/CommandLine/RunCMakeTest.cmake b/Tests/RunCMake/CommandLine/RunCMakeTest.cmake
index 08c5a49..9f342bb 100644
--- a/Tests/RunCMake/CommandLine/RunCMakeTest.cmake
+++ b/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)
diff --git a/Tests/RunCMake/pseudo_tidy.c b/Tests/RunCMake/pseudo_tidy.c
index a43133b..f227c06 100644
--- a/Tests/RunCMake/pseudo_tidy.c
+++ b/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;
-- 
cgit v0.12