Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion clang/docs/ClangFormat.rst
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ An easy way to create the ``.clang-format`` file is:

Available style options are described in :doc:`ClangFormatStyleOptions`.

.clang-format-ignore
====================

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:
Expand All @@ -141,7 +144,8 @@ 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
containing drive names (e.g. ``C:``) are not supported.
* Patterns follow the rules specified in `POSIX 2.13.1, 2.13.2, and Rule 1 of
2.13.3 <https://pubs.opengroup.org/onlinepubs/9699919799/utilities/
V3_chap02.html#tag_18_13>`_.
Expand Down
17 changes: 15 additions & 2 deletions clang/test/Format/clang-format-ignore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,18 @@
// 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
// RUN: cd ../..
// RUN: clang-format -verbose *.cc level1/*.c* level1/level2/foo.* 2> %t.stderr
// RUN: grep "Formatting \[1/5] level1/level2/foo.c" %t.stderr
// RUN: not grep "Formatting \[2/5] level1/level2/foo.js" %t.stderr

// RUN: rm .clang-format-ignore
// RUN: clang-format -verbose *.cc level1/*.c* level1/level2/foo.* 2> %t.stderr
// RUN: grep "Formatting \[1/5] foo.cc" %t.stderr
// RUN: grep "Formatting \[2/5] level1/bar.cc" %t.stderr
// RUN: grep "Formatting \[3/5] level1/baz.c" %t.stderr
// RUN: grep "Formatting \[4/5] level1/level2/foo.c" %t.stderr
// RUN: not grep "Formatting \[5/5] level1/level2/foo.js" %t.stderr

// RUN: cd ..
// RUN: rm -r %t.dir
65 changes: 45 additions & 20 deletions clang/tools/clang-format/ClangFormat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,11 @@ static int dumpConfig(bool IsSTDIN) {
return 0;
}

using String = SmallString<128>;
static String IgnoreDir; // Directory of .clang-format-ignore file.
static StringRef PrevDir; // Directory of previous `FilePath`.
static SmallVector<String> Patterns; // Patterns in .clang-format-ignore file.

// Check whether `FilePath` is ignored according to the nearest
// .clang-format-ignore file based on the rules below:
// - A blank line is skipped.
Expand All @@ -586,45 +591,65 @@ static bool isIgnored(StringRef FilePath) {
if (!is_regular_file(FilePath))
return false;

using namespace llvm::sys::path;
SmallString<128> Path, AbsPath{FilePath};
String Path;
String AbsPath{FilePath};

using namespace llvm::sys::path;
make_absolute(AbsPath);
remove_dots(AbsPath, /*remove_dot_dot=*/true);

StringRef IgnoreDir{AbsPath};
do {
IgnoreDir = parent_path(IgnoreDir);
if (IgnoreDir.empty())
if (StringRef Dir{parent_path(AbsPath)}; PrevDir != Dir) {
PrevDir = Dir;

for (;;) {
Path = Dir;
append(Path, ".clang-format-ignore");
if (is_regular_file(Path))
break;
Dir = parent_path(Dir);
if (Dir.empty())
return false;
}

IgnoreDir = convert_to_slash(Dir);

std::ifstream IgnoreFile{Path.c_str()};
if (!IgnoreFile.good())
return false;

Path = IgnoreDir;
append(Path, ".clang-format-ignore");
} while (!is_regular_file(Path));
Patterns.clear();

std::ifstream IgnoreFile{Path.c_str()};
if (!IgnoreFile.good())
return false;
for (std::string Line; std::getline(IgnoreFile, Line);) {
if (const auto Pattern{StringRef{Line}.trim()};
// Skip empty and comment lines.
!Pattern.empty() && Pattern[0] != '#') {
Patterns.push_back(Pattern);
}
}
}

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;
if (IgnoreDir.empty())
return false;

const bool IsNegated = Pattern[0] == '!';
const auto Pathname{convert_to_slash(AbsPath)};
for (const auto &Pat : Patterns) {
const bool IsNegated = Pat[0] == '!';
StringRef Pattern{Pat};
if (IsNegated)
Pattern = Pattern.drop_front();

if (Pattern.empty())
continue;

Pattern = Pattern.ltrim();

// `Pattern` is relative to `IgnoreDir` unless it starts with a slash.
// This doesn't support patterns containing drive names (e.g. `C:`).
if (Pattern[0] != '/') {
Path = convert_to_slash(IgnoreDir);
Path = IgnoreDir;
append(Path, Style::posix, Pattern);
remove_dots(Path, /*remove_dot_dot=*/true, Style::posix);
Pattern = Path.str();
Pattern = Path;
}

if (clang::format::matchFilePath(Pattern, Pathname) == !IsNegated)
Expand Down