=?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

Reply via email to