[clang] Associated documentation comments with macro definitions (PR #198452)
via cfe-commits
cfe-commits at lists.llvm.org
Tue May 19 00:17:33 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-clang
Author: Doug Gregor (DougGregor)
<details>
<summary>Changes</summary>
Generalize the infrastructure that finds comments attached to declarations to also find comments attached to macros. Surface these comments through libclang and the symbol graph.
Fixes issue #<!-- -->83819.
---
Patch is 23.57 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/198452.diff
8 Files Affected:
- (modified) clang/include/clang/AST/ASTContext.h (+23-14)
- (modified) clang/include/clang/ExtractAPI/API.h (+3-2)
- (modified) clang/lib/AST/ASTContext.cpp (+72-18)
- (modified) clang/lib/ExtractAPI/ExtractAPIConsumer.cpp (+17-8)
- (added) clang/test/ExtractAPI/macro_doc_comments.c (+67)
- (added) clang/test/Index/annotate-comments-macros.c (+27)
- (modified) clang/test/Index/annotate-comments.cpp (+2-1)
- (modified) clang/tools/libclang/CIndex.cpp (+27-23)
``````````diff
diff --git a/clang/include/clang/AST/ASTContext.h b/clang/include/clang/AST/ASTContext.h
index b2fd522e6865c..5e95c1971f33d 100644
--- a/clang/include/clang/AST/ASTContext.h
+++ b/clang/include/clang/AST/ASTContext.h
@@ -30,6 +30,7 @@
#include "clang/Basic/LLVM.h"
#include "clang/Basic/PartialDiagnostic.h"
#include "clang/Basic/SourceLocation.h"
+#include "clang/Lex/MacroInfo.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/DenseMapInfo.h"
#include "llvm/ADT/DenseSet.h"
@@ -996,11 +997,16 @@ class ASTContext : public RefCountedBase<ASTContext> {
/// True if comments are already loaded from ExternalASTSource.
mutable bool CommentsLoaded = false;
- /// Mapping from declaration to directly attached comment.
+ /// Key used to look up the raw comment attached to a declaration or macro.
+ using RawCommentLookupKey =
+ llvm::PointerUnion<const Decl *, const MacroInfo *>;
+
+ /// Mapping from declaration or macro to directly attached comment.
///
/// Raw comments are owned by Comments list. This mapping is populated
/// lazily.
- mutable llvm::DenseMap<const Decl *, const RawComment *> DeclRawComments;
+ mutable llvm::DenseMap<RawCommentLookupKey, const RawComment *>
+ DeclRawComments;
/// Mapping from canonical declaration to the first redeclaration in chain
/// that has a comment attached.
@@ -1022,37 +1028,40 @@ class ASTContext : public RefCountedBase<ASTContext> {
/// redeclaration.
mutable llvm::DenseMap<const Decl *, comments::FullComment *> ParsedComments;
- /// Attaches \p Comment to \p OriginalD and to its redeclaration chain
- /// and removes the redeclaration chain from the set of commentless chains.
+ /// Attaches \p Comment to \p Original (a declaration or macro), and to its
+ /// redeclaration chain when \p Original is a declaration. Removes the
+ /// redeclaration chain from the set of commentless chains.
///
- /// Don't do anything if a comment has already been attached to \p OriginalD
+ /// Don't do anything if a comment has already been attached to \p Original
/// or its redeclaration chain.
- void cacheRawCommentForDecl(const Decl &OriginalD,
+ void cacheRawCommentForDecl(RawCommentLookupKey Original,
const RawComment &Comment) const;
- /// \returns searches \p CommentsInFile for doc comment for \p D.
+ /// \returns searches \p CommentsInFile for doc comment for \p Key.
///
/// \p RepresentativeLocForDecl is used as a location for searching doc
/// comments. \p CommentsInFile is a mapping offset -> comment of files in the
/// same file where \p RepresentativeLocForDecl is.
RawComment *getRawCommentForDeclNoCacheImpl(
- const Decl *D, const SourceLocation RepresentativeLocForDecl,
+ RawCommentLookupKey Key, const SourceLocation RepresentativeLocForDecl,
const std::map<unsigned, RawComment *> &CommentsInFile) const;
- /// Return the documentation comment attached to a given declaration,
- /// without looking into cache.
- RawComment *getRawCommentForDeclNoCache(const Decl *D) const;
+ /// Return the documentation comment attached to a given declaration or
+ /// macro, without looking into cache.
+ RawComment *getRawCommentForDeclNoCache(RawCommentLookupKey Key) const;
public:
void addComment(const RawComment &RC);
- /// Return the documentation comment attached to a given declaration.
- /// Returns nullptr if no comment is attached.
+ /// Return the documentation comment attached to a given declaration or
+ /// macro. Returns nullptr if no comment is attached.
///
/// \param OriginalDecl if not nullptr, is set to declaration AST node that
/// had the comment, if the comment we found comes from a redeclaration.
+ /// Macros have no redeclaration chain, so this is set to nullptr when
+ /// \p Key is a \c MacroInfo.
const RawComment *
- getRawCommentForAnyRedecl(const Decl *D,
+ getRawCommentForAnyRedecl(RawCommentLookupKey Key,
const Decl **OriginalDecl = nullptr) const;
/// Searches existing comments for doc comments that should be attached to \p
diff --git a/clang/include/clang/ExtractAPI/API.h b/clang/include/clang/ExtractAPI/API.h
index bea5416a626b9..62483bcf2cdc9 100644
--- a/clang/include/clang/ExtractAPI/API.h
+++ b/clang/include/clang/ExtractAPI/API.h
@@ -1380,11 +1380,12 @@ struct ObjCProtocolRecord : ObjCContainerRecord {
/// This holds information associated with macro definitions.
struct MacroDefinitionRecord : APIRecord {
MacroDefinitionRecord(StringRef USR, StringRef Name, SymbolReference Parent,
- PresumedLoc Loc, DeclarationFragments Declaration,
+ PresumedLoc Loc, const DocComment &Comment,
+ DeclarationFragments Declaration,
DeclarationFragments SubHeading,
bool IsFromSystemHeader)
: APIRecord(RK_MacroDefinition, USR, Name, Parent, Loc,
- AvailabilityInfo(), LinkageInfo(), {}, Declaration,
+ AvailabilityInfo(), LinkageInfo(), Comment, Declaration,
SubHeading, IsFromSystemHeader) {}
static bool classof(const APIRecord *Record) {
diff --git a/clang/lib/AST/ASTContext.cpp b/clang/lib/AST/ASTContext.cpp
index bc4771aec77d1..6621789a7c1a8 100644
--- a/clang/lib/AST/ASTContext.cpp
+++ b/clang/lib/AST/ASTContext.cpp
@@ -66,6 +66,7 @@
#include "clang/Basic/TargetCXXABI.h"
#include "clang/Basic/TargetInfo.h"
#include "clang/Basic/XRayLists.h"
+#include "clang/Lex/MacroInfo.h"
#include "llvm/ADT/APFixedPoint.h"
#include "llvm/ADT/APInt.h"
#include "llvm/ADT/APSInt.h"
@@ -116,9 +117,38 @@ enum FloatingRank {
};
/// \returns The locations that are relevant when searching for Doc comments
-/// related to \p D.
+/// related to \p Key.
static SmallVector<SourceLocation, 2>
-getDeclLocsForCommentSearch(const Decl *D, SourceManager &SourceMgr) {
+getDeclLocsForCommentSearch(ASTContext::RawCommentLookupKey Key,
+ SourceManager &SourceMgr) {
+ if (const auto *MI = dyn_cast<const MacroInfo *>(Key)) {
+ SourceLocation DefLoc = MI->getDefinitionLoc();
+ if (DefLoc.isInvalid() || !DefLoc.isFileID())
+ return {};
+
+ // The macro's definition location points at its name (e.g. FOO in
+ // `#define FOO 1`). The text between a preceding documentation comment
+ // and the name contains the `#define` directive itself, which would be
+ // rejected by the preprocessor-directive guard in
+ // getRawCommentForDeclNoCacheImpl. Walk back to the leading `#` so that
+ // the guard only fires when something *else* sits between the comment
+ // and our directive.
+ FileIDAndOffset Decomposed = SourceMgr.getDecomposedLoc(DefLoc);
+ bool Invalid = false;
+ StringRef Buffer = SourceMgr.getBufferData(Decomposed.first, &Invalid);
+ if (Invalid)
+ return {};
+ unsigned Offset = Decomposed.second;
+ while (Offset > 0 && Buffer[Offset - 1] != '#' &&
+ Buffer[Offset - 1] != '\n')
+ --Offset;
+ if (Offset > 0 && Buffer[Offset - 1] == '#')
+ --Offset;
+ return {SourceMgr.getLocForStartOfFile(Decomposed.first)
+ .getLocWithOffset(Offset)};
+ }
+
+ const auto *D = cast<const Decl *>(Key);
assert(D);
// User can not attach documentation to implicit declarations.
@@ -215,7 +245,7 @@ getDeclLocsForCommentSearch(const Decl *D, SourceManager &SourceMgr) {
}
RawComment *ASTContext::getRawCommentForDeclNoCacheImpl(
- const Decl *D, const SourceLocation RepresentativeLocForDecl,
+ RawCommentLookupKey Key, const SourceLocation RepresentativeLocForDecl,
const std::map<unsigned, RawComment *> &CommentsInTheFile) const {
// If the declaration doesn't map directly to a location in a file, we
// can't find the comment.
@@ -227,6 +257,9 @@ RawComment *ASTContext::getRawCommentForDeclNoCacheImpl(
if (CommentsInTheFile.empty())
return nullptr;
+ const auto *D = dyn_cast<const Decl *>(Key);
+ const bool IsMacro = isa<const MacroInfo *>(Key);
+
// Decompose the location for the declaration and find the beginning of the
// file buffer.
const FileIDAndOffset DeclLocDecomp =
@@ -242,8 +275,10 @@ RawComment *ASTContext::getRawCommentForDeclNoCacheImpl(
if ((CommentBehindDecl->isDocumentation() ||
LangOpts.CommentOpts.ParseAllComments) &&
CommentBehindDecl->isTrailingComment() &&
- (isa<FieldDecl>(D) || isa<EnumConstantDecl>(D) || isa<VarDecl>(D) ||
- isa<ObjCMethodDecl>(D) || isa<ObjCPropertyDecl>(D))) {
+ (IsMacro ||
+ (D && (isa<FieldDecl>(D) || isa<EnumConstantDecl>(D) ||
+ isa<VarDecl>(D) || isa<ObjCMethodDecl>(D) ||
+ isa<ObjCPropertyDecl>(D))))) {
// Check that Doxygen trailing comment comes after the declaration, starts
// on the same line and in the same file as the declaration.
@@ -292,8 +327,9 @@ RawComment *ASTContext::getRawCommentForDeclNoCacheImpl(
return CommentBeforeDecl;
}
-RawComment *ASTContext::getRawCommentForDeclNoCache(const Decl *D) const {
- const auto DeclLocs = getDeclLocsForCommentSearch(D, SourceMgr);
+RawComment *
+ASTContext::getRawCommentForDeclNoCache(RawCommentLookupKey Key) const {
+ const auto DeclLocs = getDeclLocsForCommentSearch(Key, SourceMgr);
for (const auto DeclLoc : DeclLocs) {
// If the declaration doesn't map directly to a location in a file, we
@@ -318,7 +354,7 @@ RawComment *ASTContext::getRawCommentForDeclNoCache(const Decl *D) const {
continue;
if (RawComment *Comment =
- getRawCommentForDeclNoCacheImpl(D, DeclLoc, *CommentsInThisFile))
+ getRawCommentForDeclNoCacheImpl(Key, DeclLoc, *CommentsInThisFile))
return Comment;
}
@@ -332,14 +368,30 @@ void ASTContext::addComment(const RawComment &RC) {
}
const RawComment *ASTContext::getRawCommentForAnyRedecl(
- const Decl *D,
+ RawCommentLookupKey Key,
const Decl **OriginalDecl) const {
- if (!D) {
+ if (Key.isNull()) {
if (OriginalDecl)
- OriginalDecl = nullptr;
+ *OriginalDecl = nullptr;
return nullptr;
}
+ // Macros have no redeclaration chain: look up directly, populate the cache,
+ // and return.
+ if (const auto *MI = dyn_cast<const MacroInfo *>(Key)) {
+ if (OriginalDecl)
+ *OriginalDecl = nullptr;
+ auto Existing = DeclRawComments.find(Key);
+ if (Existing != DeclRawComments.end())
+ return Existing->second;
+ if (const RawComment *RC = getRawCommentForDeclNoCache(Key)) {
+ cacheRawCommentForDecl(MI, *RC);
+ return RC;
+ }
+ return nullptr;
+ }
+
+ const Decl *D = cast<const Decl *>(Key);
D = &adjustDeclToTemplate(*D);
// Any comment directly attached to D?
@@ -400,7 +452,7 @@ const RawComment *ASTContext::getRawCommentForAnyRedecl(
}
const RawComment *RedeclComment = getRawCommentForDeclNoCache(Redecl);
if (RedeclComment) {
- cacheRawCommentForDecl(*Redecl, *RedeclComment);
+ cacheRawCommentForDecl(Redecl, *RedeclComment);
if (OriginalDecl)
*OriginalDecl = Redecl;
return RedeclComment;
@@ -413,13 +465,15 @@ const RawComment *ASTContext::getRawCommentForAnyRedecl(
return nullptr;
}
-void ASTContext::cacheRawCommentForDecl(const Decl &OriginalD,
+void ASTContext::cacheRawCommentForDecl(RawCommentLookupKey Original,
const RawComment &Comment) const {
assert(Comment.isDocumentation() || LangOpts.CommentOpts.ParseAllComments);
- DeclRawComments.try_emplace(&OriginalD, &Comment);
- const Decl *const CanonicalDecl = OriginalD.getCanonicalDecl();
- RedeclChainComments.try_emplace(CanonicalDecl, &OriginalD);
- CommentlessRedeclChains.erase(CanonicalDecl);
+ DeclRawComments.try_emplace(Original, &Comment);
+ if (const auto *D = dyn_cast<const Decl *>(Original)) {
+ const Decl *const CanonicalDecl = D->getCanonicalDecl();
+ RedeclChainComments.try_emplace(CanonicalDecl, D);
+ CommentlessRedeclChains.erase(CanonicalDecl);
+ }
}
static void addRedeclaredMethods(const ObjCMethodDecl *ObjCMethod,
@@ -492,7 +546,7 @@ void ASTContext::attachCommentsToJustParsedDecls(ArrayRef<Decl *> Decls,
if (RawComment *const DocComment = getRawCommentForDeclNoCacheImpl(
D, DeclLoc, *CommentsInThisFile)) {
- cacheRawCommentForDecl(*D, *DocComment);
+ cacheRawCommentForDecl(D, *DocComment);
comments::FullComment *FC = DocComment->parse(*this, PP, D);
ParsedComments[D->getCanonicalDecl()] = FC;
break;
diff --git a/clang/lib/ExtractAPI/ExtractAPIConsumer.cpp b/clang/lib/ExtractAPI/ExtractAPIConsumer.cpp
index 20389b5e4149d..92c722d9f06bb 100644
--- a/clang/lib/ExtractAPI/ExtractAPIConsumer.cpp
+++ b/clang/lib/ExtractAPI/ExtractAPIConsumer.cpp
@@ -282,8 +282,9 @@ class ExtractAPIConsumer : public ASTConsumer {
class MacroCallback : public PPCallbacks {
public:
- MacroCallback(const SourceManager &SM, APISet &API, Preprocessor &PP)
- : SM(SM), API(API), PP(PP) {}
+ MacroCallback(ASTContext &Ctx, const SourceManager &SM, APISet &API,
+ Preprocessor &PP)
+ : Ctx(Ctx), SM(SM), API(API), PP(PP) {}
void EndOfMainFile() override {
for (const auto &M : PP.macros()) {
@@ -321,8 +322,13 @@ class MacroCallback : public PPCallbacks {
PresumedLoc Loc = SM.getPresumedLoc(DefLoc);
SmallString<128> USR;
index::generateUSRForMacro(Name, DefLoc, SM, USR);
+
+ DocComment Comment;
+ if (const auto *RC = Ctx.getRawCommentForAnyRedecl(MI))
+ Comment = RC->getFormattedLines(SM, Ctx.getDiagnostics());
+
API.createRecord<extractapi::MacroDefinitionRecord>(
- USR, Name, SymbolReference(), Loc,
+ USR, Name, SymbolReference(), Loc, Comment,
DeclarationFragmentsBuilder::getFragmentsForMacro(Name, MI),
DeclarationFragmentsBuilder::getSubHeadingForMacro(Name),
SM.isInSystemHeader(DefLoc));
@@ -334,6 +340,7 @@ class MacroCallback : public PPCallbacks {
return true;
}
+ ASTContext &Ctx;
const SourceManager &SM;
APISet &API;
Preprocessor &PP;
@@ -341,9 +348,9 @@ class MacroCallback : public PPCallbacks {
class APIMacroCallback : public MacroCallback {
public:
- APIMacroCallback(const SourceManager &SM, APISet &API, Preprocessor &PP,
- LocationFileChecker &LCF)
- : MacroCallback(SM, API, PP), LCF(LCF) {}
+ APIMacroCallback(ASTContext &Ctx, const SourceManager &SM, APISet &API,
+ Preprocessor &PP, LocationFileChecker &LCF)
+ : MacroCallback(Ctx, SM, API, PP), LCF(LCF) {}
bool shouldMacroBeIncluded(const SourceLocation &MacroLoc,
StringRef ModuleName) override {
@@ -415,7 +422,8 @@ ExtractAPIAction::CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
auto LCF = std::make_unique<LocationFileChecker>(CI, KnownInputFiles);
CI.getPreprocessor().addPPCallbacks(std::make_unique<APIMacroCallback>(
- CI.getSourceManager(), *API, CI.getPreprocessor(), *LCF));
+ CI.getASTContext(), CI.getSourceManager(), *API, CI.getPreprocessor(),
+ *LCF));
// Do not include location in anonymous decls.
PrintingPolicy Policy = CI.getASTContext().getPrintingPolicy();
@@ -519,7 +527,8 @@ WrappingExtractAPIAction::CreateASTConsumer(CompilerInstance &CI,
CI.getFrontendOpts().Inputs.back().getKind().getLanguage(), ProductName);
CI.getPreprocessor().addPPCallbacks(std::make_unique<MacroCallback>(
- CI.getSourceManager(), *API, CI.getPreprocessor()));
+ CI.getASTContext(), CI.getSourceManager(), *API,
+ CI.getPreprocessor()));
// Do not include location in anonymous decls.
PrintingPolicy Policy = CI.getASTContext().getPrintingPolicy();
diff --git a/clang/test/ExtractAPI/macro_doc_comments.c b/clang/test/ExtractAPI/macro_doc_comments.c
new file mode 100644
index 0000000000000..d1d5e41574eb7
--- /dev/null
+++ b/clang/test/ExtractAPI/macro_doc_comments.c
@@ -0,0 +1,67 @@
+// RUN: rm -rf %t
+// RUN: %clang_cc1 -extract-api --pretty-sgf --emit-sgf-symbol-labels-for-testing --product-name=MacroDoc -triple arm64-apple-macosx \
+// RUN: -fretain-comments-from-system-headers -isystem %S -x objective-c-header %s -o %t/output.symbols.json
+
+// Verify documentation comments attached to `#define` macros end up in the
+// symbol graph as a `docComment` block.
+
+// RUN: FileCheck %s --input-file %t/output.symbols.json --check-prefix HELLO
+/// The greeting count.
+#define HELLO 1
+// HELLO-LABEL: "!testLabel": "c:@macro at HELLO"
+// HELLO: "docComment": {
+// HELLO-NEXT: "lines": [
+// HELLO: "text": "The greeting count."
+// HELLO: ]
+// HELLO-NEXT: },
+
+// A C-style block doc comment also attaches.
+// RUN: FileCheck %s --input-file %t/output.symbols.json --check-prefix BLOCK
+/**
+ * \brief A version number.
+ */
+#define VERSION 42
+// BLOCK-LABEL: "!testLabel": "c:@macro at VERSION"
+// BLOCK: "docComment": {
+// BLOCK-NEXT: "lines": [
+// BLOCK: "text": " \\brief A version number."
+// BLOCK: ]
+// BLOCK-NEXT: },
+
+// Function-like macros pick up their preceding doc comment.
+// RUN: FileCheck %s --input-file %t/output.symbols.json --check-prefix SUM
+/// Add two numbers.
+#define SUM(x, y) ((x) + (y))
+// SUM-LABEL: "!testLabel": "c:@macro at SUM"
+// SUM: "docComment": {
+// SUM-NEXT: "lines": [
+// SUM: "text": "Add two numbers."
+// SUM: ]
+// SUM-NEXT: },
+
+// Trailing `///<` doc comments attach to macros, just like fields/enumerators.
+// RUN: FileCheck %s --input-file %t/output.symbols.json --check-prefix TRAIL
+#define ANSWER 42 ///< The answer.
+// TRAIL-LABEL: "!testLabel": "c:@macro at ANSWER"
+// TRAIL: "docComment": {
+// TRAIL-NEXT: "lines": [
+// TRAIL: "text": "The answer."
+// TRAIL: ]
+// TRAIL-NEXT: },
+
+// A macro with no documentation comment must NOT have a `docComment` key.
+// RUN: FileCheck %s --input-file %t/output.symbols.json --check-prefix BARE
+#define BARE 7
+// BARE-LABEL: "!testLabel": "c:@macro at BARE"
+// BARE-NOT: "docComment"
+// BARE: "kind":
+
+// A non-doc `//` comment must not be attached as a docComment.
+// RUN: FileCheck %s --input-file %t/output.symbols.json --check-prefix PLAIN
+// just an ordinary comment, not Doxygen
+#define PLAIN 8
+// PLAIN-LABEL: "!testLabel": "c:@macro at PLAIN"
+// PLAIN-NOT: "docComment"
+// PLAIN: "kind":
+
+// expected-no-diagnostics
diff --git a/clang/test/Index/annotate-comments-macros.c b/clang/test/Index/annotate-comments-macros.c
new file mode 100644
index 0000000000000..0dfa52906a04a
--- /dev/null
+++ b/clang/test/Index/annotate-comments-macros.c
@@ -0,0 +1,27 @@
+// Run lines are sensitive to line numbers and come below the code.
+
+/// Greeting count.
+#define HELLO 1
+
+#define BARE 2
+
+/**
+ * \brief Add two numbers.
+ */
+#define SUM(x, y) ((x) + (y))
+
+#define TRAILING 3 ///< Trailing macro doc.
+
+// RUN: c-index-test -test-load-source all %s | FileCheck %s
+
+// Documented object-like macro picks up the preceding `///` comment.
+// CHECK: annotate-comments-macros.c:4:9: macro definition=HELLO RawComment=[/// Greeting count.] RawCommentRange=[3:1 - 3:20] BriefComment=[Greeting count.]
+
+// A macro without any preceding doc comment must not have a RawComment.
+// CHECK: annotate-comments-macros.c:6:9: macro definition=BARE Extent=
+
+// Function-like macros are documented just like object-like ones.
+// CHECK: annotate-comments-macros.c:11:9: macro definition=SUM RawComment=[/**\n * \brief Add two numbers.\n */] RawCommentRange=[8:1 - 10:4] BriefComment=[Add two numbers.]
+
+// Trailing `///<` comments attach to macros, mirroring fields/enumerators.
+// CHECK: annotate-comments-macros.c:13:9: macro definition=TRAILING RawComment=[///< Trailing macro doc.] RawCommentRange=[13:20 - 13:44] BriefComment=[Trailing macro doc.]
diff --git a/clang/test/Index/annotate-comments.cpp b/clang/test/Index/annotate-comments.cpp
index 6f9f8f0bbbc9e..17e7beb1b7ca6 100644
--- a/clang/test/Index/annotate-comments.cpp
+++ b/clang/test/Index/annotate-comments.cpp
@@ -204,7 +204,7 @@ void isdoxy45(void);
/// Ggg. IS_DOXYGEN_E...
[truncated]
``````````
</details>
https://github.com/llvm/llvm-project/pull/198452
More information about the cfe-commits
mailing list