diff --git a/clang/docs/ClangFormatStyleOptions.rst b/clang/docs/ClangFormatStyleOptions.rst index 4dc0de3a90f26..4ba38808cd509 100644 --- a/clang/docs/ClangFormatStyleOptions.rst +++ b/clang/docs/ClangFormatStyleOptions.rst @@ -3220,6 +3220,25 @@ the configuration (without a prefix: ``Auto``). +.. _EmptyLinesAfterIncludes: + +**EmptyLinesAfterIncludes** (``Unsigned``) :versionbadge:`clang-format 18` :ref:`¶ ` + Number of lines after each include area. An include area is + a list of consecutive include statements. The include area may be + composed of multiple include blocks. + Limited by MaxEmptyLinesToKeep. + Example: + + .. code-block:: c++ + + + EmptyLinesAfterIncludes: 1 vs. EmptyLinesAfterIncludes: 2 + #include #include + #include #include + + class Test {}; + class Test {}; + .. _ExperimentalAutoDetectBinPacking: **ExperimentalAutoDetectBinPacking** (``Boolean``) :versionbadge:`clang-format 3.7` :ref:`¶ ` diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst index 4888ffe6f4dfc..e1bf86c8a83b6 100644 --- a/clang/docs/ReleaseNotes.rst +++ b/clang/docs/ReleaseNotes.rst @@ -1260,6 +1260,7 @@ clang-format - Add ``.clang-format-ignore`` files. - Add ``AlignFunctionPointers`` sub-option for ``AlignConsecutiveDeclarations``. - Add ``SkipMacroDefinitionBody`` option. +- Add ``EmptyLinesAfterIncludes`` option. libclang -------- diff --git a/clang/include/clang/Format/Format.h b/clang/include/clang/Format/Format.h index bc9eecd42f9eb..84d1a0b70b9ef 100644 --- a/clang/include/clang/Format/Format.h +++ b/clang/include/clang/Format/Format.h @@ -2459,6 +2459,23 @@ struct FormatStyle { /// \version 12 EmptyLineBeforeAccessModifierStyle EmptyLineBeforeAccessModifier; + /// \brief Number of lines after each include area. An include area is + /// a list of consecutive include statements. The include area may be + /// composed of multiple include blocks. + /// Limited by MaxEmptyLinesToKeep. + /// Example: + /// \code + /// + /// EmptyLinesAfterIncludes: 1 vs. EmptyLinesAfterIncludes: 2 + /// #include #include + /// #include #include + /// + /// class Test {}; + /// class Test {}; + /// \endcode + /// \version 18 + std::optional EmptyLinesAfterIncludes; + /// If ``true``, clang-format detects whether function calls and /// definitions are formatted with one parameter per line. /// @@ -4831,6 +4848,7 @@ struct FormatStyle { DerivePointerAlignment == R.DerivePointerAlignment && DisableFormat == R.DisableFormat && EmptyLineAfterAccessModifier == R.EmptyLineAfterAccessModifier && + EmptyLinesAfterIncludes == R.EmptyLinesAfterIncludes && EmptyLineBeforeAccessModifier == R.EmptyLineBeforeAccessModifier && ExperimentalAutoDetectBinPacking == R.ExperimentalAutoDetectBinPacking && diff --git a/clang/lib/Format/CMakeLists.txt b/clang/lib/Format/CMakeLists.txt index 84a3c136f650a..ff3860426407a 100644 --- a/clang/lib/Format/CMakeLists.txt +++ b/clang/lib/Format/CMakeLists.txt @@ -8,6 +8,7 @@ add_clang_library(clangFormat Format.cpp FormatToken.cpp FormatTokenLexer.cpp + IncludesSeparator.cpp IntegerLiteralSeparatorFixer.cpp MacroCallReconstructor.cpp MacroExpander.cpp diff --git a/clang/lib/Format/Format.cpp b/clang/lib/Format/Format.cpp index ff326dc784783..f068da97e6dfb 100644 --- a/clang/lib/Format/Format.cpp +++ b/clang/lib/Format/Format.cpp @@ -20,6 +20,7 @@ #include "FormatInternal.h" #include "FormatToken.h" #include "FormatTokenLexer.h" +#include "IncludesSeparator.h" #include "IntegerLiteralSeparatorFixer.h" #include "NamespaceEndCommentsFixer.h" #include "ObjCPropertyAttributeOrderFixer.h" @@ -995,6 +996,7 @@ template <> struct MappingTraits { IO.mapOptional("DisableFormat", Style.DisableFormat); IO.mapOptional("EmptyLineAfterAccessModifier", Style.EmptyLineAfterAccessModifier); + IO.mapOptional("EmptyLinesAfterIncludes", Style.EmptyLinesAfterIncludes); IO.mapOptional("EmptyLineBeforeAccessModifier", Style.EmptyLineBeforeAccessModifier); IO.mapOptional("ExperimentalAutoDetectBinPacking", @@ -1502,6 +1504,7 @@ FormatStyle getLLVMStyle(FormatStyle::LanguageKind Language) { LLVMStyle.DerivePointerAlignment = false; LLVMStyle.DisableFormat = false; LLVMStyle.EmptyLineAfterAccessModifier = FormatStyle::ELAAMS_Never; + LLVMStyle.EmptyLinesAfterIncludes = std::nullopt; LLVMStyle.EmptyLineBeforeAccessModifier = FormatStyle::ELBAMS_LogicalBlock; LLVMStyle.ExperimentalAutoDetectBinPacking = false; LLVMStyle.FixNamespaceComments = true; @@ -3715,6 +3718,12 @@ reformat(const FormatStyle &Style, StringRef Code, }); } + if (Style.EmptyLinesAfterIncludes.has_value()) { + Passes.emplace_back([&](const Environment &Env) { + return IncludesSeparator(Env, Expanded).process(); + }); + } + if (Style.Language == FormatStyle::LK_ObjC && !Style.ObjCPropertyAttributeOrder.empty()) { Passes.emplace_back([&](const Environment &Env) { diff --git a/clang/lib/Format/IncludesSeparator.cpp b/clang/lib/Format/IncludesSeparator.cpp new file mode 100644 index 0000000000000..0e74383c7c5b7 --- /dev/null +++ b/clang/lib/Format/IncludesSeparator.cpp @@ -0,0 +1,159 @@ +//===--- IncludesSeparator.cpp ---------------------------*- 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 implements IncludesSeparator, a TokenAnalyzer that inserts +/// new lines or removes empty lines after an include area. +/// An includes area is a list of consecutive include statements. +/// +//===----------------------------------------------------------------------===// + +#include "IncludesSeparator.h" +#include "TokenAnnotator.h" +#define DEBUG_TYPE "includes-separator" + +namespace { +bool isConditionalCompilationStart(const clang::format::AnnotatedLine &Line) { + if (!Line.First) + return false; + const auto *NextToken = Line.First->getNextNonComment(); + return Line.First->is(clang::tok::hash) && NextToken && + NextToken->isOneOf(clang::tok::pp_if, clang::tok::pp_ifdef, + clang::tok::pp_ifndef, clang::tok::pp_defined); +} + +bool isConditionalCompilationEnd(const clang::format::AnnotatedLine &Line) { + if (!Line.First) + return false; + const auto *NextToken = Line.First->getNextNonComment(); + return Line.First->is(clang::tok::hash) && NextToken && + NextToken->is(clang::tok::pp_endif); +} + +bool isConditionalCompilationStatement( + const clang::format::AnnotatedLine &Line) { + if (!Line.First) + return false; + const auto *NextToken = Line.First->getNextNonComment(); + return Line.First->is(clang::tok::hash) && NextToken && + NextToken->isOneOf(clang::tok::pp_if, clang::tok::pp_ifdef, + clang::tok::pp_ifndef, clang::tok::pp_elif, + clang::tok::pp_elifdef, clang::tok::pp_elifndef, + clang::tok::pp_else, clang::tok::pp_defined, + clang::tok::pp_endif); +} + +bool isCCOnlyWithIncludes( + const llvm::SmallVectorImpl &Lines, + unsigned StartIdx) { + int CCLevel = 0; + for (unsigned I = StartIdx; I < Lines.size(); ++I) { + const auto &CurrentLine = *Lines[I]; + if (isConditionalCompilationStart(CurrentLine)) + CCLevel++; + + if (isConditionalCompilationEnd(CurrentLine)) + CCLevel--; + + if (CCLevel == 0) + break; + + if (!(CurrentLine.isInclude() || + isConditionalCompilationStatement(CurrentLine))) { + return false; + } + } + return true; +} + +unsigned getEndOfCCBlock( + const llvm::SmallVectorImpl &Lines, + unsigned StartIdx) { + int CCLevel = 0; + unsigned I = StartIdx; + for (; I < Lines.size(); ++I) { + const auto &CurrentLine = *Lines[I]; + if (isConditionalCompilationStart(CurrentLine)) + CCLevel++; + + if (isConditionalCompilationEnd(CurrentLine)) + CCLevel--; + + if (CCLevel == 0) + break; + } + return I; +} +} // namespace + +namespace clang { +namespace format { +std::pair +IncludesSeparator::analyze(TokenAnnotator &Annotator, + SmallVectorImpl &AnnotatedLines, + FormatTokenLexer &Tokens) { + assert(Style.EmptyLinesAfterIncludes.has_value()); + AffectedRangeMgr.computeAffectedLines(AnnotatedLines); + tooling::Replacements Result; + separateIncludes(AnnotatedLines, Result, Tokens); + return {Result, 0}; +} + +void IncludesSeparator::separateIncludes( + SmallVectorImpl &Lines, tooling::Replacements &Result, + FormatTokenLexer &Tokens) { + const unsigned NewlineCount = + std::min(Style.MaxEmptyLinesToKeep, *Style.EmptyLinesAfterIncludes) + 1; + WhitespaceManager Whitespaces( + Env.getSourceManager(), Style, + Style.LineEnding > FormatStyle::LE_CRLF + ? WhitespaceManager::inputUsesCRLF( + Env.getSourceManager().getBufferData(Env.getFileID()), + Style.LineEnding == FormatStyle::LE_DeriveCRLF) + : Style.LineEnding == FormatStyle::LE_CRLF); + + bool InIncludeArea = false; + for (unsigned I = 0; I < Lines.size(); ++I) { + const auto &CurrentLine = *Lines[I]; + + if (InIncludeArea) { + if (CurrentLine.isInclude()) + continue; + + if (isConditionalCompilationStart(CurrentLine)) { + const bool CCWithOnlyIncludes = isCCOnlyWithIncludes(Lines, I); + I = getEndOfCCBlock(Lines, I); + + // Conditional compilation blocks that only contain + // include statements are considered part of the include area. + if (CCWithOnlyIncludes) + continue; + } + + if (!CurrentLine.First->is(tok::eof) && CurrentLine.Affected) { + Whitespaces.replaceWhitespace(*CurrentLine.First, NewlineCount, + CurrentLine.First->OriginalColumn, + CurrentLine.First->OriginalColumn); + } + InIncludeArea = false; + } else { + if (CurrentLine.isInclude()) + InIncludeArea = true; + } + } + + for (const auto &R : Whitespaces.generateReplacements()) { + // The add method returns an Error instance which simulates program exit + // code through overloading boolean operator, thus false here indicates + // success. + if (Result.add(R)) + return; + } +} +} // namespace format +} // namespace clang diff --git a/clang/lib/Format/IncludesSeparator.h b/clang/lib/Format/IncludesSeparator.h new file mode 100644 index 0000000000000..d093e24dbf9d4 --- /dev/null +++ b/clang/lib/Format/IncludesSeparator.h @@ -0,0 +1,42 @@ +//===--- IncludesSeparator.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 IncludesSeparator, a TokenAnalyzer that inserts +/// new lines or removes empty lines after an includes area. +/// An includes area is a list of consecutive include statements. +/// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_LIB_FORMAT_INCLUDESSEPARATOR_H +#define LLVM_CLANG_LIB_FORMAT_INCLUDESSEPARATOR_H + +#include "TokenAnalyzer.h" +#include "WhitespaceManager.h" + +namespace clang { +namespace format { +class IncludesSeparator : public TokenAnalyzer { +public: + IncludesSeparator(const Environment &Env, const FormatStyle &Style) + : TokenAnalyzer(Env, Style) {} + + std::pair + analyze(TokenAnnotator &Annotator, + SmallVectorImpl &AnnotatedLines, + FormatTokenLexer &Tokens) override; + +private: + void separateIncludes(SmallVectorImpl &Lines, + tooling::Replacements &Result, + FormatTokenLexer &Tokens); +}; +} // namespace format +} // namespace clang + +#endif diff --git a/clang/lib/Format/TokenAnnotator.h b/clang/lib/Format/TokenAnnotator.h index 05a6daa87d803..06486799ec403 100644 --- a/clang/lib/Format/TokenAnnotator.h +++ b/clang/lib/Format/TokenAnnotator.h @@ -113,6 +113,14 @@ class AnnotatedLine { return First && First->is(tok::comment) && !First->getNextNonComment(); } + bool isInclude() const { + if (!First) + return false; + + const auto *NextToken = First->getNextNonComment(); + return First->is(tok::hash) && NextToken && NextToken->is(tok::pp_include); + } + /// \c true if this line starts with the given tokens in order, ignoring /// comments. template bool startsWith(Ts... Tokens) const { diff --git a/clang/unittests/Format/ConfigParseTest.cpp b/clang/unittests/Format/ConfigParseTest.cpp index 2a8d79359a49b..cbb442203b162 100644 --- a/clang/unittests/Format/ConfigParseTest.cpp +++ b/clang/unittests/Format/ConfigParseTest.cpp @@ -1005,6 +1005,8 @@ TEST(ConfigParseTest, ParsesConfiguration) { FormatStyle::SDS_Leave); CHECK_PARSE("SeparateDefinitionBlocks: Never", SeparateDefinitionBlocks, FormatStyle::SDS_Never); + + CHECK_PARSE("EmptyLinesAfterIncludes: 2", EmptyLinesAfterIncludes, 2); } TEST(ConfigParseTest, ParsesConfigurationWithLanguages) { diff --git a/clang/unittests/Format/FormatTest.cpp b/clang/unittests/Format/FormatTest.cpp index 3fb55ae2c1f41..c2a280f003c70 100644 --- a/clang/unittests/Format/FormatTest.cpp +++ b/clang/unittests/Format/FormatTest.cpp @@ -26990,6 +26990,191 @@ TEST_F(FormatTest, BreakAdjacentStringLiterals) { Style.BreakAdjacentStringLiterals = false; verifyFormat(Code, Style); } + +TEST_F(FormatTest, EmptyLinesAfterInclude) { + auto Style = getLLVMStyle(); + Style.EmptyLinesAfterIncludes = 2; + Style.MaxEmptyLinesToKeep = 2; + + verifyFormat("#include \n" + "\n" + "\n" + "class Test {};", + Style); + + verifyFormat("#ifndef TEST_H\n" + "#define TEST_H\n" + "#include \n" + "\n" + "\n" + "#define PP_DEFINE\n" + "#include \n" + "#include \n" + "\n" + "\n" + "class Test {};", + Style); + + verifyFormat("#include \n" + "#ifdef WINDOWS\n" + "#include \n" + "#ifdef X32\n" + "#include \n" + "#else\n" + "#include \n" + "#endif\n" + "#endif\n" + "#include \n" + "\n" + "\n" + "class Test {};", + Style); + + verifyFormat("#pragma once\n" + "#include \n" + "#include \n" + "#ifdef WINDOWS\n" + "#include \n" + "#endif\n" + "#include \n" + "\n" + "\n" + "class Test {};", + Style); + + verifyFormat("#include \n" + "\n" + "\n" + "#ifdef WINDOWS\n" + "#define OS_VERSION WINDOWS\n" + "#endif\n" + "\n" + "#include \n" + "#include \n" + "\n" + "\n" + "class Test {};", + Style); + + verifyFormat("#include \n" + "#include \n" + "\n" + "\n" + "#define INCLUDE_MACRO #include #include\n" + "#include \n" + "\n" + "\n" + "class Test {};", + Style); + + verifyFormat("#include \n" + "\n" + "\n" + "#ifdef WINDOWS\n" + "#ifdef x86\n" + "#include \n" + "#endif\n" + "#define OS_VERSION WINDOWS\n" + "#endif\n" + "\n" + "#include \n" + "#include \n" + "\n" + "\n" + "class Test {};", + Style); + + Style.EmptyLinesAfterIncludes = 1; + verifyFormat("#include \n" + "\n" + "class Test {};", + Style); + + verifyFormat("#pragma once\n" + "#include \n" + "\n" + "class Test {};", + Style); + + verifyFormat("#ifndef TEST_H\n" + "#define TEST_H\n" + "#include \n" + "#include \n" + "\n" + "void func();\n" + "#endif // TEST_H", + Style); +} + +TEST_F(FormatTest, EmptyLinesAfterIncludesLimitedByMaxEmptyLinesToKeep) { + auto Style = getLLVMStyle(); + Style.EmptyLinesAfterIncludes = 2; + Style.MaxEmptyLinesToKeep = 1; + verifyFormat("#include \n" + "\n" + "class Test {};", + Style); +} + +TEST_F(FormatTest, EmptyLinesAfterIncludesWithIncludesPreseve) { + auto Style = getLLVMStyle(); + Style.EmptyLinesAfterIncludes = 2; + Style.MaxEmptyLinesToKeep = 2; + Style.IncludeStyle.IncludeBlocks = tooling::IncludeStyle::IBS_Preserve; + verifyFormat("#pragma once\n" + "// test file documentation\n" + "#include \"b.h\"\n" + "#include \"d.h\"\n" + "\n" + "#include \"a.h\"\n" + "#include \"c.h\"\n" + "#include \"e.h\"\n" + "\n" + "\n" + "class Test {};", + Style); +} + +TEST_F(FormatTest, EmptyLinesAfterIncludesWithIncludesMerge) { + auto Style = getLLVMStyle(); + Style.EmptyLinesAfterIncludes = 2; + Style.MaxEmptyLinesToKeep = 2; + Style.IncludeStyle.IncludeBlocks = tooling::IncludeStyle::IBS_Merge; + verifyFormat("#pragma once\n" + "// test file documentation\n" + "#include \"a.h\"\n" + "#include \"b.h\"\n" + "#include \"c.h\"\n" + "\n" + "\n" + "class Test {};", + Style); +} + +TEST_F(FormatTest, EmptyLinesAfterIncludesWithIncludesRegroup) { + auto Style = getLLVMStyle(); + Style.EmptyLinesAfterIncludes = 2; + Style.MaxEmptyLinesToKeep = 2; + Style.IncludeStyle.IncludeBlocks = tooling::IncludeStyle::IBS_Regroup; + verifyFormat("#pragma once\n" + "// test file documentation\n" + "#include \"a.h\"\n" + "#include \"c.h\"\n" + "\n" + "#include \n" + "#include \n" + "\n" + "\n" + "class Test {};", + Style); + + verifyFormat("#include \n" + "#include \n" + "\n" + "\n" + "class Test {};", + Style); +} } // namespace } // namespace test } // namespace format