Skip to content

Commit

Permalink
[clang-format] Add .clang-format-ignore for ignoring files (#76327)
Browse files Browse the repository at this point in the history
Closes #52975.
  • Loading branch information
owenca committed Dec 30, 2023
1 parent 589a24b commit 0930812
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 1 deletion.
19 changes: 19 additions & 0 deletions clang/docs/ClangFormat.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
===============
Expand Down
33 changes: 33 additions & 0 deletions clang/test/Format/clang-format-ignore.cpp
Original file line number Diff line number Diff line change
@@ -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
69 changes: 68 additions & 1 deletion clang/tools/clang-format/ClangFormat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
///
//===----------------------------------------------------------------------===//

#include "../../lib/Format/MatchFilePath.h"
#include "clang/Basic/Diagnostic.h"
#include "clang/Basic/DiagnosticOptions.h"
#include "clang/Basic/FileManager.h"
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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;
}

0 comments on commit 0930812

Please sign in to comment.