diff --git a/clang/docs/ClangFormatStyleOptions.rst b/clang/docs/ClangFormatStyleOptions.rst index b746df5dab264..7ade17714aa3a 100644 --- a/clang/docs/ClangFormatStyleOptions.rst +++ b/clang/docs/ClangFormatStyleOptions.rst @@ -6612,6 +6612,38 @@ the configuration (without a prefix: ``Auto``). int a [5]; vs. int a[5]; int a [5][5]; vs. int a[5][5]; +.. _SpaceInComments: + +**SpaceInComments** (``Struct``) :versionbadge:`clang-format 21` :ref:`¶ ` + Controls whitespace around block comment delimiters and parameter-style + inline comments. Each field accepts a ``CommentSpaceMode``: ``Leave`` + (preserve existing spacing, the default), ``Always`` (insert a single space), + or ``Never`` (remove all spaces). + + The available controls are: + + ``AfterOpeningComment`` + Governs the space immediately after ``/*`` in regular block comments. + ``BeforeClosingComment`` + Governs the space before ``*/`` in regular block comments. + ``AfterOpeningParamComment`` + Governs the space after ``/*`` in parameter comments such as ``/*param=*/``. + ``BeforeClosingParamComment`` + Governs the space before ``*/`` in parameter comments. + + .. code-block:: yaml + + SpaceInComments: + BeforeClosingComment: Always + BeforeClosingParamComment: Never + + .. code-block:: c++ + + // BeforeClosingComment: Always + auto Value = foo(/* comment */); + // BeforeClosingParamComment: Never + auto Number = foo(/*param=*/42); + .. _SpaceInEmptyBlock: **SpaceInEmptyBlock** (``Boolean``) :versionbadge:`clang-format 10` :ref:`¶ ` diff --git a/clang/include/clang/Format/Format.h b/clang/include/clang/Format/Format.h index 3df5b92654094..ddee11d7fffb0 100644 --- a/clang/include/clang/Format/Format.h +++ b/clang/include/clang/Format/Format.h @@ -4684,6 +4684,36 @@ struct FormatStyle { /// \version 17 bool SpaceBeforeJsonColon; + /// Defines how clang-format should treat spaces around block comment + /// delimiters and specialized inline comments (such as parameter name + /// annotations). The default `Leave` mode preserves existing whitespace. + enum class CommentSpaceMode : int8_t { + /// Preserve existing whitespace, making no formatting changes. + Leave, + /// Ensure exactly one space is present. + Always, + /// Ensure no space is present. + Never, + }; + + /// Specifies spacing behavior for different block comment forms. + struct SpaceInCommentsOptions { + CommentSpaceMode AfterOpeningComment = CommentSpaceMode::Leave; + CommentSpaceMode BeforeClosingComment = CommentSpaceMode::Leave; + CommentSpaceMode AfterOpeningParamComment = CommentSpaceMode::Leave; + CommentSpaceMode BeforeClosingParamComment = CommentSpaceMode::Leave; + + constexpr bool operator==(const SpaceInCommentsOptions &R) const { + return AfterOpeningComment == R.AfterOpeningComment && + BeforeClosingComment == R.BeforeClosingComment && + AfterOpeningParamComment == R.AfterOpeningParamComment && + BeforeClosingParamComment == R.BeforeClosingParamComment; + } + }; + + /// Controls spacing inside block comments. + SpaceInCommentsOptions SpaceInComments; + /// Different ways to put a space before opening parentheses. enum SpaceBeforeParensStyle : int8_t { /// This is **deprecated** and replaced by ``Custom`` below, with all @@ -5612,6 +5642,7 @@ struct FormatStyle { SpaceBeforeRangeBasedForLoopColon == R.SpaceBeforeRangeBasedForLoopColon && SpaceBeforeSquareBrackets == R.SpaceBeforeSquareBrackets && + SpaceInComments == R.SpaceInComments && SpaceInEmptyBraces == R.SpaceInEmptyBraces && SpacesBeforeTrailingComments == R.SpacesBeforeTrailingComments && SpacesInAngles == R.SpacesInAngles && diff --git a/clang/lib/Format/BreakableToken.cpp b/clang/lib/Format/BreakableToken.cpp index 29db20067c361..6314a9feb0a2c 100644 --- a/clang/lib/Format/BreakableToken.cpp +++ b/clang/lib/Format/BreakableToken.cpp @@ -19,6 +19,7 @@ #include "llvm/ADT/STLExtras.h" #include "llvm/Support/Debug.h" #include +#include #define DEBUG_TYPE "format-token-breaker" @@ -26,6 +27,218 @@ namespace clang { namespace format { static constexpr StringRef Blanks(" \t\v\f\r"); +static constexpr size_t BlockCommentOpenerLength = 2; +static constexpr size_t BlockCommentCloserLength = 2; + +namespace { + +bool isWellFormedBlockCommentText(StringRef Text) { + return Text.size() >= BlockCommentOpenerLength + BlockCommentCloserLength && + Text.starts_with("/*") && Text.ends_with("*/"); +} + +int countTrailingSpaces(StringRef Text) { + const size_t TrimmedSize = Text.rtrim(Blanks).size(); + return static_cast(Text.size() - TrimmedSize); +} + +FormatStyle::CommentSpaceMode resolveCommentSpaceMode( + const FormatStyle &Style, const FormatToken &Tok, + FormatStyle::CommentSpaceMode FormatStyle::SpaceInCommentsOptions::*const + GeneralField, + FormatStyle::CommentSpaceMode FormatStyle::SpaceInCommentsOptions::*const + ParamOverrideField, + const bool ForceDocstringLeave) { + const FormatStyle::SpaceInCommentsOptions &Options = Style.SpaceInComments; + if (Tok.getBlockCommentKind() == CommentKind::Parameter) { + const FormatStyle::CommentSpaceMode Mode = Options.*ParamOverrideField; + if (Mode != FormatStyle::CommentSpaceMode::Leave) + return Mode; + } + if (ForceDocstringLeave && + Tok.getBlockCommentKind() == CommentKind::Docstring) { + return FormatStyle::CommentSpaceMode::Leave; + } + return Options.*GeneralField; +} + +StringRef getBlockCommentInterior(const FormatToken &Tok) { + const StringRef Text = Tok.TokenText; + if (!isWellFormedBlockCommentText(Text)) + return {}; + return Text.drop_front(BlockCommentOpenerLength) + .drop_back(BlockCommentCloserLength); +} + +} // namespace + +FormatStyle::CommentSpaceMode getAfterOpeningSpaceMode(const FormatStyle &Style, + const FormatToken &Tok) { + return resolveCommentSpaceMode( + Style, Tok, &FormatStyle::SpaceInCommentsOptions::AfterOpeningComment, + &FormatStyle::SpaceInCommentsOptions::AfterOpeningParamComment, + /*ForceDocstringLeave=*/true); +} + +StringRef getSliceAfterOpening(const FormatToken &Tok) { + if (!isWellFormedBlockCommentText(Tok.TokenText)) + return {}; + return Tok.TokenText.drop_front(BlockCommentOpenerLength); +} + +unsigned countLeadingHorizontalWhitespaceAfterOpening(const FormatToken &Tok) { + const StringRef Body = getSliceAfterOpening(Tok); + unsigned Count = 0; + while (Count < Body.size()) { + const char C = Body[Count]; + if (C == '\n' || C == '\r') + break; + if (!isHorizontalWhitespace(static_cast(C))) + break; + ++Count; + } + return Count; +} + +FormatStyle::CommentSpaceMode +getBeforeClosingSpaceMode(const FormatStyle &Style, const FormatToken &Tok) { + return resolveCommentSpaceMode( + Style, Tok, &FormatStyle::SpaceInCommentsOptions::BeforeClosingComment, + &FormatStyle::SpaceInCommentsOptions::BeforeClosingParamComment, + /*ForceDocstringLeave=*/false); +} + +unsigned +countTrailingHorizontalWhitespaceBeforeClosing(const FormatToken &Tok) { + const StringRef Body = getBlockCommentInterior(Tok); + size_t Pos = Body.size(); + while (Pos > 0) { + const char C = Body[Pos - 1]; + if (C == '\n' || C == '\r') { + --Pos; + continue; + } + break; + } + + size_t Tail = Pos; + while (Tail > 0 && + isHorizontalWhitespace(static_cast(Body[Tail - 1]))) { + --Tail; + } + return Pos - Tail; +} + +static unsigned countLogicalNewlines(StringRef Text) { + unsigned Count = 0; + const size_t End = Text.size(); + for (size_t I = 0; I < End; ++I) { + if (Text[I] == '\r') { + ++Count; + if (I + 1 < End && Text[I + 1] == '\n') + ++I; + } else if (Text[I] == '\n') { + ++Count; + } + } + return Count; +} + +void applyAfterOpeningBlockCommentSpacing(const FormatStyle &Style, + const FormatToken &Tok, + WhitespaceManager &Whitespaces, + bool InPPDirective) { + using CommentSpaceMode = FormatStyle::CommentSpaceMode; + if (!Tok.is(tok::comment) || !isWellFormedBlockCommentText(Tok.TokenText)) + return; + + const CommentSpaceMode Mode = getAfterOpeningSpaceMode(Style, Tok); + if (Mode == CommentSpaceMode::Leave) + return; + + const StringRef Body = getSliceAfterOpening(Tok); + if (Body.empty()) + return; + + const StringRef Interior = getBlockCommentInterior(Tok); + + const bool OnlyWhitespace = + Interior.find_first_not_of(" \t\r\n") == StringRef::npos; + const unsigned OpeningOffset = Tok.TokenText.size() - Body.size(); + const unsigned LeadingSpaces = + countLeadingHorizontalWhitespaceAfterOpening(Tok); + + if (Mode == CommentSpaceMode::Always && OnlyWhitespace && !Interior.empty()) { + const unsigned ReplaceChars = Interior.size(); + if (Interior.contains('\n')) { + unsigned NewlineCount = countLogicalNewlines(Interior); + if (NewlineCount == 0) + NewlineCount = 1; + Whitespaces.replaceWhitespaceInToken( + Tok, OpeningOffset, ReplaceChars, /*PreviousPostfix=*/"", + /*CurrentPrefix=*/"", InPPDirective, /*Newlines=*/NewlineCount, + /*Spaces=*/0); + } else { + Whitespaces.replaceWhitespaceInToken( + Tok, OpeningOffset, ReplaceChars, /*PreviousPostfix=*/"", + /*CurrentPrefix=*/" ", InPPDirective, /*Newlines=*/0, + /*Spaces=*/0); + } + return; + } + + if (Mode == CommentSpaceMode::Never) { + if (LeadingSpaces == 0) + return; + Whitespaces.replaceWhitespaceInToken( + Tok, OpeningOffset, LeadingSpaces, /*PreviousPostfix=*/"", + /*CurrentPrefix=*/"", InPPDirective, /*Newlines=*/0, /*Spaces=*/0); + return; + } + + assert(Mode == CommentSpaceMode::Always && "Unexpected CommentSpaceMode"); + if (LeadingSpaces == 1) + return; + Whitespaces.replaceWhitespaceInToken( + Tok, OpeningOffset, LeadingSpaces, /*PreviousPostfix=*/"", + /*CurrentPrefix=*/" ", InPPDirective, /*Newlines=*/0, /*Spaces=*/0); +} + +void applyBeforeClosingBlockCommentSpacing(const FormatStyle &Style, + const FormatToken &Tok, + WhitespaceManager &Whitespaces, + bool InPPDirective) { + using CommentSpaceMode = FormatStyle::CommentSpaceMode; + if (!Tok.is(tok::comment) || !isWellFormedBlockCommentText(Tok.TokenText)) + return; + + const CommentSpaceMode Mode = getBeforeClosingSpaceMode(Style, Tok); + if (Mode == CommentSpaceMode::Leave) + return; + + const StringRef Text = Tok.TokenText; + + const unsigned TrailingSpaces = + countTrailingHorizontalWhitespaceBeforeClosing(Tok); + const unsigned ReplaceOffset = + Text.size() - BlockCommentCloserLength - TrailingSpaces; + + if (Mode == CommentSpaceMode::Never) { + if (TrailingSpaces == 0) + return; + Whitespaces.replaceWhitespaceInToken( + Tok, ReplaceOffset, TrailingSpaces, /*PreviousPostfix=*/"", + /*CurrentPrefix=*/"", InPPDirective, /*Newlines=*/0, /*Spaces=*/0); + return; + } + + assert(Mode == CommentSpaceMode::Always && "Unexpected CommentSpaceMode"); + if (TrailingSpaces == 1) + return; + Whitespaces.replaceWhitespaceInToken( + Tok, ReplaceOffset, TrailingSpaces, /*PreviousPostfix=*/"", + /*CurrentPrefix=*/" ", InPPDirective, /*Newlines=*/0, /*Spaces=*/0); +} static StringRef getLineCommentIndentPrefix(StringRef Comment, const FormatStyle &Style) { @@ -475,7 +688,7 @@ BreakableBlockComment::BreakableBlockComment( "block comment section must start with a block comment"); StringRef TokenText(Tok.TokenText); - assert(TokenText.starts_with("/*") && TokenText.ends_with("*/")); + assert(isWellFormedBlockCommentText(Tok.TokenText)); TokenText.substr(2, TokenText.size() - 4) .split(Lines, UseCRLF ? "\r\n" : "\n"); @@ -775,50 +988,206 @@ void BreakableBlockComment::reflow(unsigned LineIndex, void BreakableBlockComment::adaptStartOfLine( unsigned LineIndex, WhitespaceManager &Whitespaces) const { + const FormatStyle::CommentSpaceMode BeforeClosingMode = + getBeforeClosingSpaceMode(Style, Tok); + + if (isSingleLineBlockComment()) { + if (LineIndex == 0) + adaptSingleLineComment(Whitespaces, BeforeClosingMode); + return; + } + if (LineIndex == 0) { - if (DelimitersOnNewline) { - // Since we're breaking at index 1 below, the break position and the - // break length are the same. - // Note: this works because getCommentSplit is careful never to split at - // the beginning of a line. - size_t BreakLength = Lines[0].substr(1).find_first_not_of(Blanks); - if (BreakLength != StringRef::npos) { - insertBreak(LineIndex, 0, Split(1, BreakLength), /*ContentIndent=*/0, - Whitespaces); - } - } + adaptFirstLineOfMultiLineComment(Whitespaces, BeforeClosingMode); return; } - // Here no reflow with the previous line will happen. - // Fix the decoration of the line at LineIndex. - StringRef Prefix = Decoration; - if (Content[LineIndex].empty()) { - if (LineIndex + 1 == Lines.size()) { - if (!LastLineNeedsDecoration) { - // If the last line was empty, we don't need a prefix, as the */ will - // line up with the decoration (if it exists). - Prefix = ""; - } - } else if (!Decoration.empty()) { - // For other empty lines, if we do have a decoration, adapt it to not - // contain a trailing whitespace. - Prefix = Prefix.substr(0, 1); + + adaptIntermediateLineOfComment(LineIndex, Whitespaces, BeforeClosingMode); +} + +void BreakableBlockComment::adaptSingleLineComment( + WhitespaceManager &Whitespaces, + FormatStyle::CommentSpaceMode BeforeClosingMode) const { + applyAfterOpeningBlockCommentSpacing(Style, Tok, Whitespaces, InPPDirective); + applyBeforeClosingBlockCommentSpacing(Style, Tok, Whitespaces, InPPDirective); + + if (BeforeClosingMode == FormatStyle::CommentSpaceMode::Always && + isWhitespaceOnlySingleLineBlockComment()) { + formatWhitespaceOnlySingleLineBlockComment(Whitespaces); + } +} + +void BreakableBlockComment::adaptFirstLineOfMultiLineComment( + WhitespaceManager &Whitespaces, + FormatStyle::CommentSpaceMode BeforeClosingMode) const { + applyAfterOpeningBlockCommentSpacing(Style, Tok, Whitespaces, InPPDirective); + + if (BeforeClosingMode == FormatStyle::CommentSpaceMode::Always) { + const bool TerminatorOnSeparateLine = + !Lines.empty() && Lines.back().ltrim(Blanks).empty(); + + if (!TerminatorOnSeparateLine && isWellFormedBlockComment() && + Tok.NeedsSpaceBeforeClosingBlockComment && + Tok.SpaceBeforeClosingBlockCommentOffset < Tok.TokenText.size()) { + Whitespaces.replaceWhitespaceInToken( + Tok, Tok.SpaceBeforeClosingBlockCommentOffset, + /*ReplaceChars=*/0, /*PreviousPostfix=*/"", /*CurrentPrefix=*/" ", + InPPDirective, /*Newlines=*/0, /*Spaces=*/0); } - } else if (ContentColumn[LineIndex] == 1) { - // This line starts immediately after the decorating *. - Prefix = Prefix.substr(0, 1); } - // This is the offset of the end of the last line relative to the start of the - // token text in the token. - unsigned WhitespaceOffsetInToken = Content[LineIndex - 1].data() + - Content[LineIndex - 1].size() - - tokenAt(LineIndex).TokenText.data(); - unsigned WhitespaceLength = Content[LineIndex].data() - - tokenAt(LineIndex).TokenText.data() - - WhitespaceOffsetInToken; + + if (DelimitersOnNewline) { + // Since we're breaking at index 1 below, the break position and the break + // length are the same. + // Note: this works because getCommentSplit is careful never to split at the + // beginning of a line. + size_t LeadingWhitespaceLength = + Lines[0].substr(1).find_first_not_of(Blanks); + if (LeadingWhitespaceLength != StringRef::npos) { + insertBreak(/*LineIndex=*/0, /*TailOffset=*/0, + Split(/*First=*/1, LeadingWhitespaceLength), + /*ContentIndent=*/0, Whitespaces); + } + } +} + +void BreakableBlockComment::adaptIntermediateLineOfComment( + unsigned LineIndex, WhitespaceManager &Whitespaces, + FormatStyle::CommentSpaceMode BeforeClosingMode) const { + assert(LineIndex > 0 && "First line should be handled separately."); + + CommentLineInfo LineInfo; + LineInfo.IsEmpty = Content[LineIndex].empty(); + LineInfo.IsLastLine = (LineIndex + 1 == Lines.size()); + LineInfo.LastLineNeedsDecoration = LastLineNeedsDecoration; + LineInfo.StartsImmediatelyAfterDecoration = (ContentColumn[LineIndex] == 1); + LineInfo.Decoration = Decoration; + + const StringRef Prefix = calculateLinePrefix(LineInfo); + const InterLineWhitespace Whitespace = + calculateInterLineWhitespace(LineIndex); + int Spaces = ContentColumn[LineIndex] - static_cast(Prefix.size()); + const bool IsTerminatorOnSeparateLine = + Content[LineIndex].ltrim(Blanks).empty(); + + if (LineInfo.IsLastLine && IsTerminatorOnSeparateLine) { + Spaces = + calculateTerminatorIndent(LineIndex, Prefix, BeforeClosingMode, Spaces); + } + + Whitespaces.replaceWhitespaceInToken(tokenAt(LineIndex), Whitespace.Offset, + Whitespace.Length, "", Prefix, + InPPDirective, /*Newlines=*/1, Spaces); +} + +StringRef +BreakableBlockComment::calculateLinePrefix(const CommentLineInfo &Info) const { + if (Info.IsEmpty) { + if (Info.IsLastLine) + return Info.LastLineNeedsDecoration ? Info.Decoration : StringRef(); + // For empty lines other than the last one, we only want the star, + // not the trailing space of the decoration. + if (!Info.Decoration.empty()) + return Info.Decoration.substr(0, 1); + return {}; + } + + // If the content starts immediately after the decoration, it means the user + // has not put a space after the star. In that case, we should probably not + // add one either. We only add the star. + if (Info.StartsImmediatelyAfterDecoration && !Info.Decoration.empty()) + return Info.Decoration.substr(0, 1); + + return Info.Decoration; +} + +BreakableBlockComment::InterLineWhitespace +BreakableBlockComment::calculateInterLineWhitespace(unsigned LineIndex) const { + assert(LineIndex > 0 && "Expected a previous line to exist."); + const StringRef TokenText = tokenAt(LineIndex).TokenText; + const StringRef Previous = Content[LineIndex - 1]; + const StringRef Current = Content[LineIndex]; + + const auto TokenBegin = TokenText.begin(); + const auto TokenEnd = TokenText.end(); + const auto PreviousEnd = Previous.end(); + const auto CurrentBegin = Current.begin(); + + assert(TokenBegin <= PreviousEnd && PreviousEnd <= TokenEnd && + "Previous line must be within the token text."); + assert(TokenBegin <= CurrentBegin && CurrentBegin <= TokenEnd && + "Current line must be within the token text."); + + InterLineWhitespace Result; + Result.Offset = static_cast(std::distance(TokenBegin, PreviousEnd)); + Result.Length = + static_cast(std::distance(PreviousEnd, CurrentBegin)); + return Result; +} + +bool BreakableBlockComment::allPreviousLinesEmpty(unsigned LineIndex) const { + assert(LineIndex > 0 && "First line has no predecessors."); + return std::all_of(Content.begin(), Content.begin() + LineIndex, + [](StringRef Line) { return Line.trim(Blanks).empty(); }); +} + +bool BreakableBlockComment::isWellFormedBlockComment() const { + return isWellFormedBlockCommentText(Tok.TokenText); +} + +bool BreakableBlockComment::isSingleLineBlockComment() const { + // Treat long single-line JSDoc/JavaDoc (that request delimiter newlines) + // as multi-line to trigger the opening break after "/**". + return isWellFormedBlockComment() && Lines.size() == 1 && + !DelimitersOnNewline; +} + +bool BreakableBlockComment::isWhitespaceOnlySingleLineBlockComment() const { + if (!isSingleLineBlockComment()) + return false; + + const StringRef Body = getBlockCommentInterior(Tok); + return Body.trim(" \t").empty(); +} + +void BreakableBlockComment::formatWhitespaceOnlySingleLineBlockComment( + WhitespaceManager &Whitespaces) const { + const StringRef Body = getBlockCommentInterior(Tok); + const unsigned WhitespaceLength = Body.size(); Whitespaces.replaceWhitespaceInToken( - tokenAt(LineIndex), WhitespaceOffsetInToken, WhitespaceLength, "", Prefix, - InPPDirective, /*Newlines=*/1, ContentColumn[LineIndex] - Prefix.size()); + Tok, /*Offset=*/BlockCommentOpenerLength, WhitespaceLength, + /*PreviousPostfix=*/"", /*CurrentPrefix=*/"", InPPDirective, + /*Newlines=*/1, /*Spaces=*/0); +} + +int BreakableBlockComment::calculateTerminatorIndent( + unsigned LineIndex, StringRef Prefix, FormatStyle::CommentSpaceMode Mode, + int BaseSpaces) const { + switch (Mode) { + case FormatStyle::CommentSpaceMode::Leave: + return BaseSpaces; + case FormatStyle::CommentSpaceMode::Never: + return 0; + case FormatStyle::CommentSpaceMode::Always: + break; + } + + assert(Mode == FormatStyle::CommentSpaceMode::Always); + + if (!Tok.NeedsSpaceBeforeClosingBlockComment) { + if (allPreviousLinesEmpty(LineIndex)) + return 0; + return BaseSpaces; + } + + if (BaseSpaces <= 0) + return allPreviousLinesEmpty(LineIndex) ? 0 : 1; + + const int TrailingSpacesInPrefix = countTrailingSpaces(Prefix); + if (TrailingSpacesInPrefix == 0) + return BaseSpaces; + + return std::max(0, BaseSpaces - TrailingSpacesInPrefix); } BreakableToken::Split diff --git a/clang/lib/Format/BreakableToken.h b/clang/lib/Format/BreakableToken.h index 45c00b35fd01e..182a856306e5a 100644 --- a/clang/lib/Format/BreakableToken.h +++ b/clang/lib/Format/BreakableToken.h @@ -19,11 +19,34 @@ #include "Encoding.h" #include "WhitespaceManager.h" +#include "llvm/ADT/StringRef.h" #include "llvm/ADT/StringSet.h" namespace clang { namespace format { +FormatStyle::CommentSpaceMode +getBeforeClosingSpaceMode(const FormatStyle &Style, const FormatToken &Tok); + +FormatStyle::CommentSpaceMode getAfterOpeningSpaceMode(const FormatStyle &Style, + const FormatToken &Tok); + +llvm::StringRef getBlockCommentBody(const FormatToken &Tok); + +unsigned countLeadingHorizontalWhitespaceAfterOpening(const FormatToken &Tok); + +unsigned countTrailingHorizontalWhitespaceBeforeClosing(const FormatToken &Tok); + +void applyAfterOpeningBlockCommentSpacing(const FormatStyle &Style, + const FormatToken &Tok, + WhitespaceManager &Whitespaces, + bool InPPDirective); + +void applyBeforeClosingBlockCommentSpacing(const FormatStyle &Style, + const FormatToken &Tok, + WhitespaceManager &Whitespaces, + bool InPPDirective); + /// Checks if \p Token switches formatting, like /* clang-format off */. /// \p Token must be a comment. bool switchesFormatting(const FormatToken &Token); @@ -434,6 +457,40 @@ class BreakableBlockComment : public BreakableComment { static const llvm::StringSet<> ContentIndentingJavadocAnnotations; private: + struct CommentLineInfo { + bool IsEmpty = false; + bool IsLastLine = false; + bool LastLineNeedsDecoration = false; + bool StartsImmediatelyAfterDecoration = false; + StringRef Decoration; + }; + + struct InterLineWhitespace { + unsigned Offset = 0; + unsigned Length = 0; + }; + + void + adaptSingleLineComment(WhitespaceManager &Whitespaces, + FormatStyle::CommentSpaceMode BeforeClosingMode) const; + void adaptFirstLineOfMultiLineComment( + WhitespaceManager &Whitespaces, + FormatStyle::CommentSpaceMode BeforeClosingMode) const; + void adaptIntermediateLineOfComment( + unsigned LineIndex, WhitespaceManager &Whitespaces, + FormatStyle::CommentSpaceMode BeforeClosingMode) const; + StringRef calculateLinePrefix(const CommentLineInfo &Info) const; + InterLineWhitespace calculateInterLineWhitespace(unsigned LineIndex) const; + bool allPreviousLinesEmpty(unsigned LineIndex) const; + bool isWellFormedBlockComment() const; + bool isSingleLineBlockComment() const; + bool isWhitespaceOnlySingleLineBlockComment() const; + void formatWhitespaceOnlySingleLineBlockComment( + WhitespaceManager &Whitespaces) const; + int calculateTerminatorIndent(unsigned LineIndex, StringRef Prefix, + FormatStyle::CommentSpaceMode Mode, + int BaseSpaces) const; + // Rearranges the whitespace between Lines[LineIndex-1] and Lines[LineIndex]. // // Updates Content[LineIndex-1] and Content[LineIndex] by stripping off diff --git a/clang/lib/Format/Format.cpp b/clang/lib/Format/Format.cpp index 686e54128d372..813201708e528 100644 --- a/clang/lib/Format/Format.cpp +++ b/clang/lib/Format/Format.cpp @@ -44,6 +44,14 @@ struct ScalarEnumerationTraits { } }; +template <> struct ScalarEnumerationTraits { + static void enumeration(IO &IO, FormatStyle::CommentSpaceMode &Value) { + IO.enumCase(Value, "Leave", FormatStyle::CommentSpaceMode::Leave); + IO.enumCase(Value, "Always", FormatStyle::CommentSpaceMode::Always); + IO.enumCase(Value, "Never", FormatStyle::CommentSpaceMode::Never); + } +}; + template <> struct MappingTraits { static void enumInput(IO &IO, FormatStyle::AlignConsecutiveStyle &Value) { IO.enumCase(Value, "None", FormatStyle::AlignConsecutiveStyle({})); @@ -94,6 +102,16 @@ template <> struct MappingTraits { } }; +template <> struct MappingTraits { + static void mapping(IO &IO, FormatStyle::SpaceInCommentsOptions &Value) { + IO.mapOptional("AfterOpeningComment", Value.AfterOpeningComment); + IO.mapOptional("BeforeClosingComment", Value.BeforeClosingComment); + IO.mapOptional("AfterOpeningParamComment", Value.AfterOpeningParamComment); + IO.mapOptional("BeforeClosingParamComment", + Value.BeforeClosingParamComment); + } +}; + template <> struct MappingTraits { static void mapping(IO &IO, @@ -1228,6 +1246,7 @@ template <> struct MappingTraits { Style.SpaceBeforeRangeBasedForLoopColon); IO.mapOptional("SpaceBeforeSquareBrackets", Style.SpaceBeforeSquareBrackets); + IO.mapOptional("SpaceInComments", Style.SpaceInComments); IO.mapOptional("SpaceInEmptyBraces", Style.SpaceInEmptyBraces); IO.mapOptional("SpacesBeforeTrailingComments", Style.SpacesBeforeTrailingComments); diff --git a/clang/lib/Format/FormatToken.h b/clang/lib/Format/FormatToken.h index f015d27bed6af..8ff8ad5bdb507 100644 --- a/clang/lib/Format/FormatToken.h +++ b/clang/lib/Format/FormatToken.h @@ -315,6 +315,19 @@ class AnnotatedLine; /// A wrapper around a \c Token storing information about the /// whitespace characters preceding it. + +// Describes the kind of a block comment. +enum class CommentKind { + // A plain comment, i.e. /* ... */. + Plain, + // A comment that starts with /*! or /**. + Docstring, + // A comment that looks like a parameter, e.g. /*in*/. + Parameter, + // A comment that is a sentinel, e.g. /*FALLTHROUGH*/. + Sentinel, +}; + struct FormatToken { FormatToken() : HasUnescapedNewline(false), IsMultiline(false), IsFirst(false), @@ -324,9 +337,10 @@ struct FormatToken { EndsBinaryExpression(false), PartOfMultiVariableDeclStmt(false), ContinuesLineCommentSection(false), Finalized(false), ClosesRequiresClause(false), EndsCppAttributeGroup(false), - BlockKind(BK_Unknown), Decision(FD_Unformatted), - PackingKind(PPK_Inconclusive), TypeIsFinalized(false), - Type(TT_Unknown) {} + NeedsSpaceBeforeClosingBlockComment(false), BlockKind(BK_Unknown), + BlockCommentKind(static_cast(CommentKind::Plain)), + Decision(FD_Unformatted), PackingKind(PPK_Inconclusive), + TypeIsFinalized(false), Type(TT_Unknown) {} /// The \c Token. Token Tok; @@ -402,10 +416,16 @@ struct FormatToken { /// \c true if this token ends a group of C++ attributes. unsigned EndsCppAttributeGroup : 1; + /// \c true if clang-format should insert a space before the closing '*/'. + unsigned NeedsSpaceBeforeClosingBlockComment : 1; + private: /// Contains the kind of block if this token is a brace. unsigned BlockKind : 2; + /// Kind of block comment. + unsigned BlockCommentKind : 2; + public: BraceBlockKind getBlockKind() const { return static_cast(BlockKind); @@ -415,6 +435,14 @@ struct FormatToken { assert(getBlockKind() == BBK && "BraceBlockKind overflow!"); } + CommentKind getBlockCommentKind() const { + return static_cast(BlockCommentKind); + } + void setBlockCommentKind(CommentKind Kind) { + BlockCommentKind = static_cast(Kind); + assert(getBlockCommentKind() == Kind && "CommentKind overflow!"); + } + private: /// Stores the formatting decision for the token once it was made. unsigned Decision : 2; @@ -505,6 +533,11 @@ struct FormatToken { /// token. unsigned LastLineColumnWidth = 0; + /// Offset (from the start of the token) where a space should be inserted + /// before the closing '*/' when \c NeedsSpaceBeforeClosingBlockComment is + /// set. + unsigned SpaceBeforeClosingBlockCommentOffset = 0; + /// The number of spaces that should be inserted before this token. unsigned SpacesRequiredBefore = 0; diff --git a/clang/lib/Format/FormatTokenLexer.cpp b/clang/lib/Format/FormatTokenLexer.cpp index 86a5185a92a52..f80f77bd9894e 100644 --- a/clang/lib/Format/FormatTokenLexer.cpp +++ b/clang/lib/Format/FormatTokenLexer.cpp @@ -18,11 +18,66 @@ #include "clang/Basic/SourceLocation.h" #include "clang/Basic/SourceManager.h" #include "clang/Format/Format.h" +#include "llvm/ADT/StringExtras.h" #include "llvm/Support/Regex.h" namespace clang { namespace format { +namespace { + +CommentKind classifyBlockComment(StringRef Text) { + if (!Text.starts_with("/*") || !Text.ends_with("*/")) + return CommentKind::Plain; + if (Text.starts_with("/**") || Text.starts_with("/*!")) + return CommentKind::Docstring; + const StringRef Content = Text.drop_front(2).drop_back(2).trim(); + if (Content.empty()) + return CommentKind::Plain; + + // Allow '$' in identifiers. This is required for languages like JavaScript + // which clang-format supports, to correctly classify parameter/sentinel + // comments such as /*$scope=*/ or /*$FALLTHROUGH*/. + const auto IsIdentifierStart = [](char C) { + return llvm::isAlpha(C) || C == '_' || C == '$'; + }; + const auto IsIdentifierBody = [](char C) { + return llvm::isAlnum(C) || C == '_' || C == '$'; + }; + const auto IsIdentifierLike = [&](StringRef Value) { + if (Value.empty()) + return false; + if (!IsIdentifierStart(Value.front())) + return false; + for (char C : Value.drop_front()) + if (!IsIdentifierBody(C)) + return false; + return true; + }; + const auto IsUppercaseWord = [](StringRef Value) { + if (Value.empty()) + return false; + for (char C : Value) { + if (llvm::isUpper(C) || llvm::isDigit(C) || C == '_' || C == '$') + continue; + return false; + } + return true; + }; + const bool HasWhitespace = + Content.find_first_of(" \t\n\v\f\r") != StringRef::npos; + + if (!HasWhitespace && IsUppercaseWord(Content)) + return CommentKind::Sentinel; + if (Content.ends_with('=')) { + const StringRef MaybeIdentifier = Content.drop_back().rtrim(); + if (IsIdentifierLike(MaybeIdentifier)) + return CommentKind::Parameter; + } + return CommentKind::Plain; +} +} // namespace + FormatTokenLexer::FormatTokenLexer( const SourceManager &SourceMgr, FileID ID, unsigned Column, const FormatStyle &Style, encoding::Encoding Encoding, @@ -1386,6 +1441,23 @@ FormatToken *FormatTokenLexer::getNextToken() { StringRef UntrimmedText = FormatTok->TokenText; FormatTok->TokenText = FormatTok->TokenText.rtrim(" \t\v\f"); TrailingWhitespace = UntrimmedText.size() - FormatTok->TokenText.size(); + FormatTok->setBlockCommentKind(classifyBlockComment(FormatTok->TokenText)); + + if (FormatTok->TokenText.starts_with("/*") && + FormatTok->TokenText.ends_with("*/") && + FormatTok->TokenText.size() >= 4) { + const StringRef Content = + FormatTok->TokenText.drop_front(2).drop_back(2).rtrim("\r\n"); + if (!Content.empty()) { + const unsigned char LastChar = + static_cast(Content.back()); + if (!isHorizontalWhitespace(LastChar)) { + FormatTok->NeedsSpaceBeforeClosingBlockComment = true; + FormatTok->SpaceBeforeClosingBlockCommentOffset = + FormatTok->TokenText.size() - 2; + } + } + } } else if (FormatTok->is(tok::raw_identifier)) { IdentifierInfo &Info = IdentTable.get(FormatTok->TokenText); FormatTok->Tok.setIdentifierInfo(&Info); diff --git a/clang/lib/Format/TokenAnnotator.cpp b/clang/lib/Format/TokenAnnotator.cpp index 5b784eded4601..a3da496ef8b87 100644 --- a/clang/lib/Format/TokenAnnotator.cpp +++ b/clang/lib/Format/TokenAnnotator.cpp @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// #include "TokenAnnotator.h" +#include "BreakableToken.h" #include "FormatToken.h" #include "clang/Basic/TokenKinds.h" #include "llvm/ADT/SmallPtrSet.h" @@ -4821,7 +4822,16 @@ bool TokenAnnotator::spaceRequiredBetween(const AnnotatedLine &Line, } if (Left.is(TT_BlockComment)) { // No whitespace in x(/*foo=*/1), except for JavaScript. - return Style.isJavaScript() || !Left.TokenText.ends_with("=*/"); + const StringRef Trimmed = Left.TokenText.rtrim(" \t"); + bool EndsWithAssignmentComment = Trimmed.ends_with("=*/"); + const FormatStyle::CommentSpaceMode BeforeClosingMode = + getBeforeClosingSpaceMode(Style, Left); + if (!EndsWithAssignmentComment && Trimmed.ends_with("= */") && + (BeforeClosingMode == FormatStyle::CommentSpaceMode::Always || + BeforeClosingMode == FormatStyle::CommentSpaceMode::Never)) { + EndsWithAssignmentComment = true; + } + return Style.isJavaScript() || !EndsWithAssignmentComment; } // Space between template and attribute. diff --git a/clang/lib/Format/WhitespaceManager.cpp b/clang/lib/Format/WhitespaceManager.cpp index 54f366fc02502..d008e099a99c0 100644 --- a/clang/lib/Format/WhitespaceManager.cpp +++ b/clang/lib/Format/WhitespaceManager.cpp @@ -12,6 +12,7 @@ //===----------------------------------------------------------------------===// #include "WhitespaceManager.h" +#include "BreakableToken.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/SmallVector.h" #include @@ -61,6 +62,10 @@ void WhitespaceManager::replaceWhitespace(FormatToken &Tok, unsigned Newlines, Spaces, StartOfTokenColumn, Newlines, "", "", IsAligned, InPPDirective && !Tok.IsFirst, /*IsInsideToken=*/false)); + if (Style.ReflowComments == FormatStyle::RCS_Never) { + applyAfterOpeningBlockCommentSpacing(Style, Tok, *this, InPPDirective); + applyBeforeClosingBlockCommentSpacing(Style, Tok, *this, InPPDirective); + } } void WhitespaceManager::addUntouchableToken(const FormatToken &Tok, diff --git a/clang/unittests/Format/ConfigParseTest.cpp b/clang/unittests/Format/ConfigParseTest.cpp index 6111e86ff7076..660897c985fcd 100644 --- a/clang/unittests/Format/ConfigParseTest.cpp +++ b/clang/unittests/Format/ConfigParseTest.cpp @@ -699,6 +699,31 @@ TEST(ConfigParseTest, ParsesConfiguration) { CHECK_PARSE("SpaceInEmptyBlock: true", SpaceInEmptyBraces, FormatStyle::SIEB_Block); + CHECK_PARSE_NESTED_VALUE("AfterOpeningComment: Always", SpaceInComments, + AfterOpeningComment, + FormatStyle::CommentSpaceMode::Always); + CHECK_PARSE_NESTED_VALUE("AfterOpeningComment: Never", SpaceInComments, + AfterOpeningComment, + FormatStyle::CommentSpaceMode::Never); + CHECK_PARSE_NESTED_VALUE("BeforeClosingComment: Always", SpaceInComments, + BeforeClosingComment, + FormatStyle::CommentSpaceMode::Always); + CHECK_PARSE_NESTED_VALUE("BeforeClosingComment: Never", SpaceInComments, + BeforeClosingComment, + FormatStyle::CommentSpaceMode::Never); + CHECK_PARSE_NESTED_VALUE("AfterOpeningParamComment: Always", SpaceInComments, + AfterOpeningParamComment, + FormatStyle::CommentSpaceMode::Always); + CHECK_PARSE_NESTED_VALUE("AfterOpeningParamComment: Never", SpaceInComments, + AfterOpeningParamComment, + FormatStyle::CommentSpaceMode::Never); + CHECK_PARSE_NESTED_VALUE("BeforeClosingParamComment: Always", SpaceInComments, + BeforeClosingParamComment, + FormatStyle::CommentSpaceMode::Always); + CHECK_PARSE_NESTED_VALUE("BeforeClosingParamComment: Never", SpaceInComments, + BeforeClosingParamComment, + FormatStyle::CommentSpaceMode::Never); + // For backward compatibility: Style.SpacesInParens = FormatStyle::SIPO_Never; Style.SpacesInParensOptions = {}; diff --git a/clang/unittests/Format/FormatTestComments.cpp b/clang/unittests/Format/FormatTestComments.cpp index 69026bce98705..64a5c22bd3518 100644 --- a/clang/unittests/Format/FormatTestComments.cpp +++ b/clang/unittests/Format/FormatTestComments.cpp @@ -332,6 +332,181 @@ TEST_F(FormatTestComments, UnderstandsSingleLineComments) { verifyNoCrash(StringRef("/*\\\0\n/", 6)); } +TEST_F(FormatTestComments, InsertsSpaceAfterOpeningBlockComment) { + FormatStyle Style = getLLVMStyle(); + Style.SpaceInComments.AfterOpeningComment = + FormatStyle::CommentSpaceMode::Always; + Style.ReflowComments = FormatStyle::RCS_Never; + + verifyFormat("foo(/* comment */);", "foo(/*comment */);", Style); + verifyFormat("foo(/* Logger=*/nullptr);", "foo(/*Logger=*/nullptr);", Style); + verifyFormat("/* comment */", "/*comment */", Style); + verifyFormat("/* comment before code */\nint x;", + "/*comment before code */\nint x;", Style); + verifyFormat("/* \ncomment */", "/*\ncomment */", Style); + verifyFormat("/* \ncomment */\nint x;", "/*\ncomment */\nint x;", Style); + verifyFormat("/* \ncomment line\n*/", "/*\ncomment line\n*/", Style); + verifyFormat("/* \n * comment star\n*/", "/*\n * comment star\n*/", Style); + verifyFormat("/* comment line\nnext */", "/*comment line\nnext */", Style); + EXPECT_EQ("/*\n*/", format("/*\n*/", Style)); + verifyFormat("/*\n\n*/", "/*\n \n*/", Style); + verifyFormat("/* This is a multi line comment\n" + "this is the next line\n" + "and this is the 3th line. */", + "/*This is a multi line comment\n" + "this is the next line\n" + "and this is the 3th line. */", + Style); + verifyFormat( + "/* \n * Another multi line comment\n * this is the next line. */", + "/*\n * Another multi line comment\n * this is the next line. */", Style); +} + +TEST_F(FormatTestComments, AfterOpeningCommentModes) { + FormatStyle Style = getLLVMStyle(); + Style.SpaceInComments.AfterOpeningComment = + FormatStyle::CommentSpaceMode::Always; + Style.ReflowComments = FormatStyle::RCS_Never; + + verifyFormat("foo(/* comment */);", "foo(/*comment */);", Style); + verifyFormat("foo(/* comment */);", "foo(/* comment */);", Style); + verifyFormat("/* \ncomment */", "/*\ncomment */", Style); + verifyFormat("/* */", "/* */", Style); + + Style = getLLVMStyle(); + Style.SpaceInComments.AfterOpeningComment = + FormatStyle::CommentSpaceMode::Never; + Style.ReflowComments = FormatStyle::RCS_Never; + + verifyFormat("foo(/*comment */);", "foo(/* comment */);", Style); + verifyFormat("/*\ncomment */", "/* \ncomment */", Style); + EXPECT_EQ("/*\ncomment */", format("/*\ncomment */", Style)); + verifyFormat("/**/", "/* */", Style); +} + +TEST_F(FormatTestComments, AfterOpeningParamCommentOverrides) { + FormatStyle Style = getLLVMStyle(); + Style.SpaceInComments.AfterOpeningParamComment = + FormatStyle::CommentSpaceMode::Always; + Style.ReflowComments = FormatStyle::RCS_Never; + + verifyFormat("call(/* Arg=*/value);", "call(/*Arg=*/value);", Style); + + Style.SpaceInComments.AfterOpeningParamComment = + FormatStyle::CommentSpaceMode::Never; + Style.ReflowComments = FormatStyle::RCS_Never; + + verifyFormat("call(/*Arg=*/value);", "call(/* Arg=*/value);", Style); +} + +TEST_F(FormatTestComments, InsertsSpaceBeforeClosingBlockComment) { + FormatStyle Style = getLLVMStyle(); + Style.SpaceInComments.BeforeClosingComment = + FormatStyle::CommentSpaceMode::Always; + + verifyFormat("foo(/* comment */);", "foo(/* comment*/);", Style); + verifyFormat("foo(/*Logger= */nullptr);", "foo(/*Logger=*/nullptr);", Style); + verifyFormat("/* comment */", Style); + verifyFormat("/* comment before code */\nint x;", + "/* comment before code*/\nint x;", Style); + verifyFormat("/* comment\n */", "/* comment\n*/", Style); + verifyFormat("/* comment\n */\nint x;", "/* comment\n*/\nint x;", Style); + verifyFormat("/*\ncomment line\n */", "/*\ncomment line\n*/", Style); + verifyFormat("/*\n * comment star\n */", "/*\n * comment star\n*/", Style); + verifyFormat("/* comment line\nnext */", "/* comment line\nnext*/", Style); + verifyFormat("/*\n*/", Style); + verifyFormat("/*\n\n*/", "/*\n \n*/", Style); + verifyFormat("/*This is a multi line comment\n" + "this is the next line\n" + "and this is the 3th line. */", + "/*This is a multi line comment\n" + "this is the next line\n" + "and this is the 3th line.*/", + Style); + verifyFormat( + "/*\n * Another multi line comment\n * this is the next line. */", + "/*\n * Another multi line comment\n * this is the next line.*/", Style); +} + +TEST_F(FormatTestComments, BeforeClosingCommentModes) { + FormatStyle Style = getLLVMStyle(); + Style.SpaceInComments.BeforeClosingComment = + FormatStyle::CommentSpaceMode::Always; + Style.ReflowComments = FormatStyle::RCS_Never; + + verifyFormat("foo(/* comment */);", "foo(/* comment*/);", Style); + verifyFormat("foo(/* comment */);", "foo(/* comment */);", Style); + verifyFormat("/* comment\n */", "/* comment\n*/", Style); + verifyFormat("/* */", "/* */", Style); + + Style = getLLVMStyle(); + Style.SpaceInComments.BeforeClosingComment = + FormatStyle::CommentSpaceMode::Never; + Style.ReflowComments = FormatStyle::RCS_Never; + + verifyFormat("foo(/* comment*/);", "foo(/* comment */);", Style); + verifyFormat("/* comment\n*/", "/* comment\n */", Style); + EXPECT_EQ("/* comment\n*/", format("/* comment\n*/", Style)); + verifyFormat("/**/", "/* */", Style); +} + +TEST_F(FormatTestComments, BeforeClosingParamCommentOverrides) { + FormatStyle Style = getLLVMStyle(); + Style.SpaceInComments.BeforeClosingParamComment = + FormatStyle::CommentSpaceMode::Always; + Style.ReflowComments = FormatStyle::RCS_Never; + + verifyFormat("call(/*Arg= */value);", "call(/*Arg=*/value);", Style); + + Style.SpaceInComments.BeforeClosingParamComment = + FormatStyle::CommentSpaceMode::Never; + Style.ReflowComments = FormatStyle::RCS_Never; + + verifyFormat("call(/*Arg=*/value);", "call(/*Arg= */value);", Style); +} + +TEST_F(FormatTestComments, BlockCommentDoesNotForceBreakBeforeFollowingToken) { + FormatStyle Style = getLLVMStyle(); + Style.SpaceInComments.AfterOpeningComment = + FormatStyle::CommentSpaceMode::Leave; + Style.SpaceInComments.BeforeClosingComment = + FormatStyle::CommentSpaceMode::Always; + + verifyFormat("switch (n) {\n" + "case 1:\n" + " foo();\n" + "/*FALLTHROUGH */ case 2:\n" + " bar();\n" + "}\n", + "switch (n) {\n" + "case 1:\n" + " foo();\n" + " /*FALLTHROUGH*/ case 2:\n" + " bar();\n" + "}\n", + Style); + + Style = getLLVMStyle(); + Style.SpaceInComments.AfterOpeningComment = + FormatStyle::CommentSpaceMode::Always; + Style.SpaceInComments.BeforeClosingComment = + FormatStyle::CommentSpaceMode::Leave; + + verifyFormat("switch (n) {\n" + "case 1:\n" + " foo();\n" + "/* FALLTHROUGH*/ case 2:\n" + " bar();\n" + "}\n", + "switch (n) {\n" + "case 1:\n" + " foo();\n" + " /*FALLTHROUGH*/ case 2:\n" + " bar();\n" + "}\n", + Style); +} + TEST_F(FormatTestComments, KeepsParameterWithTrailingCommentsOnTheirOwnLine) { EXPECT_EQ("SomeFunction(a,\n" " b, // comment\n" diff --git a/clang/unittests/Format/TokenAnnotatorTest.cpp b/clang/unittests/Format/TokenAnnotatorTest.cpp index c21b118847d99..7a052245bdd14 100644 --- a/clang/unittests/Format/TokenAnnotatorTest.cpp +++ b/clang/unittests/Format/TokenAnnotatorTest.cpp @@ -4196,6 +4196,38 @@ TEST_F(TokenAnnotatorTest, LineCommentTrailingBackslash) { EXPECT_TOKEN(Tokens[1], tok::comment, TT_LineComment); } +TEST_F(TokenAnnotatorTest, ClassifiesBlockCommentKinds) { + static constexpr struct { + StringRef Code; + CommentKind Kind; + } Cases[] = { + {"int value; /* comment */\n", CommentKind::Plain}, + {"int value; /** doc */\n", CommentKind::Docstring}, + {"call(/*Arg=*/value);", CommentKind::Parameter}, + {"switch (x) {\n" + "case 0:\n" + " /*FALLTHROUGH*/\n" + "default:\n" + " break;\n" + "}\n", + CommentKind::Sentinel}, + }; + + for (const auto &Test : Cases) { + const auto Tokens = annotate(Test.Code); + FormatToken *Comment = nullptr; + for (FormatToken *Tok : Tokens) { + if (Tok->is(tok::comment)) { + Comment = Tok; + break; + } + } + ASSERT_NE(Comment, nullptr) << "Missing comment token in: " << Test.Code; + EXPECT_EQ(Comment->getBlockCommentKind(), Test.Kind) + << "Comment text: " << Comment->TokenText; + } +} + TEST_F(TokenAnnotatorTest, ArrowAfterSubscript) { auto Tokens = annotate("return (getStructType()->getElements())[eIdx]->getName();");