Skip to content

Commit

Permalink
[clang-format] Add .clang-format-ignore for ignoring files
Browse files Browse the repository at this point in the history
  • Loading branch information
owenca committed Dec 19, 2023
1 parent 401f039 commit 52866dc
Show file tree
Hide file tree
Showing 4 changed files with 213 additions and 1 deletion.
1 change: 1 addition & 0 deletions clang/tools/clang-format/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ set(LLVM_LINK_COMPONENTS support)

add_clang_tool(clang-format
ClangFormat.cpp
ClangFormatIgnore.cpp
)

set(CLANG_FORMAT_LIB_DEPS
Expand Down
6 changes: 5 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 "ClangFormatIgnore.h"
#include "clang/Basic/Diagnostic.h"
#include "clang/Basic/DiagnosticOptions.h"
#include "clang/Basic/FileManager.h"
Expand Down Expand Up @@ -618,11 +619,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 && clang::format::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;
}
179 changes: 179 additions & 0 deletions clang/tools/clang-format/ClangFormatIgnore.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
//===-- ClangFormatIgnore.cpp - Specify files clang-format should ignore --===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
///
/// \file
/// This file implements the functionality of matching a file path name to
/// patterns in a .clang-format-ignore file to see whether clang-format should
/// ignore it.
///
//===----------------------------------------------------------------------===//

#include "llvm/Support/VirtualFileSystem.h"
#include <fstream>

using namespace llvm;

namespace clang {
namespace format {

// Check whether `FilePath` matches `Pattern` based on POSIX Section 2.13.
static bool matchFilePath(const StringRef Pattern, const StringRef FilePath) {
assert(!Pattern.empty());
assert(!FilePath.empty());

constexpr auto Separator = '/';
const auto EOP = Pattern.size(); // End of `Pattern`.
const auto End = FilePath.size(); // End of `FilePath`.
unsigned I = 0; // Index to `Pattern`.

// No match if `Pattern` ends with a non-meta character not equal to the last
// character of `FilePath`.
if (const auto C = Pattern.back(); !strchr("?*]", C) && C != FilePath.back())
return false;

for (const auto &F : FilePath) {
if (I == EOP)
return false;

switch (Pattern[I]) {
case '\\':
if (++I == EOP || F != '\\')
return false;
++I;
break;
case '?':
if (F == Separator)
return false;
++I;
break;
case '*': {
unsigned J = &F - FilePath.data(); // Index of `F`.
// Skip consecutive stars.
do {
if (++I == EOP)
return FilePath.find(Separator, J + 1) == StringRef::npos;
} while (Pattern[I] == '*');
while (FilePath[J] != Separator) {
if (matchFilePath(Pattern.substr(I), FilePath.substr(J)))
return true;
if (++J == End)
return false;
}
break;
}
case '[':
// Skip e.g. `[!]`.
if (I + 3 < EOP || (I + 3 == EOP && Pattern[I + 1] != '!')) {
// Skip unpaired `[`, brackets containing slashes, and `[]`.
if (const auto J = Pattern.find_first_of("]/", I + 1);
J != StringRef::npos && Pattern[J] == ']' && J > I + 1) {
if (F == Separator)
return false;
++I; // After the `[`.
bool Negated = false;
if (Pattern[I] == '!') {
Negated = true;
++I; // After the `!`.
}
bool Match = false;
do {
if (I + 2 < J && Pattern[I + 1] == '-') {
Match = Pattern[I] <= F && F <= Pattern[I + 2];
I += 3; // After the range, e.g. `A-Z`.
} else {
Match = F == Pattern[I++];
}
} while (!Match && I < J);
if (Negated ? Match : !Match)
return false;
I = J + 1; // After the `]`.
break;
}
}
[[fallthrough]]; // Match `[` literally.
default:
if (F != Pattern[I])
return false;
++I;
}
}

// Match trailing stars with null strings.
while (I < EOP && Pattern[I] == '*')
++I;

return I == EOP;
}

// 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 (`!`).
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 (matchFilePath(Pattern.c_str(), AbsPath.c_str()) == !IsNegated) {
HasMatch = true;
break;
}
}

IgnoreFile.close();
return HasMatch;
}

} // namespace format
} // namespace clang
28 changes: 28 additions & 0 deletions clang/tools/clang-format/ClangFormatIgnore.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//===--- ClangFormatIgnore.h ------------------------------------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
///
/// \file
/// This file declares isIgnored() that checks whether a file should be ignored
/// by clang-format.
///
//===----------------------------------------------------------------------===//

#ifndef LLVM_CLANG_TOOLS_CLANGFORMAT_CLANGFORMATIGNORE_H
#define LLVM_CLANG_TOOLS_CLANGFORMAT_CLANGFORMATIGNORE_H

#include "llvm/ADT/StringRef.h"

namespace clang {
namespace format {

bool isIgnored(const llvm::StringRef FilePath);

} // end namespace format
} // end namespace clang

#endif

0 comments on commit 52866dc

Please sign in to comment.