=?utf-8?q?Marc-André?= Lureau <[email protected]>, =?utf-8?q?Marc-André?= Lureau <[email protected]> Message-ID: <llvm.org/llvm/llvm-project/pull/[email protected]> In-Reply-To:
https://github.com/elmarco created https://github.com/llvm/llvm-project/pull/198529 Add support for Linux kernel-doc comment format in clangd hover. This includes parsing kernel-doc structured comments (brief, @param, Return/Returns, named sections like Context/Note/Warning/Locking), RST-style indented and fenced code blocks, and inline markup conversion for %CONSTANT, @param, &struct references, ``literals``, $ENVVAR, and bare function() references. Related: https://github.com/clangd/clangd/issues/2662 Co-Authored-By: Claude Opus 4.6 <[email protected]> >From ba237bb0052951510cd8df66ec9d1aba8a2e37d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Lureau?= <[email protected]> Date: Wed, 13 May 2026 14:46:19 +0400 Subject: [PATCH 1/3] [clangd] Add C Doxygen hover test Add a test case for Doxygen-formatted documentation on a C function declaration, verifying that the structured rendering (brief, parameters, returns) works correctly for C source files. Co-Authored-By: Claude Opus 4.6 <[email protected]> --- .../clangd/unittests/HoverTests.cpp | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/clang-tools-extra/clangd/unittests/HoverTests.cpp b/clang-tools-extra/clangd/unittests/HoverTests.cpp index 7b168b0bdca60..f497ca5cb1ce7 100644 --- a/clang-tools-extra/clangd/unittests/HoverTests.cpp +++ b/clang-tools-extra/clangd/unittests/HoverTests.cpp @@ -5245,6 +5245,65 @@ TEST(Hover, FunctionParameters) { } } +TEST(Hover, CDoxygenFunction) { + Annotations T(R"c( + /** + * \brief Appends an element to the list. + * + * \param list The list to append to. + * \param data The data for the new element. + * \returns The new start of the list. + */ + int *[[^my_list_append]](int *list, int data); + )c"); + + TestTU TU = TestTU::withCode(T.code()); + TU.Filename = "TestTU.c"; + TU.ExtraArgs = {"-std=c17"}; + auto AST = TU.build(); + + Config Cfg; + Cfg.Documentation.CommentFormat = Config::CommentFormatPolicy::Doxygen; + WithContextValue WithCfg(Config::Key, std::move(Cfg)); + + auto H = getHover(AST, T.point(), format::getLLVMStyle(), nullptr); + ASSERT_TRUE(H); + + EXPECT_EQ(H->Name, "my_list_append"); + EXPECT_EQ(H->Kind, index::SymbolKind::Function); + EXPECT_EQ(H->ReturnType->Type, "int *"); + ASSERT_TRUE(H->Parameters); + ASSERT_EQ(H->Parameters->size(), 2u); + EXPECT_EQ(H->Parameters->at(0).Name, "list"); + EXPECT_EQ(H->Parameters->at(1).Name, "data"); + + auto Rendered = H->present(MarkupKind::Markdown); + llvm::StringRef ExpectedRender = + "### function\n" + "\n" + "---\n" + "```cpp\n" + "int *my_list_append(int *list, int data)\n" + "```\n" + "\n" + "---\n" + "### Brief\n" + "\n" + "Appends an element to the list.\n" + "\n" + "---\n" + "### Parameters\n" + "\n" + "- `int * list` - The list to append to.\n" + "- `int data` - The data for the new element.\n" + "\n" + "---\n" + "### Returns\n" + "\n" + "`int *` - The new start of the list."; + EXPECT_EQ(Rendered, ExpectedRender); +} + } // namespace } // namespace clangd } // namespace clang >From d4073e0bd71710a327f8223afbe79ce9969631c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Lureau?= <[email protected]> Date: Tue, 19 May 2026 16:38:12 +0400 Subject: [PATCH 2/3] [clangd][NFC] Extract appendCommonMetadata from presentDoxygen Co-Authored-By: Claude Opus 4.6 <[email protected]> --- clang-tools-extra/clangd/Hover.cpp | 53 +++++++++++++++--------------- clang-tools-extra/clangd/Hover.h | 3 ++ 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/clang-tools-extra/clangd/Hover.cpp b/clang-tools-extra/clangd/Hover.cpp index fab77af3ebcea..0ccbb40b90137 100644 --- a/clang-tools-extra/clangd/Hover.cpp +++ b/clang-tools-extra/clangd/Hover.cpp @@ -1528,6 +1528,32 @@ void HoverInfo::sizeToMarkupParagraph(markup::Paragraph &P) const { P.appendText(", alignment " + formatSize(*Align)); } +void HoverInfo::appendCommonMetadata(markup::Document &Output) const { + Output.addRuler(); + + // Don't print Type after Parameters or ReturnType as this will just duplicate + // the information + if (Type && !ReturnType && !Parameters) + Output.addParagraph().appendText("Type: ").appendCode( + llvm::to_string(*Type)); + + if (Value) + valueToMarkupParagraph(Output.addParagraph()); + + if (Offset) + offsetToMarkupParagraph(Output.addParagraph()); + if (Size) + sizeToMarkupParagraph(Output.addParagraph()); + + if (CalleeArgInfo) + calleeArgInfoToMarkupParagraph(Output.addParagraph()); + + if (!UsedSymbolNames.empty()) { + Output.addRuler(); + usedSymbolNamesToMarkup(Output); + } +} + markup::Document HoverInfo::presentDoxygen() const { markup::Document Output; @@ -1650,32 +1676,7 @@ markup::Document HoverInfo::presentDoxygen() const { SymbolDoc.detailedDocToMarkup(Output); } - Output.addRuler(); - - // Don't print Type after Parameters or ReturnType as this will just duplicate - // the information - if (Type && !ReturnType && !Parameters) - Output.addParagraph().appendText("Type: ").appendCode( - llvm::to_string(*Type)); - - if (Value) { - valueToMarkupParagraph(Output.addParagraph()); - } - - if (Offset) - offsetToMarkupParagraph(Output.addParagraph()); - if (Size) { - sizeToMarkupParagraph(Output.addParagraph()); - } - - if (CalleeArgInfo) { - calleeArgInfoToMarkupParagraph(Output.addParagraph()); - } - - if (!UsedSymbolNames.empty()) { - Output.addRuler(); - usedSymbolNamesToMarkup(Output); - } + appendCommonMetadata(Output); return Output; } diff --git a/clang-tools-extra/clangd/Hover.h b/clang-tools-extra/clangd/Hover.h index 614180a7b9846..c1d9e93221665 100644 --- a/clang-tools-extra/clangd/Hover.h +++ b/clang-tools-extra/clangd/Hover.h @@ -132,6 +132,9 @@ struct HoverInfo { void offsetToMarkupParagraph(markup::Paragraph &P) const; void sizeToMarkupParagraph(markup::Paragraph &P) const; + /// Append common metadata (type, value, offset, size, etc.) to the output. + void appendCommonMetadata(markup::Document &Output) const; + /// Parse and render the hover information as Doxygen documentation. markup::Document presentDoxygen() const; >From d5a35749e3e43086884e5882054bc77a06e2d79c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Lureau?= <[email protected]> Date: Sat, 16 May 2026 14:12:20 +0400 Subject: [PATCH 3/3] [clangd] Add kernel-doc hover support Add support for Linux kernel-doc comment format in clangd hover. This includes parsing kernel-doc structured comments (brief, @param, Return/Returns, named sections like Context/Note/Warning/Locking), RST-style indented and fenced code blocks, and inline markup conversion for %CONSTANT, @param, &struct references, ``literals``, $ENVVAR, and bare function() references. Related: https://github.com/clangd/clangd/issues/2662 Co-Authored-By: Claude Opus 4.6 <[email protected]> --- clang-tools-extra/clangd/Config.h | 2 + clang-tools-extra/clangd/ConfigCompile.cpp | 1 + clang-tools-extra/clangd/ConfigFragment.h | 1 + clang-tools-extra/clangd/Hover.cpp | 41 + clang-tools-extra/clangd/Hover.h | 3 + .../clangd/SymbolDocumentation.cpp | 522 +++++++++ .../clangd/SymbolDocumentation.h | 29 + .../clangd/unittests/HoverTests.cpp | 70 ++ .../unittests/SymbolDocumentationTests.cpp | 1025 +++++++++++++++++ 9 files changed, 1694 insertions(+) diff --git a/clang-tools-extra/clangd/Config.h b/clang-tools-extra/clangd/Config.h index 56d7ac453deeb..28d09d394e743 100644 --- a/clang-tools-extra/clangd/Config.h +++ b/clang-tools-extra/clangd/Config.h @@ -216,6 +216,8 @@ struct Config { Markdown, /// Treat comments as doxygen. Doxygen, + /// Treat comments as kernel-doc. + KernelDoc, }; struct { diff --git a/clang-tools-extra/clangd/ConfigCompile.cpp b/clang-tools-extra/clangd/ConfigCompile.cpp index 2b41949d6d05c..4c0f3d99743e2 100644 --- a/clang-tools-extra/clangd/ConfigCompile.cpp +++ b/clang-tools-extra/clangd/ConfigCompile.cpp @@ -820,6 +820,7 @@ struct FragmentCompiler { .map("Plaintext", Config::CommentFormatPolicy::PlainText) .map("Markdown", Config::CommentFormatPolicy::Markdown) .map("Doxygen", Config::CommentFormatPolicy::Doxygen) + .map("KernelDoc", Config::CommentFormatPolicy::KernelDoc) .value()) Out.Apply.push_back([Val](const Params &, Config &C) { C.Documentation.CommentFormat = *Val; diff --git a/clang-tools-extra/clangd/ConfigFragment.h b/clang-tools-extra/clangd/ConfigFragment.h index 7604fe4e24c97..90fb60f53d734 100644 --- a/clang-tools-extra/clangd/ConfigFragment.h +++ b/clang-tools-extra/clangd/ConfigFragment.h @@ -409,6 +409,7 @@ struct Fragment { /// - Plaintext: Treat comments as plain text. /// - Markdown: Treat comments as Markdown. /// - Doxygen: Treat comments as doxygen. + /// - KernelDoc: Treat comments as kernel-doc. std::optional<Located<std::string>> CommentFormat; }; DocumentationBlock Documentation; diff --git a/clang-tools-extra/clangd/Hover.cpp b/clang-tools-extra/clangd/Hover.cpp index 0ccbb40b90137..14f90330b4e7a 100644 --- a/clang-tools-extra/clangd/Hover.cpp +++ b/clang-tools-extra/clangd/Hover.cpp @@ -1681,6 +1681,44 @@ markup::Document HoverInfo::presentDoxygen() const { return Output; } +markup::Document HoverInfo::presentKernelDoc() const { + markup::Document Output; + + markup::Paragraph &Header = Output.addHeading(3); + if (!Definition.empty()) { + Output.addRuler(); + definitionScopeToMarkup(Output); + } else { + Header.appendCode(Name); + } + + Output.addRuler(); + KernelDocInfo DocInfo = parseKernelDoc(Documentation); + renderKernelDocToMarkup(DocInfo, Output); + + if (Parameters && !Parameters->empty() && DocInfo.Params.empty()) { + Output.addHeading(3).appendText("Parameters"); + markup::BulletList &L = Output.addBulletList(); + for (const auto &Param : *Parameters) + L.addItem().addParagraph().appendCode(llvm::to_string(Param)); + } + + if (ReturnType && + ReturnType->AKA.value_or(ReturnType->Type) != "void") { + if (DocInfo.Returns.empty() && DocInfo.ReturnItems.empty()) { + Output.addHeading(3).appendText("Returns"); + Output.addParagraph().appendCode(llvm::to_string(*ReturnType)); + } + } + + appendCommonMetadata(Output); + + if (!Provider.empty()) + providerToMarkupParagraph(Output); + + return Output; +} + markup::Document HoverInfo::presentDefault() const { markup::Document Output; // Header contains a text of the form: @@ -1771,6 +1809,9 @@ std::string HoverInfo::present(MarkupKind Kind) const { return presentDefault().asMarkdown(); if (Cfg.Documentation.CommentFormat == Config::CommentFormatPolicy::Doxygen) return presentDoxygen().asMarkdown(); + if (Cfg.Documentation.CommentFormat == + Config::CommentFormatPolicy::KernelDoc) + return presentKernelDoc().asMarkdown(); if (Cfg.Documentation.CommentFormat == Config::CommentFormatPolicy::PlainText) // If the user prefers plain text, we use the present() method to generate diff --git a/clang-tools-extra/clangd/Hover.h b/clang-tools-extra/clangd/Hover.h index c1d9e93221665..8422c449e89f0 100644 --- a/clang-tools-extra/clangd/Hover.h +++ b/clang-tools-extra/clangd/Hover.h @@ -138,6 +138,9 @@ struct HoverInfo { /// Parse and render the hover information as Doxygen documentation. markup::Document presentDoxygen() const; + /// Parse and render the hover information as kernel-doc documentation. + markup::Document presentKernelDoc() const; + /// Render the hover information as a default documentation. markup::Document presentDefault() const; }; diff --git a/clang-tools-extra/clangd/SymbolDocumentation.cpp b/clang-tools-extra/clangd/SymbolDocumentation.cpp index a50d7a565b1bc..e0c5f5e9edacb 100644 --- a/clang-tools-extra/clangd/SymbolDocumentation.cpp +++ b/clang-tools-extra/clangd/SymbolDocumentation.cpp @@ -557,5 +557,527 @@ void SymbolDocCommentVisitor::retvalsToMarkup(markup::Document &Out) const { } } +namespace { + +void convertKernelDocInlineMarkup(llvm::StringRef Text, + markup::Paragraph &Para) { + unsigned I = 0; + unsigned Start = 0; + while (I < Text.size()) { + char C = Text[I]; + + // Double-backtick literal: ``text`` + if (C == '`' && I + 1 < Text.size() && Text[I + 1] == '`') { + auto Close = Text.find("``", I + 2); + if (Close != StringRef::npos) { + if (I > Start) + Para.appendText(Text.slice(Start, I)); + Para.appendCode(Text.slice(I + 2, Close)); + I = Close + 2; + Start = I; + continue; + } + } + + // &struct name, &enum name, &typedef name, &struct->member + if (C == '&') { + unsigned J = I + 1; + // Optional keyword: struct, enum, typedef, union + unsigned KeywordEnd = J; + while (KeywordEnd < Text.size() && + (llvm::isAlpha(Text[KeywordEnd]) || Text[KeywordEnd] == '_')) + ++KeywordEnd; + StringRef MaybeKeyword = Text.slice(J, KeywordEnd); + bool HasKeyword = (MaybeKeyword == "struct" || MaybeKeyword == "enum" || + MaybeKeyword == "typedef" || MaybeKeyword == "union"); + unsigned NameStart = HasKeyword ? KeywordEnd : J; + if (HasKeyword && NameStart < Text.size() && Text[NameStart] == ' ') + ++NameStart; + unsigned NameEnd = NameStart; + while (NameEnd < Text.size() && + (llvm::isAlnum(Text[NameEnd]) || Text[NameEnd] == '_')) + ++NameEnd; + // Allow ->member or .member suffix + if (NameEnd < Text.size() && + (Text[NameEnd] == '.' || + (NameEnd + 1 < Text.size() && Text[NameEnd] == '-' && + Text[NameEnd + 1] == '>'))) { + unsigned MemberStart = + Text[NameEnd] == '.' ? NameEnd + 1 : NameEnd + 2; + unsigned MemberEnd = MemberStart; + while (MemberEnd < Text.size() && + (llvm::isAlnum(Text[MemberEnd]) || Text[MemberEnd] == '_')) + ++MemberEnd; + if (MemberEnd > MemberStart) + NameEnd = MemberEnd; + } + if (NameEnd > NameStart) { + if (I > Start) + Para.appendText(Text.slice(Start, I)); + Para.appendCode(Text.slice(J, NameEnd)); + I = NameEnd; + Start = I; + continue; + } + } + + // %CONSTANT or %-ERRNO + if (C == '%') { + unsigned J = I + 1; + if (J < Text.size() && Text[J] == '-') + ++J; + while (J < Text.size() && (llvm::isAlnum(Text[J]) || Text[J] == '_')) + ++J; + if (J > I + 1) { + if (I > Start) + Para.appendText(Text.slice(Start, I)); + Para.appendCode(Text.slice(I + 1, J)); + I = J; + Start = J; + continue; + } + } + + // @parameter + if (C == '@') { + unsigned J = I + 1; + while (J < Text.size() && (llvm::isAlnum(Text[J]) || Text[J] == '_')) + ++J; + if (J > I + 1) { + if (I > Start) + Para.appendText(Text.slice(Start, I)); + Para.appendCode(Text.slice(I + 1, J)); + I = J; + Start = J; + continue; + } + } + + // $ENVVAR + if (C == '$') { + unsigned J = I + 1; + while (J < Text.size() && (llvm::isAlnum(Text[J]) || Text[J] == '_')) + ++J; + if (J > I + 1) { + if (I > Start) + Para.appendText(Text.slice(Start, I)); + Para.appendCode(Text.slice(I, J)); + I = J; + Start = J; + continue; + } + } + + // Bare function references: identifier() + if ((llvm::isAlpha(C) || C == '_') && + (I == 0 || (!llvm::isAlnum(Text[I - 1]) && Text[I - 1] != '_'))) { + unsigned J = I + 1; + while (J < Text.size() && (llvm::isAlnum(Text[J]) || Text[J] == '_')) + ++J; + if (J + 1 < Text.size() && Text[J] == '(' && Text[J + 1] == ')') { + if (I > Start) + Para.appendText(Text.slice(Start, I)); + Para.appendCode(Text.slice(I, J + 2)); + I = J + 2; + Start = I; + continue; + } + } + + ++I; + } + if (Start < Text.size()) + Para.appendText(Text.slice(Start, Text.size())); +} + +} // namespace + +KernelDocInfo parseKernelDoc(llvm::StringRef Doc) { + KernelDocInfo Info; + + enum State { + Brief, + Params, + Returns, + Section, + Body, + FencedCodeBlock, + IndentedCodeBlock + } St = Brief; + std::string CurrentCodeBlock; + std::string CurrentCodeLang; + std::string CodeFence; + std::string CurrentParagraph; + + auto FlushParagraph = [&] { + StringRef Trimmed = StringRef(CurrentParagraph).trim(); + if (!Trimmed.empty()) { + // RST :: literal block marker: strip trailing :: + // "word::" → "word:", "word ::" → "word", "::" → nothing + if (Trimmed.ends_with("::")) { + StringRef WithoutDC = Trimmed.drop_back(2); + if (WithoutDC.ends_with(' ')) + WithoutDC = WithoutDC.rtrim(); + else if (!WithoutDC.empty()) + WithoutDC = Trimmed.drop_back(1); + if (!WithoutDC.empty()) + Info.Description.push_back( + {KernelDocDescriptionBlock::Paragraph, WithoutDC.str(), ""}); + } else { + Info.Description.push_back( + {KernelDocDescriptionBlock::Paragraph, Trimmed.str(), ""}); + } + } + CurrentParagraph.clear(); + }; + + // Detect named section headers: a capitalized word followed by ':' + // at the start of a line. Matches kernel-doc convention for Context:, + // Note:, Warning:, Locking:, etc. + auto IsSectionHeader = [](StringRef T) -> bool { + if (T.empty() || !llvm::isUpper(T[0])) + return false; + auto ColonPos = T.find(':'); + if (ColonPos == StringRef::npos || ColonPos < 2) + return false; + // Reject RST literal block markers like "Example::" + if (ColonPos + 1 < T.size() && T[ColonPos + 1] == ':') + return false; + StringRef Name = T.slice(0, ColonPos); + return llvm::all_of(Name, + [](char C) { return llvm::isAlnum(C) || C == '_'; }); + }; + + auto FlushIndentedCodeBlock = [&] { + StringRef Code = StringRef(CurrentCodeBlock).rtrim('\n'); + if (!Code.empty()) { + // Strip common leading indentation from all non-empty lines. + size_t MinIndent = StringRef::npos; + StringRef L, R = Code; + while (!R.empty()) { + std::tie(L, R) = R.split('\n'); + if (!L.empty()) + MinIndent = std::min(MinIndent, L.size() - L.ltrim().size()); + } + std::string Stripped; + R = Code; + bool First = true; + while (!R.empty()) { + std::tie(L, R) = R.split('\n'); + if (!First) + Stripped += '\n'; + First = false; + if (L.size() >= MinIndent) + Stripped += L.drop_front(MinIndent).str(); + } + Info.Description.push_back( + {KernelDocDescriptionBlock::Code, std::move(Stripped), ""}); + } + CurrentCodeBlock.clear(); + }; + + StringRef Line, Rest; + for (std::tie(Line, Rest) = Doc.split('\n'); + !(Line.empty() && Rest.empty()); + std::tie(Line, Rest) = Rest.split('\n')) { + + StringRef Trimmed = Line.ltrim(); + + if (St == FencedCodeBlock) { + if (Trimmed.starts_with(CodeFence)) { + StringRef Code = StringRef(CurrentCodeBlock).rtrim('\n'); + if (!Code.empty()) + Info.Description.push_back( + {KernelDocDescriptionBlock::Code, Code.str(), CurrentCodeLang}); + St = Body; + continue; + } + CurrentCodeBlock += Line.str() + "\n"; + continue; + } + + // RST-style indented code block: indented text after a blank line + if (St == IndentedCodeBlock) { + if (!Trimmed.empty() && (Line[0] == ' ' || Line[0] == '\t')) { + CurrentCodeBlock += Line.str() + "\n"; + continue; + } + if (Trimmed.empty()) { + CurrentCodeBlock += "\n"; + continue; + } + // Non-indented, non-blank line ends the code block. + FlushIndentedCodeBlock(); + St = Body; + // Fall through to process this line normally. + } + + // Markdown fenced code block: ```lang or ~~~ + if (Trimmed.starts_with("```") || Trimmed.starts_with("~~~")) { + if (St == Body) + FlushParagraph(); + CodeFence = + Trimmed.take_while([](char C) { return C == '`' || C == '~'; }).str(); + CurrentCodeLang = Trimmed.drop_front(CodeFence.size()).ltrim().str(); + CurrentCodeBlock.clear(); + St = FencedCodeBlock; + continue; + } + + // Brief line: "function_name() - Brief description" or just first + // non-empty line. May span multiple lines until a @param, blank line, + // or a named section/tag is seen. + if (St == Brief) { + if (Trimmed.empty()) { + if (!Info.Brief.empty()) + St = Params; + continue; + } + // End brief on structured tags — fall through to their handlers. + if (!Info.Brief.empty() && + (Trimmed.starts_with("@") || IsSectionHeader(Trimmed))) { + St = Params; + } else { + if (Info.Brief.empty()) { + auto DashPos = Trimmed.find(" - "); + if (DashPos != StringRef::npos) { + Info.Brief = Trimmed.drop_front(DashPos + 3).str(); + } else if (Trimmed.starts_with("@")) { + // Inline member doc: /** @member: description */ + auto ColonPos = Trimmed.find(':'); + if (ColonPos != StringRef::npos) + Info.Brief = Trimmed.drop_front(ColonPos + 1).ltrim().str(); + else + Info.Brief = Trimmed.str(); + } else { + // Try "identifier():" or "identifier:" colon-style brief. + bool FoundColonBrief = false; + unsigned J = 0; + while (J < Trimmed.size() && + (llvm::isAlnum(Trimmed[J]) || Trimmed[J] == '_')) + ++J; + if (J > 0 && J < Trimmed.size()) { + unsigned K = J; + if (K + 1 < Trimmed.size() && Trimmed[K] == '(' && + Trimmed[K + 1] == ')') + K += 2; + if (K < Trimmed.size() && Trimmed[K] == ' ') + ++K; + if (K < Trimmed.size() && Trimmed[K] == ':' && + (K + 1 >= Trimmed.size() || Trimmed[K + 1] != ':')) { + Info.Brief = Trimmed.drop_front(K + 1).ltrim().str(); + FoundColonBrief = true; + } + } + if (!FoundColonBrief) + Info.Brief = Trimmed.str(); + } + } else { + Info.Brief += " " + Trimmed.str(); + } + continue; + } + } + + // @return: / @returns: — treated as Return section per reference parser + if (Trimmed.starts_with_insensitive("@return:") || + Trimmed.starts_with_insensitive("@returns:")) { + if (St == Body) + FlushParagraph(); + St = Returns; + StringRef Tag = Trimmed.starts_with_insensitive("@returns:") + ? Trimmed.take_front(9) + : Trimmed.take_front(8); + Info.Returns = Trimmed.drop_front(Tag.size()).ltrim().str(); + continue; + } + + // @...: for variadic arguments + if (Trimmed.starts_with("@...:")) { + if (St == Body) + FlushParagraph(); + St = Params; + StringRef Desc = Trimmed.drop_front(5).ltrim(); + Info.Params.push_back({"...", Desc.str()}); + continue; + } + + // Parameter line: @name: description + if (Trimmed.starts_with("@")) { + auto ColonPos = Trimmed.find(':'); + if (ColonPos != StringRef::npos && ColonPos > 1) { + StringRef ParamName = Trimmed.slice(1, ColonPos); + bool IsParam = true; + for (unsigned K = 0; K < ParamName.size(); ++K) { + char C = ParamName[K]; + if (llvm::isAlnum(C) || C == '_' || C == '.') + continue; + if (C == '-' && K + 1 < ParamName.size() && ParamName[K + 1] == '>') { + ++K; // skip '>' + continue; + } + IsParam = false; + break; + } + if (IsParam) { + if (St == Body) + FlushParagraph(); + St = Params; + StringRef Desc = Trimmed.drop_front(ColonPos + 1).ltrim(); + Info.Params.push_back({ParamName.str(), Desc.str()}); + continue; + } + } + } + + // Return: or Returns: description (but not Return:: literal block marker) + if ((Trimmed.starts_with_insensitive("Return:") && + !Trimmed.starts_with_insensitive("Return::")) || + (Trimmed.starts_with_insensitive("Returns:") && + !Trimmed.starts_with_insensitive("Returns::"))) { + if (St == Body) + FlushParagraph(); + St = Returns; + StringRef Tag = Trimmed.starts_with_insensitive("Returns:") + ? Trimmed.take_front(8) + : Trimmed.take_front(7); + Info.Returns = Trimmed.drop_front(Tag.size()).ltrim().str(); + continue; + } + + // Description: is an optional explicit section header — strip the tag + // and treat the remainder as the start of body text. + if (Trimmed.starts_with_insensitive("Description:") && + !Trimmed.starts_with_insensitive("Description::")) { + if (St == Body) + FlushParagraph(); + St = Body; + StringRef Desc = Trimmed.drop_front(12).ltrim(); + if (!Desc.empty()) { + CurrentParagraph = Desc.str(); + } + continue; + } + + // Generic named section: "Word:" at start of line. + // Handles Context:, Note:, Warning:, Locking:, etc. + // When in a continuation state, only match non-indented lines as + // section headers — indented lines are continuation text. + bool IsIndented = !Line.empty() && (Line[0] == ' ' || Line[0] == '\t'); + bool InContinuation = (St == Params || St == Returns || St == Section); + if (IsSectionHeader(Trimmed) && !(InContinuation && IsIndented)) { + if (St == Body) + FlushParagraph(); + St = Section; + auto ColonPos = Trimmed.find(':'); + StringRef Name = Trimmed.slice(0, ColonPos); + StringRef Desc = Trimmed.drop_front(ColonPos + 1).ltrim(); + Info.Sections.push_back({Name.str(), Desc.str()}); + continue; + } + + // Param continuation: indented or non-tag non-empty line while in Params + if (St == Params && !Trimmed.empty() && !Info.Params.empty()) { + Info.Params.back().Description += " " + Trimmed.str(); + continue; + } + + // Returns continuation: detect RST list items (* or -) + if (St == Returns && !Trimmed.empty()) { + if (Trimmed.starts_with("* ") || Trimmed.starts_with("- ")) { + Info.ReturnItems.push_back(Trimmed.drop_front(2).str()); + } else if (!Info.ReturnItems.empty()) { + Info.ReturnItems.back() += " " + Trimmed.str(); + } else { + Info.Returns += " " + Trimmed.str(); + } + continue; + } + + // Section continuation + if (St == Section && !Trimmed.empty() && !Info.Sections.empty()) { + Info.Sections.back().Description += " " + Trimmed.str(); + continue; + } + + // Transition to body on blank line or first non-structured content + if (St == Params || St == Returns || St == Section) { + St = Body; + } + + // Body text + if (Trimmed.empty()) { + FlushParagraph(); + } else if (CurrentParagraph.empty() && Line[0] == '\t') { + CurrentCodeBlock = Line.str() + "\n"; + St = IndentedCodeBlock; + } else if (CurrentParagraph.empty() && Line.size() >= 2 && + Line[0] == ' ' && Line[1] == ' ') { + CurrentCodeBlock = Line.str() + "\n"; + St = IndentedCodeBlock; + } else { + if (!CurrentParagraph.empty()) + CurrentParagraph += " "; + CurrentParagraph += Trimmed.str(); + } + } + + if (St == IndentedCodeBlock) + FlushIndentedCodeBlock(); + else if (St == FencedCodeBlock) { + StringRef Code = StringRef(CurrentCodeBlock).rtrim('\n'); + if (!Code.empty()) + Info.Description.push_back( + {KernelDocDescriptionBlock::Code, Code.str(), CurrentCodeLang}); + } + FlushParagraph(); + + return Info; +} + +void renderKernelDocToMarkup(const KernelDocInfo &Info, + markup::Document &Output) { + if (!Info.Brief.empty()) + convertKernelDocInlineMarkup(Info.Brief, Output.addParagraph()); + + for (const auto &Block : Info.Description) { + if (Block.BlockKind == KernelDocDescriptionBlock::Paragraph) + convertKernelDocInlineMarkup(Block.Text, Output.addParagraph()); + else + Output.addCodeBlock(Block.Text, Block.Language); + } + + if (!Info.Params.empty()) { + Output.addHeading(3).appendText("Parameters"); + markup::BulletList &L = Output.addBulletList(); + for (const auto &P : Info.Params) { + markup::Paragraph &Para = L.addItem().addParagraph(); + Para.appendCode(P.Name); + if (!P.Description.empty()) { + Para.appendText(" - "); + convertKernelDocInlineMarkup(P.Description, Para); + } + } + } + + if (!Info.Returns.empty() || !Info.ReturnItems.empty()) { + Output.addHeading(3).appendText("Returns"); + if (!Info.Returns.empty()) + convertKernelDocInlineMarkup(Info.Returns, Output.addParagraph()); + if (!Info.ReturnItems.empty()) { + markup::BulletList &L = Output.addBulletList(); + for (const auto &Item : Info.ReturnItems) { + markup::Paragraph &Para = L.addItem().addParagraph(); + convertKernelDocInlineMarkup(Item, Para); + } + } + } + + for (const auto &S : Info.Sections) { + Output.addHeading(3).appendText(S.Name); + convertKernelDocInlineMarkup(S.Description, Output.addParagraph()); + } +} + } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/SymbolDocumentation.h b/clang-tools-extra/clangd/SymbolDocumentation.h index 88c7ade633516..4a550ae85da15 100644 --- a/clang-tools-extra/clangd/SymbolDocumentation.h +++ b/clang-tools-extra/clangd/SymbolDocumentation.h @@ -199,6 +199,35 @@ class SymbolDocCommentVisitor FreeParagraphs; }; +struct KernelDocParam { + std::string Name; + std::string Description; +}; + +struct KernelDocDescriptionBlock { + enum Kind { Paragraph, Code } BlockKind; + std::string Text; + std::string Language; +}; + +struct KernelDocSection { + std::string Name; + std::string Description; +}; + +struct KernelDocInfo { + std::string Brief; + llvm::SmallVector<KernelDocDescriptionBlock> Description; + llvm::SmallVector<KernelDocParam> Params; + std::string Returns; + llvm::SmallVector<std::string> ReturnItems; + llvm::SmallVector<KernelDocSection> Sections; +}; + +KernelDocInfo parseKernelDoc(llvm::StringRef Doc); +void renderKernelDocToMarkup(const KernelDocInfo &Info, + markup::Document &Output); + } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/unittests/HoverTests.cpp b/clang-tools-extra/clangd/unittests/HoverTests.cpp index f497ca5cb1ce7..1753a44c183fc 100644 --- a/clang-tools-extra/clangd/unittests/HoverTests.cpp +++ b/clang-tools-extra/clangd/unittests/HoverTests.cpp @@ -5304,6 +5304,76 @@ TEST(Hover, CDoxygenFunction) { EXPECT_EQ(Rendered, ExpectedRender); } +TEST(Hover, CKernelDocFunction) { + Annotations T(R"c( + /** + * my_alloc() - Allocate a buffer. + * @size: the size of the buffer to allocate + * @flags: allocation flags + * + * Allocates a contiguous buffer of at least @size bytes. + * + * Context: Process context. May sleep if %GFP_KERNEL is used. + * Return: Pointer to the buffer or %NULL on failure. + */ + void *[[^my_alloc]](int size, int flags); + )c"); + + TestTU TU = TestTU::withCode(T.code()); + TU.Filename = "TestTU.c"; + TU.ExtraArgs = {"-std=c17"}; + auto AST = TU.build(); + + Config Cfg; + Cfg.Documentation.CommentFormat = Config::CommentFormatPolicy::KernelDoc; + WithContextValue WithCfg(Config::Key, std::move(Cfg)); + + auto H = getHover(AST, T.point(), format::getLLVMStyle(), nullptr); + ASSERT_TRUE(H); + + EXPECT_EQ(H->Name, "my_alloc"); + EXPECT_EQ(H->Kind, index::SymbolKind::Function); + + auto Rendered = H->present(MarkupKind::Markdown); + EXPECT_NE(Rendered.find("Allocate a buffer."), std::string::npos); + EXPECT_NE(Rendered.find("`size`"), std::string::npos); + EXPECT_NE(Rendered.find("`flags`"), std::string::npos); + EXPECT_NE(Rendered.find("### Parameters"), std::string::npos); + EXPECT_NE(Rendered.find("### Returns"), std::string::npos); + EXPECT_NE(Rendered.find("### Context"), std::string::npos); + EXPECT_NE(Rendered.find("`GFP_KERNEL`"), std::string::npos); + EXPECT_NE(Rendered.find("`NULL`"), std::string::npos); +} + +TEST(Hover, CKernelDocInlineMember) { + Annotations T(R"c( + /** + * struct my_device - A device structure. + * @name: the device name + */ + struct my_device { + /** @bar: the status flags */ + int [[^bar]]; + }; + )c"); + + TestTU TU = TestTU::withCode(T.code()); + TU.Filename = "TestTU.c"; + TU.ExtraArgs = {"-std=c17"}; + auto AST = TU.build(); + + Config Cfg; + Cfg.Documentation.CommentFormat = Config::CommentFormatPolicy::KernelDoc; + WithContextValue WithCfg(Config::Key, std::move(Cfg)); + + auto H = getHover(AST, T.point(), format::getLLVMStyle(), nullptr); + ASSERT_TRUE(H); + + EXPECT_EQ(H->Name, "bar"); + auto Rendered = H->present(MarkupKind::Markdown); + EXPECT_NE(Rendered.find("the status flags"), std::string::npos); +} + } // namespace } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp b/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp index 676f7dfc74483..2a4f95ee47d1f 100644 --- a/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp +++ b/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp @@ -732,5 +732,1030 @@ line } } +TEST(KernelDoc, ParseBasic) { + KernelDocInfo Info = parseKernelDoc( + "kfree() - Free previously allocated memory\n" + "@objp: pointer returned by kmalloc()\n" + "\n" + "Don't free memory not originally allocated by kmalloc()\n" + "or you will run into trouble.\n" + "\n" + "Context: May be called from interrupt context.\n" + "Return: Nothing.\n"); + + EXPECT_EQ(Info.Brief, "Free previously allocated memory"); + ASSERT_EQ(Info.Params.size(), 1u); + EXPECT_EQ(Info.Params[0].Name, "objp"); + EXPECT_EQ(Info.Params[0].Description, "pointer returned by kmalloc()"); + ASSERT_EQ(Info.Description.size(), 1u); + EXPECT_EQ(Info.Description[0].Text, + "Don't free memory not originally allocated by kmalloc() " + "or you will run into trouble."); + ASSERT_EQ(Info.Sections.size(), 1u); + EXPECT_EQ(Info.Sections[0].Name, "Context"); + EXPECT_EQ(Info.Sections[0].Description, + "May be called from interrupt context."); + EXPECT_EQ(Info.Returns, "Nothing."); +} + +TEST(KernelDoc, ParseBriefOnly) { + KernelDocInfo Info = parseKernelDoc("my_func() - Just a brief\n"); + + EXPECT_EQ(Info.Brief, "Just a brief"); + EXPECT_TRUE(Info.Params.empty()); + EXPECT_TRUE(Info.Returns.empty()); + EXPECT_TRUE(Info.Sections.empty()); + EXPECT_TRUE(Info.Description.empty()); +} + +TEST(KernelDoc, ParseParamContinuation) { + KernelDocInfo Info = parseKernelDoc( + "my_func() - Brief\n" + "@buf: pointer to the buffer that will\n" + " receive the data\n" + "@len: length of the buffer\n"); + + ASSERT_EQ(Info.Params.size(), 2u); + EXPECT_EQ(Info.Params[0].Name, "buf"); + EXPECT_EQ(Info.Params[0].Description, + "pointer to the buffer that will receive the data"); + EXPECT_EQ(Info.Params[1].Name, "len"); + EXPECT_EQ(Info.Params[1].Description, "length of the buffer"); +} + +TEST(KernelDoc, ParseVariadicParam) { + KernelDocInfo Info = parseKernelDoc( + "printk() - Print a kernel message\n" + "@fmt: format string\n" + "@...: variable arguments\n"); + + ASSERT_EQ(Info.Params.size(), 2u); + EXPECT_EQ(Info.Params[0].Name, "fmt"); + EXPECT_EQ(Info.Params[1].Name, "..."); + EXPECT_EQ(Info.Params[1].Description, "variable arguments"); +} + +TEST(KernelDoc, ParseReturns) { + KernelDocInfo Info = parseKernelDoc( + "alloc_pages() - Allocate pages\n" + "@gfp: allocation flags\n" + "\n" + "Returns: A pointer to the first page or %NULL on failure.\n"); + + EXPECT_EQ(Info.Returns, "A pointer to the first page or %NULL on failure."); +} + +TEST(KernelDoc, ParseReturnsContinuation) { + KernelDocInfo Info = parseKernelDoc( + "do_something() - Do it\n" + "\n" + "Return: %0 on success, negative error code\n" + " on failure.\n"); + + EXPECT_EQ(Info.Returns, + "%0 on success, negative error code on failure."); +} + +TEST(KernelDoc, ParseContext) { + KernelDocInfo Info = parseKernelDoc( + "mutex_lock() - Acquire a mutex\n" + "@lock: the mutex to be acquired\n" + "\n" + "Context: Process context. May sleep if @lock is contended.\n"); + + ASSERT_EQ(Info.Sections.size(), 1u); + EXPECT_EQ(Info.Sections[0].Name, "Context"); + EXPECT_EQ(Info.Sections[0].Description, + "Process context. May sleep if @lock is contended."); +} + +TEST(KernelDoc, ParseCodeBlock) { + KernelDocInfo Info = parseKernelDoc( + "example() - Example function\n" + "\n" + "Usage:\n" + "\n" + "```c\n" + "example();\n" + "```\n"); + + bool HasCode = false; + for (const auto &Block : Info.Description) { + if (Block.BlockKind == KernelDocDescriptionBlock::Code) { + HasCode = true; + EXPECT_EQ(Block.Text, "example();"); + EXPECT_EQ(Block.Language, "c"); + } + } + EXPECT_TRUE(HasCode); +} + +TEST(KernelDoc, ParseNoBriefDash) { + KernelDocInfo Info = parseKernelDoc( + "This is a plain brief without function name pattern\n" + "@x: param\n"); + + EXPECT_EQ(Info.Brief, + "This is a plain brief without function name pattern"); + ASSERT_EQ(Info.Params.size(), 1u); + EXPECT_EQ(Info.Params[0].Name, "x"); +} + +TEST(KernelDoc, RenderToMarkup) { + KernelDocInfo Info; + Info.Brief = "Free previously allocated memory"; + Info.Params.push_back({"objp", "pointer returned by kmalloc()"}); + Info.Returns = "Nothing."; + Info.Sections.push_back( + {"Context", "May be called from interrupt context."}); + Info.Description.push_back( + {KernelDocDescriptionBlock::Paragraph, + "Don't free memory not originally allocated by kmalloc().", ""}); + + markup::Document Doc; + renderKernelDocToMarkup(Info, Doc); + std::string Rendered = Doc.asMarkdown(); + + EXPECT_NE(Rendered.find("Free previously allocated memory"), + std::string::npos); + EXPECT_NE(Rendered.find("`objp`"), std::string::npos); + EXPECT_NE(Rendered.find("kmalloc()"), std::string::npos); + EXPECT_NE(Rendered.find("### Parameters"), std::string::npos); + EXPECT_NE(Rendered.find("### Returns"), std::string::npos); + EXPECT_NE(Rendered.find("### Context"), std::string::npos); +} + +TEST(KernelDoc, InlineMarkup) { + KernelDocInfo Info; + Info.Brief = "Use %NULL and &struct device and @param and func()"; + + markup::Document Doc; + renderKernelDocToMarkup(Info, Doc); + std::string Rendered = Doc.asMarkdown(); + + EXPECT_NE(Rendered.find("`NULL`"), std::string::npos); + EXPECT_NE(Rendered.find("`struct device`"), std::string::npos); + EXPECT_NE(Rendered.find("`param`"), std::string::npos); + EXPECT_NE(Rendered.find("`func()`"), std::string::npos); +} + +TEST(KernelDoc, InlineMarkupStructMember) { + KernelDocInfo Info; + Info.Brief = "Access &device->name field"; + + markup::Document Doc; + renderKernelDocToMarkup(Info, Doc); + std::string Rendered = Doc.asMarkdown(); + + EXPECT_NE(Rendered.find("`device->name`"), std::string::npos); +} + +TEST(KernelDoc, InlineMarkupDoubleTick) { + KernelDocInfo Info; + Info.Brief = "Use ``literal text`` in docs"; + + markup::Document Doc; + renderKernelDocToMarkup(Info, Doc); + std::string Rendered = Doc.asMarkdown(); + + EXPECT_NE(Rendered.find("`literal text`"), std::string::npos); +} + +TEST(KernelDoc, ParseNegativeErrno) { + KernelDocInfo Info = parseKernelDoc( + "do_something() - Do it\n" + "\n" + "Return: %0 on success, %-ENOMEM or %-1 on failure.\n"); + + EXPECT_EQ(Info.Returns, "%0 on success, %-ENOMEM or %-1 on failure."); + + markup::Document Doc; + renderKernelDocToMarkup(Info, Doc); + std::string Rendered = Doc.asMarkdown(); + + EXPECT_NE(Rendered.find("`0`"), std::string::npos); + EXPECT_NE(Rendered.find("`-ENOMEM`"), std::string::npos); + EXPECT_NE(Rendered.find("`-1`"), std::string::npos); +} + +TEST(KernelDoc, ParseMultiLineBrief) { + KernelDocInfo Info = parseKernelDoc( + "func() - Allocate and initialize\n" + " a frobnicator for the device.\n" + "@dev: the target device\n"); + + EXPECT_EQ(Info.Brief, + "Allocate and initialize a frobnicator for the device."); + ASSERT_EQ(Info.Params.size(), 1u); + EXPECT_EQ(Info.Params[0].Name, "dev"); +} + +TEST(KernelDoc, ParseMultiLineBriefBlankEnd) { + KernelDocInfo Info = parseKernelDoc( + "func() - A long brief\n" + " that ends with a blank line.\n" + "\n" + "Description paragraph.\n"); + + EXPECT_EQ(Info.Brief, "A long brief that ends with a blank line."); + ASSERT_EQ(Info.Description.size(), 1u); + EXPECT_EQ(Info.Description[0].Text, "Description paragraph."); +} + +TEST(KernelDoc, ParseNestedStructMember) { + KernelDocInfo Info = parseKernelDoc( + "struct outer - An outer struct\n" + "@foo: simple member\n" + "@bar.baz: nested member\n" + "@bar.baz.qux: deeply nested member\n"); + + ASSERT_EQ(Info.Params.size(), 3u); + EXPECT_EQ(Info.Params[0].Name, "foo"); + EXPECT_EQ(Info.Params[1].Name, "bar.baz"); + EXPECT_EQ(Info.Params[1].Description, "nested member"); + EXPECT_EQ(Info.Params[2].Name, "bar.baz.qux"); + EXPECT_EQ(Info.Params[2].Description, "deeply nested member"); +} + +TEST(KernelDoc, InlineMarkupEnumTypedefUnion) { + KernelDocInfo Info; + Info.Brief = "See &enum color and &typedef handler_t and &union data"; + + markup::Document Doc; + renderKernelDocToMarkup(Info, Doc); + std::string Rendered = Doc.asMarkdown(); + + EXPECT_NE(Rendered.find("`enum color`"), std::string::npos); + EXPECT_NE(Rendered.find("`typedef handler_t`"), std::string::npos); + EXPECT_NE(Rendered.find("`union data`"), std::string::npos); +} + +TEST(KernelDoc, InlineMarkupDotMember) { + KernelDocInfo Info; + Info.Brief = "Access &device.name field"; + + markup::Document Doc; + renderKernelDocToMarkup(Info, Doc); + std::string Rendered = Doc.asMarkdown(); + + EXPECT_NE(Rendered.find("`device.name`"), std::string::npos); +} + +TEST(KernelDoc, ParseEmptyParamDescription) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "@x:\n" + "@y: has description\n"); + + ASSERT_EQ(Info.Params.size(), 2u); + EXPECT_EQ(Info.Params[0].Name, "x"); + EXPECT_EQ(Info.Params[0].Description, ""); + EXPECT_EQ(Info.Params[1].Name, "y"); + EXPECT_EQ(Info.Params[1].Description, "has description"); +} + +TEST(KernelDoc, ParseMultipleDescriptionParagraphs) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "@x: param\n" + "\n" + "First paragraph of description.\n" + "\n" + "Second paragraph of description.\n"); + + ASSERT_EQ(Info.Description.size(), 2u); + EXPECT_EQ(Info.Description[0].Text, "First paragraph of description."); + EXPECT_EQ(Info.Description[1].Text, "Second paragraph of description."); +} + +TEST(KernelDoc, ParseCodeBlockNoLanguage) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "\n" + "```\n" + "some_code();\n" + "```\n"); + + bool HasCode = false; + for (const auto &Block : Info.Description) { + if (Block.BlockKind == KernelDocDescriptionBlock::Code) { + HasCode = true; + EXPECT_EQ(Block.Text, "some_code();"); + EXPECT_EQ(Block.Language, ""); + } + } + EXPECT_TRUE(HasCode); +} + +TEST(KernelDoc, ParseUnclosedFencedCodeBlock) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "\n" + "```c\n" + "code_here();\n"); + + bool HasCode = false; + for (const auto &Block : Info.Description) { + if (Block.BlockKind == KernelDocDescriptionBlock::Code) { + HasCode = true; + EXPECT_EQ(Block.Text, "code_here();"); + EXPECT_EQ(Block.Language, "c"); + } + } + EXPECT_TRUE(HasCode); +} + +TEST(KernelDoc, InlineMarkupInParamDescription) { + KernelDocInfo Info; + Info.Brief = "Do something"; + Info.Params.push_back({"buf", "pointer to &struct page returned by alloc()"}); + + markup::Document Doc; + renderKernelDocToMarkup(Info, Doc); + std::string Rendered = Doc.asMarkdown(); + + EXPECT_NE(Rendered.find("`struct page`"), std::string::npos); + EXPECT_NE(Rendered.find("`alloc()`"), std::string::npos); +} + +TEST(KernelDoc, InlineMarkupInSectionDescription) { + KernelDocInfo Info; + Info.Brief = "Do something"; + Info.Sections.push_back( + {"Context", "Caller must hold @lock and not be in %IRQ context."}); + + markup::Document Doc; + renderKernelDocToMarkup(Info, Doc); + std::string Rendered = Doc.asMarkdown(); + + EXPECT_NE(Rendered.find("`lock`"), std::string::npos); + EXPECT_NE(Rendered.find("`IRQ`"), std::string::npos); +} + +TEST(KernelDoc, ParseTildeFencedCodeBlock) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "\n" + "~~~c\n" + "int x = 42;\n" + "~~~\n"); + + bool HasCode = false; + for (const auto &Block : Info.Description) { + if (Block.BlockKind == KernelDocDescriptionBlock::Code) { + HasCode = true; + EXPECT_EQ(Block.Text, "int x = 42;"); + EXPECT_EQ(Block.Language, "c"); + } + } + EXPECT_TRUE(HasCode); +} + +TEST(KernelDoc, ParseDescriptionHeader) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "@x: param\n" + "\n" + "Description: The detailed description here.\n"); + + EXPECT_EQ(Info.Brief, "Brief"); + ASSERT_EQ(Info.Params.size(), 1u); + ASSERT_EQ(Info.Description.size(), 1u); + EXPECT_EQ(Info.Description[0].Text, "The detailed description here."); +} + +TEST(KernelDoc, ParseDescriptionHeaderMultiParagraph) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "@x: param\n" + "\n" + "Description: First paragraph.\n" + "\n" + "Second paragraph.\n"); + + ASSERT_EQ(Info.Description.size(), 2u); + EXPECT_EQ(Info.Description[0].Text, "First paragraph."); + EXPECT_EQ(Info.Description[1].Text, "Second paragraph."); +} + +TEST(KernelDoc, ParseDescriptionHeaderStripped) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "@x: param\n" + "\n" + "Description:\n" + "The description follows on the next line.\n"); + + ASSERT_EQ(Info.Description.size(), 1u); + EXPECT_EQ(Info.Description[0].Text, + "The description follows on the next line."); +} + +TEST(KernelDoc, ParseNoteSection) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "@x: param\n" + "\n" + "Note: This function should only be called with interrupts disabled.\n"); + + ASSERT_EQ(Info.Sections.size(), 1u); + EXPECT_EQ(Info.Sections[0].Name, "Note"); + EXPECT_EQ(Info.Sections[0].Description, + "This function should only be called with interrupts disabled."); +} + +TEST(KernelDoc, ParseNoteContinuation) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "\n" + "Note: This is important\n" + " and spans multiple lines.\n"); + + ASSERT_EQ(Info.Sections.size(), 1u); + EXPECT_EQ(Info.Sections[0].Name, "Note"); + EXPECT_EQ(Info.Sections[0].Description, + "This is important and spans multiple lines."); +} + +TEST(KernelDoc, RenderNote) { + KernelDocInfo Info; + Info.Brief = "Do something"; + Info.Sections.push_back({"Note", "Only call from process context."}); + + markup::Document Doc; + renderKernelDocToMarkup(Info, Doc); + std::string Rendered = Doc.asMarkdown(); + + EXPECT_NE(Rendered.find("### Note"), std::string::npos); + EXPECT_NE(Rendered.find("Only call from process context."), std::string::npos); +} + +TEST(KernelDoc, ParseWarningSection) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "@x: param\n" + "\n" + "Warning: This function is not thread-safe.\n"); + + ASSERT_EQ(Info.Sections.size(), 1u); + EXPECT_EQ(Info.Sections[0].Name, "Warning"); + EXPECT_EQ(Info.Sections[0].Description, + "This function is not thread-safe."); +} + +TEST(KernelDoc, ParseLockingSection) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "@lock: the mutex\n" + "\n" + "Locking: Caller must hold @lock.\n"); + + ASSERT_EQ(Info.Sections.size(), 1u); + EXPECT_EQ(Info.Sections[0].Name, "Locking"); + EXPECT_EQ(Info.Sections[0].Description, "Caller must hold @lock."); +} + +TEST(KernelDoc, ParseMultipleSections) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "@x: param\n" + "\n" + "Context: Process context. May sleep.\n" + "Note: Only valid after initialization.\n" + "Warning: Not thread-safe.\n"); + + ASSERT_EQ(Info.Sections.size(), 3u); + EXPECT_EQ(Info.Sections[0].Name, "Context"); + EXPECT_EQ(Info.Sections[0].Description, "Process context. May sleep."); + EXPECT_EQ(Info.Sections[1].Name, "Note"); + EXPECT_EQ(Info.Sections[1].Description, + "Only valid after initialization."); + EXPECT_EQ(Info.Sections[2].Name, "Warning"); + EXPECT_EQ(Info.Sections[2].Description, "Not thread-safe."); +} + +TEST(KernelDoc, ParseSectionWithContinuation) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "\n" + "Context: Process context.\n" + " May sleep if lock is contended.\n" + "Warning: Do not call from interrupt context\n" + " or atomic sections.\n"); + + ASSERT_EQ(Info.Sections.size(), 2u); + EXPECT_EQ(Info.Sections[0].Name, "Context"); + EXPECT_EQ(Info.Sections[0].Description, + "Process context. May sleep if lock is contended."); + EXPECT_EQ(Info.Sections[1].Name, "Warning"); + EXPECT_EQ(Info.Sections[1].Description, + "Do not call from interrupt context or atomic sections."); +} + +TEST(KernelDoc, RenderMultipleSections) { + KernelDocInfo Info; + Info.Brief = "Do something"; + Info.Sections.push_back({"Context", "Process context."}); + Info.Sections.push_back({"Warning", "Not thread-safe."}); + + markup::Document Doc; + renderKernelDocToMarkup(Info, Doc); + std::string Rendered = Doc.asMarkdown(); + + EXPECT_NE(Rendered.find("### Context"), std::string::npos); + EXPECT_NE(Rendered.find("Process context."), std::string::npos); + EXPECT_NE(Rendered.find("### Warning"), std::string::npos); + EXPECT_NE(Rendered.find("Not thread-safe."), std::string::npos); +} + +TEST(KernelDoc, InlineMarkupEnvVar) { + KernelDocInfo Info; + Info.Brief = "Uses $HOME and $PATH_INFO for lookup"; + + markup::Document Doc; + renderKernelDocToMarkup(Info, Doc); + std::string Rendered = Doc.asMarkdown(); + + EXPECT_NE(Rendered.find("`$HOME`"), std::string::npos); + EXPECT_NE(Rendered.find("`$PATH_INFO`"), std::string::npos); +} + +TEST(KernelDoc, ParseIndentedCodeBlock) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "\n" + "Example::\n" + "\n" + " int x = func();\n" + " use(x);\n" + "\n" + "More text.\n"); + + EXPECT_EQ(Info.Brief, "Brief"); + bool HasParagraph = false, HasCode = false, HasMore = false; + for (const auto &Block : Info.Description) { + if (Block.BlockKind == KernelDocDescriptionBlock::Paragraph) { + if (Block.Text == "Example:") + HasParagraph = true; + if (Block.Text == "More text.") + HasMore = true; + } + if (Block.BlockKind == KernelDocDescriptionBlock::Code) { + HasCode = true; + EXPECT_EQ(Block.Text, "int x = func();\nuse(x);"); + } + } + EXPECT_TRUE(HasParagraph); + EXPECT_TRUE(HasCode); + EXPECT_TRUE(HasMore); +} + +TEST(KernelDoc, ParseIndentedCodeBlockNoDC) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "\n" + "Usage example\n" + "\n" + " result = func();\n" + " check(result);\n"); + + bool HasCode = false; + for (const auto &Block : Info.Description) { + if (Block.BlockKind == KernelDocDescriptionBlock::Code) { + HasCode = true; + EXPECT_EQ(Block.Text, "result = func();\ncheck(result);"); + } + } + EXPECT_TRUE(HasCode); +} + +TEST(KernelDoc, ParseIndentedCodeBlockStandaloneDC) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "\n" + "::\n" + "\n" + " code_here();\n"); + + // Standalone :: should not produce a paragraph. + for (const auto &Block : Info.Description) + EXPECT_NE(Block.Text, "::"); + bool HasCode = false; + for (const auto &Block : Info.Description) { + if (Block.BlockKind == KernelDocDescriptionBlock::Code) { + HasCode = true; + EXPECT_EQ(Block.Text, "code_here();"); + } + } + EXPECT_TRUE(HasCode); +} + +TEST(KernelDoc, ParseIndentedCodeBlockBlankWithin) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "\n" + " first_block();\n" + "\n" + " second_block();\n"); + + bool HasCode = false; + for (const auto &Block : Info.Description) { + if (Block.BlockKind == KernelDocDescriptionBlock::Code) { + HasCode = true; + EXPECT_EQ(Block.Text, "first_block();\n\nsecond_block();"); + } + } + EXPECT_TRUE(HasCode); +} + +TEST(KernelDoc, ParseIndentedCodeBlockStripsIndent) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "\n" + " line1();\n" + " line2();\n"); + + bool HasCode = false; + for (const auto &Block : Info.Description) { + if (Block.BlockKind == KernelDocDescriptionBlock::Code) { + HasCode = true; + EXPECT_EQ(Block.Text, "line1();\nline2();"); + } + } + EXPECT_TRUE(HasCode); +} + +TEST(KernelDoc, ParseIndentedCodeBlockThenText) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "\n" + " code();\n" + "Normal paragraph after code.\n"); + + bool HasCode = false, HasText = false; + for (const auto &Block : Info.Description) { + if (Block.BlockKind == KernelDocDescriptionBlock::Code) { + HasCode = true; + EXPECT_EQ(Block.Text, "code();"); + } + if (Block.BlockKind == KernelDocDescriptionBlock::Paragraph && + Block.Text == "Normal paragraph after code.") + HasText = true; + } + EXPECT_TRUE(HasCode); + EXPECT_TRUE(HasText); +} + +TEST(KernelDoc, ParseExampleDCNotSection) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "\n" + "Example::\n" + "\n" + " func(42);\n"); + + EXPECT_TRUE(Info.Sections.empty()); + bool HasParagraph = false, HasCode = false; + for (const auto &Block : Info.Description) { + if (Block.BlockKind == KernelDocDescriptionBlock::Paragraph && + Block.Text == "Example:") + HasParagraph = true; + if (Block.BlockKind == KernelDocDescriptionBlock::Code) + HasCode = true; + } + EXPECT_TRUE(HasParagraph); + EXPECT_TRUE(HasCode); +} + +TEST(KernelDoc, ParseReturnListItems) { + KernelDocInfo Info = parseKernelDoc( + "do_something() - Do it\n" + "\n" + "Return:\n" + "* %0 - OK\n" + "* %-EINVAL - Invalid argument\n" + "* %-ENOMEM - Out of memory\n"); + + EXPECT_TRUE(Info.Returns.empty()); + ASSERT_EQ(Info.ReturnItems.size(), 3u); + EXPECT_EQ(Info.ReturnItems[0], "%0 - OK"); + EXPECT_EQ(Info.ReturnItems[1], "%-EINVAL - Invalid argument"); + EXPECT_EQ(Info.ReturnItems[2], "%-ENOMEM - Out of memory"); +} + +TEST(KernelDoc, ParseReturnListWithPreamble) { + KernelDocInfo Info = parseKernelDoc( + "do_something() - Do it\n" + "\n" + "Return: One of the following error codes:\n" + "* %0 - OK\n" + "* %-EINVAL - Invalid argument\n"); + + EXPECT_EQ(Info.Returns, "One of the following error codes:"); + ASSERT_EQ(Info.ReturnItems.size(), 2u); + EXPECT_EQ(Info.ReturnItems[0], "%0 - OK"); + EXPECT_EQ(Info.ReturnItems[1], "%-EINVAL - Invalid argument"); +} + +TEST(KernelDoc, ParseReturnListDashMarker) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "\n" + "Return:\n" + "- zero on success\n" + "- negative errno on failure\n"); + + EXPECT_TRUE(Info.Returns.empty()); + ASSERT_EQ(Info.ReturnItems.size(), 2u); + EXPECT_EQ(Info.ReturnItems[0], "zero on success"); + EXPECT_EQ(Info.ReturnItems[1], "negative errno on failure"); +} + +TEST(KernelDoc, ParseReturnListContinuation) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "\n" + "Return:\n" + "* %0 - OK to proceed\n" + " with the operation\n" + "* %-EINVAL - Bad argument\n"); + + ASSERT_EQ(Info.ReturnItems.size(), 2u); + EXPECT_EQ(Info.ReturnItems[0], "%0 - OK to proceed with the operation"); + EXPECT_EQ(Info.ReturnItems[1], "%-EINVAL - Bad argument"); +} + +TEST(KernelDoc, RenderReturnList) { + KernelDocInfo Info; + Info.Brief = "Do something"; + Info.ReturnItems.push_back("%0 - OK"); + Info.ReturnItems.push_back("%-EINVAL - Invalid argument"); + + markup::Document Doc; + renderKernelDocToMarkup(Info, Doc); + std::string Rendered = Doc.asMarkdown(); + + EXPECT_NE(Rendered.find("### Returns"), std::string::npos); + EXPECT_NE(Rendered.find("`0`"), std::string::npos); + EXPECT_NE(Rendered.find("`-EINVAL`"), std::string::npos); +} + +TEST(KernelDoc, RenderReturnListWithPreamble) { + KernelDocInfo Info; + Info.Brief = "Do something"; + Info.Returns = "One of:"; + Info.ReturnItems.push_back("%0 - OK"); + + markup::Document Doc; + renderKernelDocToMarkup(Info, Doc); + std::string Rendered = Doc.asMarkdown(); + + EXPECT_NE(Rendered.find("### Returns"), std::string::npos); + EXPECT_NE(Rendered.find("One of:"), std::string::npos); + EXPECT_NE(Rendered.find("`0`"), std::string::npos); +} + +TEST(KernelDoc, ParseInlineMemberDoc) { + KernelDocInfo Info = parseKernelDoc("@bar: description of bar"); + EXPECT_EQ(Info.Brief, "description of bar"); + EXPECT_TRUE(Info.Params.empty()); +} + +TEST(KernelDoc, ParseInlineMemberDocMultiLine) { + KernelDocInfo Info = parseKernelDoc( + "@bar: brief text\n" + "\n" + "Longer description of bar."); + + EXPECT_EQ(Info.Brief, "brief text"); + ASSERT_EQ(Info.Description.size(), 1u); + EXPECT_EQ(Info.Description[0].Text, "Longer description of bar."); + EXPECT_TRUE(Info.Params.empty()); +} + +TEST(KernelDoc, ParseRSTDCWithSpace) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "\n" + "Some text ::\n" + "\n" + " code();\n"); + + bool HasParagraph = false, HasCode = false; + for (const auto &Block : Info.Description) { + if (Block.BlockKind == KernelDocDescriptionBlock::Paragraph && + Block.Text == "Some text") + HasParagraph = true; + if (Block.BlockKind == KernelDocDescriptionBlock::Code) + HasCode = true; + } + EXPECT_TRUE(HasParagraph); + EXPECT_TRUE(HasCode); +} + +TEST(KernelDoc, ParseRSTDCAttached) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "\n" + "Example::\n" + "\n" + " code();\n"); + + bool HasParagraph = false; + for (const auto &Block : Info.Description) { + if (Block.BlockKind == KernelDocDescriptionBlock::Paragraph && + Block.Text == "Example:") + HasParagraph = true; + } + EXPECT_TRUE(HasParagraph); +} + +TEST(KernelDoc, ParseTypedef) { + KernelDocInfo Info = parseKernelDoc("my_type - A custom type"); + EXPECT_EQ(Info.Brief, "A custom type"); +} + +TEST(KernelDoc, ParseMacro) { + KernelDocInfo Info = + parseKernelDoc("MY_MACRO - A useful macro\n" + "@x: first argument\n" + "@y: second argument\n"); + EXPECT_EQ(Info.Brief, "A useful macro"); + ASSERT_EQ(Info.Params.size(), 2u); + EXPECT_EQ(Info.Params[0].Name, "x"); + EXPECT_EQ(Info.Params[1].Name, "y"); +} + +TEST(KernelDoc, ParseParamsAfterBody) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "\n" + "Description first.\n" + "\n" + "@a: param after body\n"); + + EXPECT_EQ(Info.Brief, "Brief"); + ASSERT_EQ(Info.Description.size(), 1u); + EXPECT_EQ(Info.Description[0].Text, "Description first."); + ASSERT_EQ(Info.Params.size(), 1u); + EXPECT_EQ(Info.Params[0].Name, "a"); + EXPECT_EQ(Info.Params[0].Description, "param after body"); +} + +TEST(KernelDoc, ParseSectionAfterBrief) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "Context: Process context.\n"); + + EXPECT_EQ(Info.Brief, "Brief"); + ASSERT_EQ(Info.Sections.size(), 1u); + EXPECT_EQ(Info.Sections[0].Name, "Context"); + EXPECT_EQ(Info.Sections[0].Description, "Process context."); +} + +TEST(KernelDoc, ParseBriefEmptyAfterDash) { + KernelDocInfo Info = parseKernelDoc("func() - "); + EXPECT_EQ(Info.Brief, ""); +} + +TEST(KernelDoc, ParseReturnContinuationNotSection) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "\n" + "Return: The pointer is\n" + " Valid: only when active.\n" + "Context: Process context.\n"); + + EXPECT_EQ(Info.Returns, "The pointer is Valid: only when active."); + ASSERT_EQ(Info.Sections.size(), 1u); + EXPECT_EQ(Info.Sections[0].Name, "Context"); + EXPECT_EQ(Info.Sections[0].Description, "Process context."); +} + +TEST(KernelDoc, ParseParamContinuationNotSection) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "@buf: Pointer to the\n" + " Buffer: must be aligned.\n" + "@len: length\n"); + + ASSERT_EQ(Info.Params.size(), 2u); + EXPECT_EQ(Info.Params[0].Name, "buf"); + EXPECT_EQ(Info.Params[0].Description, + "Pointer to the Buffer: must be aligned."); + EXPECT_EQ(Info.Params[1].Name, "len"); +} + +TEST(KernelDoc, ParseSectionContinuationNotSection) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "\n" + "Context: Cannot be called from\n" + " Interrupt: context or atomic sections.\n"); + + ASSERT_EQ(Info.Sections.size(), 1u); + EXPECT_EQ(Info.Sections[0].Name, "Context"); + EXPECT_EQ(Info.Sections[0].Description, + "Cannot be called from Interrupt: context or atomic sections."); +} + +TEST(KernelDoc, ParseArrowParam) { + KernelDocInfo Info = parseKernelDoc( + "struct outer - An outer struct\n" + "@foo: simple member\n" + "@foo->bar: arrow member\n" + "@foo->bar.baz: chained member\n"); + + ASSERT_EQ(Info.Params.size(), 3u); + EXPECT_EQ(Info.Params[0].Name, "foo"); + EXPECT_EQ(Info.Params[1].Name, "foo->bar"); + EXPECT_EQ(Info.Params[1].Description, "arrow member"); + EXPECT_EQ(Info.Params[2].Name, "foo->bar.baz"); + EXPECT_EQ(Info.Params[2].Description, "chained member"); +} + +TEST(KernelDoc, ParseAtReturn) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "@x: param\n" + "\n" + "@return: Zero on success.\n"); + + ASSERT_EQ(Info.Params.size(), 1u); + EXPECT_EQ(Info.Returns, "Zero on success."); +} + +TEST(KernelDoc, ParseAtReturns) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "@x: param\n" + "\n" + "@returns: A pointer or %NULL.\n"); + + ASSERT_EQ(Info.Params.size(), 1u); + EXPECT_EQ(Info.Returns, "A pointer or %NULL."); +} + +TEST(KernelDoc, ParseReturnCaseInsensitive) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "\n" + "RETURN: Zero on success.\n"); + + EXPECT_EQ(Info.Returns, "Zero on success."); +} + +TEST(KernelDoc, ParseReturnsCaseInsensitive) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "\n" + "RETURNS: A pointer.\n"); + + EXPECT_EQ(Info.Returns, "A pointer."); +} + +TEST(KernelDoc, ParseDescriptionCaseInsensitive) { + KernelDocInfo Info = parseKernelDoc( + "func() - Brief\n" + "@x: param\n" + "\n" + "description: The detailed description.\n"); + + ASSERT_EQ(Info.Description.size(), 1u); + EXPECT_EQ(Info.Description[0].Text, "The detailed description."); +} + +TEST(KernelDoc, ParseColonBrief) { + KernelDocInfo Info = parseKernelDoc( + "func(): Return temperature from raw value\n" + "@x: param\n"); + + EXPECT_EQ(Info.Brief, "Return temperature from raw value"); + ASSERT_EQ(Info.Params.size(), 1u); + EXPECT_EQ(Info.Params[0].Name, "x"); +} + +TEST(KernelDoc, ParseColonBriefNoParens) { + KernelDocInfo Info = parseKernelDoc( + "my_type: A custom type definition\n"); + + EXPECT_EQ(Info.Brief, "A custom type definition"); +} + +TEST(KernelDoc, ParseColonBriefWithSpace) { + KernelDocInfo Info = parseKernelDoc( + "func() : Brief with space before colon\n"); + + EXPECT_EQ(Info.Brief, "Brief with space before colon"); +} + +TEST(KernelDoc, ParseColonBriefNotRSTDC) { + // "name::" should NOT be treated as a colon-style brief — it's + // a RST literal block marker. + KernelDocInfo Info = parseKernelDoc("example::\n"); + // Should fall through to plain text brief, not extract empty brief + EXPECT_EQ(Info.Brief, "example::"); +} + } // namespace clangd } // namespace clang _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
