[clang] [clang-format] Add .clang-format.ignore for ignoring files (PR #76327)

Owen Pan via cfe-commits cfe-commits at lists.llvm.org
Tue Dec 26 00:52:38 PST 2023


https://github.com/owenca updated https://github.com/llvm/llvm-project/pull/76327

>From 4afd12db61528b40d842a7fbee9af37c2235822c Mon Sep 17 00:00:00 2001
From: Owen Pan <owenpiano at gmail.com>
Date: Sun, 24 Dec 2023 01:18:55 -0800
Subject: [PATCH 1/3] [clang-format] Add .clang-format.ignore for ignoring
 files

Closes #52975.
---
 clang/docs/ClangFormat.rst                | 18 ++++++
 clang/test/Format/clang-format-ignore.cpp | 24 ++++++++
 clang/tools/clang-format/ClangFormat.cpp  | 71 ++++++++++++++++++++++-
 3 files changed, 112 insertions(+), 1 deletion(-)
 create mode 100644 clang/test/Format/clang-format-ignore.cpp

diff --git a/clang/docs/ClangFormat.rst b/clang/docs/ClangFormat.rst
index f52f35550d03eb..a0b28f2273991f 100644
--- a/clang/docs/ClangFormat.rst
+++ b/clang/docs/ClangFormat.rst
@@ -131,6 +131,24 @@ An easy way to create the ``.clang-format`` file is:
 
 Available style options are described in :doc:`ClangFormatStyleOptions`.
 
+You can create ``.clang-format-ignore`` files to make ``clang-format`` ignore
+certain files. A ``.clang-format-ignore`` file consists of patterns of file path
+names. It has the following format:
+- A blank line is skipped.
+- Leading and trailing spaces of a line are trimmed.
+- A line starting with a hash (``#``) is a comment.
+- A non-comment line is a single pattern.
+- The slash (``/``) is used as the directory separator.
+- A pattern is relative to the directory of the ``.clang-format-ignore`` file
+  (or the root directory if the pattern starts with a slash).
+- Patterns follow the rules specified in POSIX 2.13.1, 2.13.2, and Rule 1 of
+  2.13.3.
+- A pattern is negated if it starts with a bang (``!``).
+To match all files in a directory, use e.g. ``foo/bar/*``. To match all files in
+the directory of the ``.clang-format-ignore`` file, use ``*``.
+Multiple ``.clang-format-ignore`` files are supported similar to the
+``.clang-format`` files, with a lower directory level file voiding the higher
+level ones.
 
 Vim Integration
 ===============
diff --git a/clang/test/Format/clang-format-ignore.cpp b/clang/test/Format/clang-format-ignore.cpp
new file mode 100644
index 00000000000000..a2210266034d4c
--- /dev/null
+++ b/clang/test/Format/clang-format-ignore.cpp
@@ -0,0 +1,24 @@
+// RUN: mkdir -p %t.dir/level1/level2
+
+// RUN: cd %t.dir
+// RUN: printf "*\nlevel*/*.c*\n*/*2/foo.*\n" > .clang-format-ignore
+// RUN: touch foo.cc
+// RUN: clang-format -verbose .clang-format-ignore foo.cc 2> %t.stderr
+// RUN: not grep "Formatting" %t.stderr
+
+// RUN: cd level1
+// RUN: touch bar.cc baz.c
+// RUN: clang-format -verbose bar.cc baz.c 2> %t.stderr
+// RUN: not grep "Formatting" %t.stderr
+
+// RUN: cd level2
+// RUN: touch foo.c foo.js
+// RUN: clang-format -verbose foo.c foo.js 2> %t.stderr
+// RUN: not grep "Formatting" %t.stderr
+// RUN: printf "*.js\n" > .clang-format-ignore
+// RUN: clang-format -verbose foo.c foo.js 2> %t.stderr
+// RUN: grep -E "Formatting (.*)foo.c(.*)" %t.stderr
+// RUN: not grep -E "Formatting (.*)foo.js(.*)" %t.stderr
+
+// RUN: cd ../../..
+// RUN: rm -rf %t.dir
diff --git a/clang/tools/clang-format/ClangFormat.cpp b/clang/tools/clang-format/ClangFormat.cpp
index d2e3d8d43aef21..be78f8cbebf5e1 100644
--- a/clang/tools/clang-format/ClangFormat.cpp
+++ b/clang/tools/clang-format/ClangFormat.cpp
@@ -12,6 +12,7 @@
 ///
 //===----------------------------------------------------------------------===//
 
+#include "../../lib/Format/MatchFilePath.h"
 #include "clang/Basic/Diagnostic.h"
 #include "clang/Basic/DiagnosticOptions.h"
 #include "clang/Basic/FileManager.h"
@@ -570,6 +571,71 @@ static int dumpConfig(bool IsSTDIN) {
   return 0;
 }
 
+// Check whether `FilePath` is ignored according to the nearest
+// .clang-format-ignore file based on the rules below:
+// - A blank line is skipped.
+// - Leading and trailing spaces of a line are trimmed.
+// - A line starting with a hash (`#`) is a comment.
+// - A non-comment line is a single pattern.
+// - The slash (`/`) is used as the directory separator.
+// - A pattern is relative to the directory of the .clang-format-ignore file (or
+//   the root directory if the pattern starts with a slash).
+// - A pattern is negated if it starts with a bang (`!`).
+static bool isIgnored(const StringRef FilePath) {
+  if (!llvm::sys::fs::is_regular_file(FilePath))
+    return false;
+
+  using namespace llvm::sys::path;
+  SmallString<128> Path, AbsPath{convert_to_slash(FilePath)};
+
+  llvm::vfs::getRealFileSystem()->makeAbsolute(AbsPath);
+  remove_dots(AbsPath, /*remove_dot_dot=*/true);
+
+  StringRef IgnoreDir{AbsPath};
+  do {
+    IgnoreDir = parent_path(IgnoreDir);
+    if (IgnoreDir.empty())
+      return false;
+
+    Path = IgnoreDir;
+    append(Path, ".clang-format-ignore");
+  } while (!llvm::sys::fs::is_regular_file(Path));
+
+  std::ifstream IgnoreFile{Path.c_str()};
+  if (!IgnoreFile.good())
+    return false;
+
+  bool HasMatch = false;
+  for (std::string Pattern; std::getline(IgnoreFile, Pattern);) {
+    Pattern = StringRef(Pattern).trim();
+    if (Pattern.empty() || Pattern[0] == '#')
+      continue;
+
+    const bool IsNegated = Pattern[0] == '!';
+    if (IsNegated)
+      Pattern.erase(0, 1);
+
+    if (Pattern.empty())
+      continue;
+
+    Pattern = StringRef(Pattern).ltrim();
+    if (Pattern[0] != '/') {
+      Path = IgnoreDir;
+      append(Path, Pattern);
+      remove_dots(Path, /*remove_dot_dot=*/true);
+      Pattern = Path.str();
+    }
+
+    if (clang::format::matchFilePath(Pattern, AbsPath.str()) == !IsNegated) {
+      HasMatch = true;
+      break;
+    }
+  }
+
+  IgnoreFile.close();
+  return HasMatch;
+}
+
 int main(int argc, const char **argv) {
   llvm::InitLLVM X(argc, argv);
 
@@ -618,11 +684,14 @@ int main(int argc, const char **argv) {
   unsigned FileNo = 1;
   bool Error = false;
   for (const auto &FileName : FileNames) {
+    const bool IsSTDIN = FileName == "-";
+    if (!IsSTDIN && isIgnored(FileName))
+      continue;
     if (Verbose) {
       errs() << "Formatting [" << FileNo++ << "/" << FileNames.size() << "] "
              << FileName << "\n";
     }
-    Error |= clang::format::format(FileName, FileName == "-");
+    Error |= clang::format::format(FileName, IsSTDIN);
   }
   return Error ? 1 : 0;
 }

>From 0abdba2a457be700921e903f66b5a3d7ca54c55f Mon Sep 17 00:00:00 2001
From: Owen Pan <owenpiano at gmail.com>
Date: Sun, 24 Dec 2023 01:41:22 -0800
Subject: [PATCH 2/3] Update clang/docs/ClangFormat.rst

---
 clang/docs/ClangFormat.rst | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/clang/docs/ClangFormat.rst b/clang/docs/ClangFormat.rst
index a0b28f2273991f..822839c6842521 100644
--- a/clang/docs/ClangFormat.rst
+++ b/clang/docs/ClangFormat.rst
@@ -140,9 +140,9 @@ names. It has the following format:
 - A non-comment line is a single pattern.
 - The slash (``/``) is used as the directory separator.
 - A pattern is relative to the directory of the ``.clang-format-ignore`` file
-  (or the root directory if the pattern starts with a slash).
+(or the root directory if the pattern starts with a slash).
 - Patterns follow the rules specified in POSIX 2.13.1, 2.13.2, and Rule 1 of
-  2.13.3.
+2.13.3.
 - A pattern is negated if it starts with a bang (``!``).
 To match all files in a directory, use e.g. ``foo/bar/*``. To match all files in
 the directory of the ``.clang-format-ignore`` file, use ``*``.

>From fbf142f563a72ecc9b561296d14c96f2558f80ea Mon Sep 17 00:00:00 2001
From: Owen Pan <owenpiano at gmail.com>
Date: Tue, 26 Dec 2023 00:48:01 -0800
Subject: [PATCH 3/3] Fix bugs for Windows.

---
 clang/docs/ClangFormat.rst                |  1 +
 clang/test/Format/clang-format-ignore.cpp | 21 ++++++++++++++-------
 clang/tools/clang-format/ClangFormat.cpp  | 15 +++++++++------
 3 files changed, 24 insertions(+), 13 deletions(-)

diff --git a/clang/docs/ClangFormat.rst b/clang/docs/ClangFormat.rst
index 822839c6842521..67fdffbd116d8a 100644
--- a/clang/docs/ClangFormat.rst
+++ b/clang/docs/ClangFormat.rst
@@ -144,6 +144,7 @@ names. It has the following format:
 - Patterns follow the rules specified in POSIX 2.13.1, 2.13.2, and Rule 1 of
 2.13.3.
 - A pattern is negated if it starts with a bang (``!``).
+
 To match all files in a directory, use e.g. ``foo/bar/*``. To match all files in
 the directory of the ``.clang-format-ignore`` file, use ``*``.
 Multiple ``.clang-format-ignore`` files are supported similar to the
diff --git a/clang/test/Format/clang-format-ignore.cpp b/clang/test/Format/clang-format-ignore.cpp
index a2210266034d4c..3209588de80b02 100644
--- a/clang/test/Format/clang-format-ignore.cpp
+++ b/clang/test/Format/clang-format-ignore.cpp
@@ -1,24 +1,31 @@
+// RUN: rm -rf %t.dir
 // RUN: mkdir -p %t.dir/level1/level2
 
 // RUN: cd %t.dir
-// RUN: printf "*\nlevel*/*.c*\n*/*2/foo.*\n" > .clang-format-ignore
+// RUN: printf "%%s\n" "\*" "level*/*.c*" "*/*2/foo.*" > .clang-format-ignore
 // RUN: touch foo.cc
 // RUN: clang-format -verbose .clang-format-ignore foo.cc 2> %t.stderr
-// RUN: not grep "Formatting" %t.stderr
+// RUN: not grep Formatting %t.stderr
 
 // RUN: cd level1
 // RUN: touch bar.cc baz.c
 // RUN: clang-format -verbose bar.cc baz.c 2> %t.stderr
-// RUN: not grep "Formatting" %t.stderr
+// RUN: not grep Formatting %t.stderr
 
 // RUN: cd level2
 // RUN: touch foo.c foo.js
 // RUN: clang-format -verbose foo.c foo.js 2> %t.stderr
-// RUN: not grep "Formatting" %t.stderr
-// RUN: printf "*.js\n" > .clang-format-ignore
+// RUN: not grep Formatting %t.stderr
+
+// RUN: touch .clang-format-ignore
+// RUN: clang-format -verbose foo.c foo.js 2> %t.stderr
+// RUN: grep "Formatting \[1/2] foo.c" %t.stderr
+// RUN: grep "Formatting \[2/2] foo.js" %t.stderr
+
+// RUN: printf "%%s\n" "*.js" > .clang-format-ignore
 // RUN: clang-format -verbose foo.c foo.js 2> %t.stderr
-// RUN: grep -E "Formatting (.*)foo.c(.*)" %t.stderr
-// RUN: not grep -E "Formatting (.*)foo.js(.*)" %t.stderr
+// RUN: grep "Formatting \[1/2] foo.c" %t.stderr
+// RUN: not grep "Formatting \[2/2] foo.js" %t.stderr
 
 // RUN: cd ../../..
 // RUN: rm -rf %t.dir
diff --git a/clang/tools/clang-format/ClangFormat.cpp b/clang/tools/clang-format/ClangFormat.cpp
index be78f8cbebf5e1..7d39aab9ff37dc 100644
--- a/clang/tools/clang-format/ClangFormat.cpp
+++ b/clang/tools/clang-format/ClangFormat.cpp
@@ -581,14 +581,15 @@ static int dumpConfig(bool IsSTDIN) {
 // - A pattern is relative to the directory of the .clang-format-ignore file (or
 //   the root directory if the pattern starts with a slash).
 // - A pattern is negated if it starts with a bang (`!`).
-static bool isIgnored(const StringRef FilePath) {
-  if (!llvm::sys::fs::is_regular_file(FilePath))
+static bool isIgnored(StringRef FilePath) {
+  using namespace llvm::sys::fs;
+  if (!is_regular_file(FilePath))
     return false;
 
   using namespace llvm::sys::path;
-  SmallString<128> Path, AbsPath{convert_to_slash(FilePath)};
+  SmallString<128> Path, AbsPath{FilePath};
 
-  llvm::vfs::getRealFileSystem()->makeAbsolute(AbsPath);
+  make_absolute(AbsPath);
   remove_dots(AbsPath, /*remove_dot_dot=*/true);
 
   StringRef IgnoreDir{AbsPath};
@@ -599,12 +600,14 @@ static bool isIgnored(const StringRef FilePath) {
 
     Path = IgnoreDir;
     append(Path, ".clang-format-ignore");
-  } while (!llvm::sys::fs::is_regular_file(Path));
+  } while (!is_regular_file(Path));
 
   std::ifstream IgnoreFile{Path.c_str()};
   if (!IgnoreFile.good())
     return false;
 
+  AbsPath = convert_to_slash(AbsPath);
+
   bool HasMatch = false;
   for (std::string Pattern; std::getline(IgnoreFile, Pattern);) {
     Pattern = StringRef(Pattern).trim();
@@ -623,7 +626,7 @@ static bool isIgnored(const StringRef FilePath) {
       Path = IgnoreDir;
       append(Path, Pattern);
       remove_dots(Path, /*remove_dot_dot=*/true);
-      Pattern = Path.str();
+      Pattern = convert_to_slash(Path);
     }
 
     if (clang::format::matchFilePath(Pattern, AbsPath.str()) == !IsNegated) {



More information about the cfe-commits mailing list