https://github.com/seranu updated https://github.com/llvm/llvm-project/pull/78957
>From 501cd2230cc878189e2dde912bd659faf711a2db Mon Sep 17 00:00:00 2001 From: Serban Ungureanu <serban.ungure...@randstaddigital.com> Date: Sat, 20 Jan 2024 17:02:04 +0200 Subject: [PATCH] [clang-format] Add options to set number of empty lines after includes --- clang/docs/ClangFormatStyleOptions.rst | 19 +++ clang/docs/ReleaseNotes.rst | 1 + clang/include/clang/Format/Format.h | 18 ++ clang/lib/Format/CMakeLists.txt | 1 + clang/lib/Format/Format.cpp | 9 + clang/lib/Format/IncludesSeparator.cpp | 159 ++++++++++++++++++ clang/lib/Format/IncludesSeparator.h | 42 +++++ clang/lib/Format/TokenAnnotator.h | 8 + clang/unittests/Format/ConfigParseTest.cpp | 2 + clang/unittests/Format/FormatTest.cpp | 185 +++++++++++++++++++++ 10 files changed, 444 insertions(+) create mode 100644 clang/lib/Format/IncludesSeparator.cpp create mode 100644 clang/lib/Format/IncludesSeparator.h diff --git a/clang/docs/ClangFormatStyleOptions.rst b/clang/docs/ClangFormatStyleOptions.rst index 4dc0de3a90f2650..4ba38808cd5090a 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:`¶ <EmptyLinesAfterIncludes>` + 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 <string> #include <string> + #include <map> #include <map> + + class Test {}; + class Test {}; + .. _ExperimentalAutoDetectBinPacking: **ExperimentalAutoDetectBinPacking** (``Boolean``) :versionbadge:`clang-format 3.7` :ref:`¶ <ExperimentalAutoDetectBinPacking>` diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst index 4888ffe6f4dfc85..e1bf86c8a83b671 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 bc9eecd42f9ebfd..84d1a0b70b9efd2 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 <string> #include <string> + /// #include <map> #include <map> + /// + /// class Test {}; + /// class Test {}; + /// \endcode + /// \version 18 + std::optional<unsigned> 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 84a3c136f650a85..ff3860426407adc 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 ff326dc784783b2..f068da97e6dfbff 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<FormatStyle> { 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 000000000000000..0e74383c7c5b78f --- /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<clang::format::AnnotatedLine *> &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<clang::format::AnnotatedLine *> &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<tooling::Replacements, unsigned> +IncludesSeparator::analyze(TokenAnnotator &Annotator, + SmallVectorImpl<AnnotatedLine *> &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<AnnotatedLine *> &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 000000000000000..d093e24dbf9d412 --- /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<tooling::Replacements, unsigned> + analyze(TokenAnnotator &Annotator, + SmallVectorImpl<AnnotatedLine *> &AnnotatedLines, + FormatTokenLexer &Tokens) override; + +private: + void separateIncludes(SmallVectorImpl<AnnotatedLine *> &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 05a6daa87d80340..06486799ec4031a 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 <typename... Ts> bool startsWith(Ts... Tokens) const { diff --git a/clang/unittests/Format/ConfigParseTest.cpp b/clang/unittests/Format/ConfigParseTest.cpp index 2a8d79359a49b40..cbb442203b1628a 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 3fb55ae2c1f4137..c2a280f003c70f9 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 <string>\n" + "\n" + "\n" + "class Test {};", + Style); + + verifyFormat("#ifndef TEST_H\n" + "#define TEST_H\n" + "#include <string>\n" + "\n" + "\n" + "#define PP_DEFINE\n" + "#include <map>\n" + "#include <vector>\n" + "\n" + "\n" + "class Test {};", + Style); + + verifyFormat("#include <string>\n" + "#ifdef WINDOWS\n" + "#include <win32>\n" + "#ifdef X32\n" + "#include <additionalHeader>\n" + "#else\n" + "#include <unistd.h>\n" + "#endif\n" + "#endif\n" + "#include <map>\n" + "\n" + "\n" + "class Test {};", + Style); + + verifyFormat("#pragma once\n" + "#include <string>\n" + "#include <map>\n" + "#ifdef WINDOWS\n" + "#include <w32>\n" + "#endif\n" + "#include <vector>\n" + "\n" + "\n" + "class Test {};", + Style); + + verifyFormat("#include <string>\n" + "\n" + "\n" + "#ifdef WINDOWS\n" + "#define OS_VERSION WINDOWS\n" + "#endif\n" + "\n" + "#include <map>\n" + "#include <vector>\n" + "\n" + "\n" + "class Test {};", + Style); + + verifyFormat("#include <string>\n" + "#include <map>\n" + "\n" + "\n" + "#define INCLUDE_MACRO #include<vector> #include<set>\n" + "#include <queue>\n" + "\n" + "\n" + "class Test {};", + Style); + + verifyFormat("#include <string>\n" + "\n" + "\n" + "#ifdef WINDOWS\n" + "#ifdef x86\n" + "#include <x86_windows>\n" + "#endif\n" + "#define OS_VERSION WINDOWS\n" + "#endif\n" + "\n" + "#include <map>\n" + "#include <vector>\n" + "\n" + "\n" + "class Test {};", + Style); + + Style.EmptyLinesAfterIncludes = 1; + verifyFormat("#include <string>\n" + "\n" + "class Test {};", + Style); + + verifyFormat("#pragma once\n" + "#include <string>\n" + "\n" + "class Test {};", + Style); + + verifyFormat("#ifndef TEST_H\n" + "#define TEST_H\n" + "#include <string>\n" + "#include <map>\n" + "\n" + "void func();\n" + "#endif // TEST_H", + Style); +} + +TEST_F(FormatTest, EmptyLinesAfterIncludesLimitedByMaxEmptyLinesToKeep) { + auto Style = getLLVMStyle(); + Style.EmptyLinesAfterIncludes = 2; + Style.MaxEmptyLinesToKeep = 1; + verifyFormat("#include <string>\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 <b.h>\n" + "#include <d.h>\n" + "\n" + "\n" + "class Test {};", + Style); + + verifyFormat("#include <b.h>\n" + "#include <d.h>\n" + "\n" + "\n" + "class Test {};", + Style); +} } // namespace } // namespace test } // namespace format _______________________________________________ cfe-commits mailing list cfe-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits