diff --git a/clang/docs/ClangFormat.rst b/clang/docs/ClangFormat.rst index f52f35550d03e..67fdffbd116d8 100644 --- a/clang/docs/ClangFormat.rst +++ b/clang/docs/ClangFormat.rst @@ -131,6 +131,25 @@ 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 0000000000000..0d6396a64a668 --- /dev/null +++ b/clang/test/Format/clang-format-ignore.cpp @@ -0,0 +1,33 @@ +// RUN: rm -rf %t.dir +// RUN: mkdir -p %t.dir/level1/level2 + +// RUN: cd %t.dir +// RUN: echo "*" > .clang-format-ignore +// RUN: echo "level*/*.c*" >> .clang-format-ignore +// RUN: echo "*/*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: 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: 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: echo "*.js" > .clang-format-ignore +// RUN: clang-format -verbose foo.c foo.js 2> %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 d2e3d8d43aef2..be34dbbe886a1 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,69 @@ 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(StringRef FilePath) { + using namespace llvm::sys::fs; + if (!is_regular_file(FilePath)) + return false; + + using namespace llvm::sys::path; + SmallString<128> Path, AbsPath{FilePath}; + + make_absolute(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 (!is_regular_file(Path)); + + std::ifstream IgnoreFile{Path.c_str()}; + if (!IgnoreFile.good()) + return false; + + const auto Pathname = convert_to_slash(AbsPath); + for (std::string Line; std::getline(IgnoreFile, Line);) { + auto Pattern = StringRef(Line).trim(); + if (Pattern.empty() || Pattern[0] == '#') + continue; + + const bool IsNegated = Pattern[0] == '!'; + if (IsNegated) + Pattern = Pattern.drop_front(); + + if (Pattern.empty()) + continue; + + Pattern = Pattern.ltrim(); + if (Pattern[0] != '/') { + Path = convert_to_slash(IgnoreDir); + append(Path, Style::posix, Pattern); + remove_dots(Path, /*remove_dot_dot=*/true, Style::posix); + Pattern = Path.str(); + } + + if (clang::format::matchFilePath(Pattern, Pathname) == !IsNegated) + return true; + } + + return false; +} + int main(int argc, const char **argv) { llvm::InitLLVM X(argc, argv); @@ -618,11 +682,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; }