hdoc 9f04d75b2b
[Clang][Comments] Attach comments to decl even if preproc directives are in between (#88367)
### Background

It's surprisingly common for C++ code in the wild to conditionally
show/hide declarations to Doxygen through the use of preprocessor
directives. One especially common version of this pattern is
demonstrated below:

```cpp
/// @brief Test comment
#ifdef DOXYGEN_BUILD_ENABLED
template<typename T>
#else
template <typename T>
typename std::enable_if<std::is_integral<T>::value>::type
#endif
void f() {}
```

There are more examples I've collected below to demonstrate usage of
this pattern:
- Example 1:
[Magnum](8538610fa2/src/Magnum/Resource.h (L117-L127))
- Example 2:
[libcds](9985d2a87f/cds/container/michael_list_nogc.h (L36-L54))
- Example 3:
[rocPRIM](609ae19565/rocprim/include/rocprim/block/detail/block_reduce_raking_reduce.hpp (L60-L65))
 
From my research, it seems like the most common rationale for this
functionality is hiding difficult-to-parse code from Doxygen, especially
where template metaprogramming is concerned.

Currently, Clang does not support attaching comments to decls if there
are preprocessor comments between the comment and the decl. This is
enforced here:
b6ebea7972/clang/lib/AST/ASTContext.cpp (L284-L287)

Alongside preprocessor directives, any instance of `;{}#@` between a
comment and decl will cause the comment to not be attached to the decl.

#### Rationale

It would be nice for Clang-based documentation tools, such as
[hdoc](https://hdoc.io), to support code using this pattern. Users
expect to see comments attached to the relevant decl — even if there is
an `#ifdef` in the way — which Clang does not currently do.

#### History

Originally, commas were also in the list of "banned" characters, but
were removed in `b534d3a0ef69`
([link](b534d3a0ef))
because availability macros often have commas in them. From my reading
of the code, it appears that the original intent of the code was to
exclude macros and decorators between comments and decls, possibly in an
attempt to properly attribute comments to macros (discussed further in
"Complications", below). There's some more discussion here:
https://reviews.llvm.org/D125061.

### Change

This modifies Clang comment parsing so that comments are attached to
subsequent declarations even if there are preprocessor directives
between the end of the comment and the start of the decl. Furthermore,
this change:

- Adds tests to verify that comments are attached to their associated
decls even if there are preprocessor directives in between
- Adds tests to verify that current behavior has not changed (i.e. use
of the other characters between comment and decl will result in the
comment not being attached to the decl)
- Updates existing `lit` tests which would otherwise break.

#### Complications

Clang [does not yet
support](https://github.com/llvm/llvm-project/issues/38206) attaching
doc comments to macros. Consequently, the change proposed in this RFC
affects cases where a doc comment attached to a macro is followed
immediately by a normal declaration. In these cases, the macro's doc
comments will be attached to the subsequent decl. Previously they would
be ignored because any preprocessor directives between a comment and a
decl would result in the comment not being attached to the decl. An
example of this is shown below.

```cpp
/// Doc comment for a function-like macro
/// @param n
///    A macro argument
#define custom_sqrt(n) __internal_sqrt(n)

int __internal_sqrt(int n) { return __builtin_sqrt(n); }

// NB: the doc comment for the custom_sqrt macro will actually be attached to __internal_sqrt!
```

There is a real instance of this problem in the Clang codebase, namely
here:
be10070f91/clang/lib/Headers/amxcomplexintrin.h (L65-L114)

As part of this RFC, I've added a semicolon to break up the Clang
comment parsing so that the `-Wdocumentation` errors go away, but this
is a hack. The real solution is to fix Clang comment parsing so that doc
comments are properly attached to macros, however this would be a large
change that is outside of the scope of this RFC.
2024-07-01 08:47:26 -04:00

889 lines
25 KiB
C++

//===- unittests/AST/DeclTest.cpp --- Declaration tests -------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// Unit tests for Decl nodes in the AST.
//
//===----------------------------------------------------------------------===//
#include "clang/AST/Decl.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/DeclTemplate.h"
#include "clang/AST/Mangle.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Basic/Diagnostic.h"
#include "clang/Basic/LLVM.h"
#include "clang/Basic/TargetInfo.h"
#include "clang/Lex/Lexer.h"
#include "clang/Tooling/Tooling.h"
#include "llvm/IR/DataLayout.h"
#include "llvm/Testing/Annotations/Annotations.h"
#include "gtest/gtest.h"
using namespace clang::ast_matchers;
using namespace clang::tooling;
using namespace clang;
TEST(Decl, CleansUpAPValues) {
MatchFinder Finder;
std::unique_ptr<FrontendActionFactory> Factory(
newFrontendActionFactory(&Finder));
// This is a regression test for a memory leak in APValues for structs that
// allocate memory. This test only fails if run under valgrind with full leak
// checking enabled.
std::vector<std::string> Args(1, "-std=c++11");
Args.push_back("-fno-ms-extensions");
ASSERT_TRUE(runToolOnCodeWithArgs(
Factory->create(),
"struct X { int a; }; constexpr X x = { 42 };"
"union Y { constexpr Y(int a) : a(a) {} int a; }; constexpr Y y = { 42 };"
"constexpr int z[2] = { 42, 43 };"
"constexpr int __attribute__((vector_size(16))) v1 = {};"
"\n#ifdef __SIZEOF_INT128__\n"
"constexpr __uint128_t large_int = 0xffffffffffffffff;"
"constexpr __uint128_t small_int = 1;"
"\n#endif\n"
"constexpr double d1 = 42.42;"
"constexpr long double d2 = 42.42;"
"constexpr _Complex long double c1 = 42.0i;"
"constexpr _Complex long double c2 = 42.0;"
"template<int N> struct A : A<N-1> {};"
"template<> struct A<0> { int n; }; A<50> a;"
"constexpr int &r = a.n;"
"constexpr int A<50>::*p = &A<50>::n;"
"void f() { foo: bar: constexpr int k = __builtin_constant_p(0) ?"
" (char*)&&foo - (char*)&&bar : 0; }",
Args));
// FIXME: Once this test starts breaking we can test APValue::needsCleanup
// for ComplexInt.
ASSERT_FALSE(runToolOnCodeWithArgs(
Factory->create(),
"constexpr _Complex __uint128_t c = 0xffffffffffffffff;",
Args));
}
TEST(Decl, AsmLabelAttr) {
// Create two method decls: `f` and `g`.
StringRef Code = R"(
struct S {
void f() {}
void g() {}
};
)";
auto AST =
tooling::buildASTFromCodeWithArgs(Code, {"-target", "i386-apple-darwin"});
ASTContext &Ctx = AST->getASTContext();
assert(Ctx.getTargetInfo().getUserLabelPrefix() == StringRef("_") &&
"Expected target to have a global prefix");
DiagnosticsEngine &Diags = AST->getDiagnostics();
const auto *DeclS =
selectFirst<CXXRecordDecl>("d", match(cxxRecordDecl().bind("d"), Ctx));
NamedDecl *DeclF = *DeclS->method_begin();
NamedDecl *DeclG = *(++DeclS->method_begin());
// Attach asm labels to the decls: one literal, and one not.
DeclF->addAttr(AsmLabelAttr::Create(Ctx, "foo", /*LiteralLabel=*/true));
DeclG->addAttr(AsmLabelAttr::Create(Ctx, "goo", /*LiteralLabel=*/false));
// Mangle the decl names.
std::string MangleF, MangleG;
std::unique_ptr<ItaniumMangleContext> MC(
ItaniumMangleContext::create(Ctx, Diags));
{
llvm::raw_string_ostream OS_F(MangleF);
llvm::raw_string_ostream OS_G(MangleG);
MC->mangleName(DeclF, OS_F);
MC->mangleName(DeclG, OS_G);
}
ASSERT_TRUE(0 == MangleF.compare("\x01" "foo"));
ASSERT_TRUE(0 == MangleG.compare("goo"));
}
TEST(Decl, MangleDependentSizedArray) {
StringRef Code = R"(
template <int ...N>
int A[] = {N...};
template <typename T, int N>
struct S {
T B[N];
};
)";
auto AST =
tooling::buildASTFromCodeWithArgs(Code, {"-target", "i386-apple-darwin"});
ASTContext &Ctx = AST->getASTContext();
assert(Ctx.getTargetInfo().getUserLabelPrefix() == StringRef("_") &&
"Expected target to have a global prefix");
DiagnosticsEngine &Diags = AST->getDiagnostics();
const auto *DeclA =
selectFirst<VarDecl>("A", match(varDecl().bind("A"), Ctx));
const auto *DeclB =
selectFirst<FieldDecl>("B", match(fieldDecl().bind("B"), Ctx));
std::string MangleA, MangleB;
llvm::raw_string_ostream OS_A(MangleA), OS_B(MangleB);
std::unique_ptr<ItaniumMangleContext> MC(
ItaniumMangleContext::create(Ctx, Diags));
MC->mangleCanonicalTypeName(DeclA->getType(), OS_A);
MC->mangleCanonicalTypeName(DeclB->getType(), OS_B);
ASSERT_TRUE(0 == MangleA.compare("_ZTSA_i"));
ASSERT_TRUE(0 == MangleB.compare("_ZTSAT0__T_"));
}
TEST(Decl, ConceptDecl) {
llvm::StringRef Code(R"(
template<class T>
concept integral = __is_integral(T);
)");
auto AST = tooling::buildASTFromCodeWithArgs(Code, {"-std=c++20"});
ASTContext &Ctx = AST->getASTContext();
const auto *Decl =
selectFirst<ConceptDecl>("decl", match(conceptDecl().bind("decl"), Ctx));
ASSERT_TRUE(Decl != nullptr);
EXPECT_EQ(Decl->getName(), "integral");
}
TEST(Decl, EnumDeclRange) {
llvm::Annotations Code(R"(
typedef int Foo;
[[enum Bar : Foo]];)");
auto AST = tooling::buildASTFromCodeWithArgs(Code.code(), /*Args=*/{});
ASTContext &Ctx = AST->getASTContext();
const auto &SM = Ctx.getSourceManager();
const auto *Bar =
selectFirst<TagDecl>("Bar", match(enumDecl().bind("Bar"), Ctx));
auto BarRange =
Lexer::getAsCharRange(Bar->getSourceRange(), SM, Ctx.getLangOpts());
EXPECT_EQ(SM.getFileOffset(BarRange.getBegin()), Code.range().Begin);
EXPECT_EQ(SM.getFileOffset(BarRange.getEnd()), Code.range().End);
}
TEST(Decl, IsInExportDeclContext) {
llvm::Annotations Code(R"(
export module m;
export template <class T>
void f() {})");
auto AST =
tooling::buildASTFromCodeWithArgs(Code.code(), /*Args=*/{"-std=c++20"});
ASTContext &Ctx = AST->getASTContext();
const auto *f =
selectFirst<FunctionDecl>("f", match(functionDecl().bind("f"), Ctx));
EXPECT_TRUE(f->isInExportDeclContext());
}
TEST(Decl, InConsistLinkageForTemplates) {
llvm::Annotations Code(R"(
export module m;
export template <class T>
void f() {}
template <>
void f<int>() {}
export template <class T>
class C {};
template<>
class C<int> {};
)");
auto AST =
tooling::buildASTFromCodeWithArgs(Code.code(), /*Args=*/{"-std=c++20"});
ASTContext &Ctx = AST->getASTContext();
llvm::SmallVector<ast_matchers::BoundNodes, 2> Funcs =
match(functionDecl().bind("f"), Ctx);
EXPECT_EQ(Funcs.size(), 2U);
const FunctionDecl *TemplateF = Funcs[0].getNodeAs<FunctionDecl>("f");
const FunctionDecl *SpecializedF = Funcs[1].getNodeAs<FunctionDecl>("f");
EXPECT_EQ(TemplateF->getLinkageInternal(),
SpecializedF->getLinkageInternal());
llvm::SmallVector<ast_matchers::BoundNodes, 1> ClassTemplates =
match(classTemplateDecl().bind("C"), Ctx);
llvm::SmallVector<ast_matchers::BoundNodes, 1> ClassSpecializations =
match(classTemplateSpecializationDecl().bind("C"), Ctx);
EXPECT_EQ(ClassTemplates.size(), 1U);
EXPECT_EQ(ClassSpecializations.size(), 1U);
const NamedDecl *TemplatedC = ClassTemplates[0].getNodeAs<NamedDecl>("C");
const NamedDecl *SpecializedC = ClassSpecializations[0].getNodeAs<NamedDecl>("C");
EXPECT_EQ(TemplatedC->getLinkageInternal(),
SpecializedC->getLinkageInternal());
}
TEST(Decl, ModuleAndInternalLinkage) {
llvm::Annotations Code(R"(
export module M;
static int a;
static int f(int x);
int b;
int g(int x);)");
auto AST =
tooling::buildASTFromCodeWithArgs(Code.code(), /*Args=*/{"-std=c++20"});
ASTContext &Ctx = AST->getASTContext();
const auto *a =
selectFirst<VarDecl>("a", match(varDecl(hasName("a")).bind("a"), Ctx));
const auto *f = selectFirst<FunctionDecl>(
"f", match(functionDecl(hasName("f")).bind("f"), Ctx));
EXPECT_EQ(a->getFormalLinkage(), Linkage::Internal);
EXPECT_EQ(f->getFormalLinkage(), Linkage::Internal);
const auto *b =
selectFirst<VarDecl>("b", match(varDecl(hasName("b")).bind("b"), Ctx));
const auto *g = selectFirst<FunctionDecl>(
"g", match(functionDecl(hasName("g")).bind("g"), Ctx));
EXPECT_EQ(b->getFormalLinkage(), Linkage::Module);
EXPECT_EQ(g->getFormalLinkage(), Linkage::Module);
}
TEST(Decl, GetNonTransparentDeclContext) {
llvm::Annotations Code(R"(
export module m3;
export template <class> struct X {
template <class Self> friend void f(Self &&self) {
(Self&)self;
}
};)");
auto AST =
tooling::buildASTFromCodeWithArgs(Code.code(), /*Args=*/{"-std=c++20"});
ASTContext &Ctx = AST->getASTContext();
auto *f = selectFirst<FunctionDecl>(
"f", match(functionDecl(hasName("f")).bind("f"), Ctx));
EXPECT_TRUE(f->getNonTransparentDeclContext()->isFileContext());
}
TEST(Decl, MemberFunctionInModules) {
llvm::Annotations Code(R"(
module;
class G {
void bar() {}
};
export module M;
class A {
void foo() {}
};
)");
auto AST =
tooling::buildASTFromCodeWithArgs(Code.code(), /*Args=*/{"-std=c++20"});
ASTContext &Ctx = AST->getASTContext();
auto *foo = selectFirst<FunctionDecl>(
"foo", match(functionDecl(hasName("foo")).bind("foo"), Ctx));
// The function defined within a class definition is not implicitly inline
// if it is not attached to global module
EXPECT_FALSE(foo->isInlined());
auto *bar = selectFirst<FunctionDecl>(
"bar", match(functionDecl(hasName("bar")).bind("bar"), Ctx));
// In global module, the function defined within a class definition is
// implicitly inline.
EXPECT_TRUE(bar->isInlined());
}
TEST(Decl, MemberFunctionInHeaderUnit) {
llvm::Annotations Code(R"(
class foo {
public:
int memFn() {
return 43;
}
};
)");
auto AST = tooling::buildASTFromCodeWithArgs(
Code.code(), {"-std=c++20", " -xc++-user-header ", "-emit-header-unit"});
ASTContext &Ctx = AST->getASTContext();
auto *memFn = selectFirst<FunctionDecl>(
"memFn", match(functionDecl(hasName("memFn")).bind("memFn"), Ctx));
EXPECT_TRUE(memFn->isInlined());
}
TEST(Decl, FriendFunctionWithinClassInHeaderUnit) {
llvm::Annotations Code(R"(
class foo {
int value;
public:
foo(int v) : value(v) {}
friend int getFooValue(foo f) {
return f.value;
}
};
)");
auto AST = tooling::buildASTFromCodeWithArgs(
Code.code(), {"-std=c++20", " -xc++-user-header ", "-emit-header-unit"});
ASTContext &Ctx = AST->getASTContext();
auto *getFooValue = selectFirst<FunctionDecl>(
"getFooValue",
match(functionDecl(hasName("getFooValue")).bind("getFooValue"), Ctx));
EXPECT_TRUE(getFooValue->isInlined());
}
TEST(Decl, FunctionDeclBitsShouldNotOverlapWithCXXConstructorDeclBits) {
llvm::Annotations Code(R"(
struct A {
A() : m() {}
int m;
};
A f() { return A(); }
)");
auto AST = tooling::buildASTFromCodeWithArgs(Code.code(), {"-std=c++14"});
ASTContext &Ctx = AST->getASTContext();
auto HasCtorInit =
hasAnyConstructorInitializer(cxxCtorInitializer(isMemberInitializer()));
auto ImpMoveCtor =
cxxConstructorDecl(isMoveConstructor(), isImplicit(), HasCtorInit)
.bind("MoveCtor");
auto *ToImpMoveCtor =
selectFirst<CXXConstructorDecl>("MoveCtor", match(ImpMoveCtor, Ctx));
EXPECT_TRUE(ToImpMoveCtor->getNumCtorInitializers() == 1);
EXPECT_FALSE(ToImpMoveCtor->FriendConstraintRefersToEnclosingTemplate());
}
TEST(Decl, NoProtoFunctionDeclAttributes) {
llvm::Annotations Code(R"(
void f();
)");
auto AST = tooling::buildASTFromCodeWithArgs(
Code.code(),
/*Args=*/{"-target", "i386-apple-darwin", "-x", "objective-c",
"-std=c89"});
ASTContext &Ctx = AST->getASTContext();
auto *f = selectFirst<FunctionDecl>(
"f", match(functionDecl(hasName("f")).bind("f"), Ctx));
const auto *FPT = f->getType()->getAs<FunctionNoProtoType>();
// Functions without prototypes always have 0 initialized qualifiers
EXPECT_FALSE(FPT->isConst());
EXPECT_FALSE(FPT->isVolatile());
EXPECT_FALSE(FPT->isRestrict());
}
TEST(Decl, ImplicitlyDeclaredAllocationFunctionsInModules) {
// C++ [basic.stc.dynamic.general]p2:
// The library provides default definitions for the global allocation
// and deallocation functions. Some global allocation and deallocation
// functions are replaceable ([new.delete]); these are attached to the
// global module ([module.unit]).
llvm::Annotations Code(R"(
export module base;
export struct Base {
virtual void hello() = 0;
virtual ~Base() = default;
};
)");
auto AST =
tooling::buildASTFromCodeWithArgs(Code.code(), /*Args=*/{"-std=c++20"});
ASTContext &Ctx = AST->getASTContext();
// void* operator new(std::size_t);
auto *SizedOperatorNew = selectFirst<FunctionDecl>(
"operator new",
match(functionDecl(hasName("operator new"), parameterCountIs(1),
hasParameter(0, hasType(isUnsignedInteger())))
.bind("operator new"),
Ctx));
ASSERT_TRUE(SizedOperatorNew->getOwningModule());
EXPECT_TRUE(SizedOperatorNew->isFromExplicitGlobalModule());
// void* operator new(std::size_t, std::align_val_t);
auto *SizedAlignedOperatorNew = selectFirst<FunctionDecl>(
"operator new",
match(functionDecl(
hasName("operator new"), parameterCountIs(2),
hasParameter(0, hasType(isUnsignedInteger())),
hasParameter(1, hasType(enumDecl(hasName("std::align_val_t")))))
.bind("operator new"),
Ctx));
ASSERT_TRUE(SizedAlignedOperatorNew->getOwningModule());
EXPECT_TRUE(SizedAlignedOperatorNew->isFromExplicitGlobalModule());
// void* operator new[](std::size_t);
auto *SizedArrayOperatorNew = selectFirst<FunctionDecl>(
"operator new[]",
match(functionDecl(hasName("operator new[]"), parameterCountIs(1),
hasParameter(0, hasType(isUnsignedInteger())))
.bind("operator new[]"),
Ctx));
ASSERT_TRUE(SizedArrayOperatorNew->getOwningModule());
EXPECT_TRUE(SizedArrayOperatorNew->isFromExplicitGlobalModule());
// void* operator new[](std::size_t, std::align_val_t);
auto *SizedAlignedArrayOperatorNew = selectFirst<FunctionDecl>(
"operator new[]",
match(functionDecl(
hasName("operator new[]"), parameterCountIs(2),
hasParameter(0, hasType(isUnsignedInteger())),
hasParameter(1, hasType(enumDecl(hasName("std::align_val_t")))))
.bind("operator new[]"),
Ctx));
ASSERT_TRUE(SizedAlignedArrayOperatorNew->getOwningModule());
EXPECT_TRUE(
SizedAlignedArrayOperatorNew->isFromExplicitGlobalModule());
// void operator delete(void*) noexcept;
auto *Delete = selectFirst<FunctionDecl>(
"operator delete",
match(functionDecl(
hasName("operator delete"), parameterCountIs(1),
hasParameter(0, hasType(pointerType(pointee(voidType())))))
.bind("operator delete"),
Ctx));
ASSERT_TRUE(Delete->getOwningModule());
EXPECT_TRUE(Delete->isFromExplicitGlobalModule());
// void operator delete(void*, std::align_val_t) noexcept;
auto *AlignedDelete = selectFirst<FunctionDecl>(
"operator delete",
match(functionDecl(
hasName("operator delete"), parameterCountIs(2),
hasParameter(0, hasType(pointerType(pointee(voidType())))),
hasParameter(1, hasType(enumDecl(hasName("std::align_val_t")))))
.bind("operator delete"),
Ctx));
ASSERT_TRUE(AlignedDelete->getOwningModule());
EXPECT_TRUE(AlignedDelete->isFromExplicitGlobalModule());
// Sized deallocation is not enabled by default. So we skip it here.
// void operator delete[](void*) noexcept;
auto *ArrayDelete = selectFirst<FunctionDecl>(
"operator delete[]",
match(functionDecl(
hasName("operator delete[]"), parameterCountIs(1),
hasParameter(0, hasType(pointerType(pointee(voidType())))))
.bind("operator delete[]"),
Ctx));
ASSERT_TRUE(ArrayDelete->getOwningModule());
EXPECT_TRUE(ArrayDelete->isFromExplicitGlobalModule());
// void operator delete[](void*, std::align_val_t) noexcept;
auto *AlignedArrayDelete = selectFirst<FunctionDecl>(
"operator delete[]",
match(functionDecl(
hasName("operator delete[]"), parameterCountIs(2),
hasParameter(0, hasType(pointerType(pointee(voidType())))),
hasParameter(1, hasType(enumDecl(hasName("std::align_val_t")))))
.bind("operator delete[]"),
Ctx));
ASSERT_TRUE(AlignedArrayDelete->getOwningModule());
EXPECT_TRUE(AlignedArrayDelete->isFromExplicitGlobalModule());
}
TEST(Decl, TemplateArgumentDefaulted) {
llvm::Annotations Code(R"cpp(
template<typename T1, typename T2>
struct Alloc {};
template <typename T1,
typename T2 = double,
int T3 = 42,
typename T4 = Alloc<T1, T2>>
struct Foo {
};
Foo<char, int, 42, Alloc<char, int>> X;
)cpp");
auto AST =
tooling::buildASTFromCodeWithArgs(Code.code(), /*Args=*/{"-std=c++20"});
ASTContext &Ctx = AST->getASTContext();
auto const *CTSD = selectFirst<ClassTemplateSpecializationDecl>(
"id",
match(classTemplateSpecializationDecl(hasName("Foo")).bind("id"), Ctx));
ASSERT_NE(CTSD, nullptr);
auto const &ArgList = CTSD->getTemplateArgs();
EXPECT_FALSE(ArgList.get(0).getIsDefaulted());
EXPECT_FALSE(ArgList.get(1).getIsDefaulted());
EXPECT_TRUE(ArgList.get(2).getIsDefaulted());
EXPECT_TRUE(ArgList.get(3).getIsDefaulted());
}
TEST(Decl, CXXDestructorDeclsShouldHaveWellFormedNameInfoRanges) {
// GH71161
llvm::Annotations Code(R"cpp(
template <typename T> struct Resource {
~Resource(); // 1
};
template <typename T>
Resource<T>::~Resource() {} // 2,3
void instantiate_template() {
Resource<int> x;
}
)cpp");
auto AST = tooling::buildASTFromCode(Code.code());
ASTContext &Ctx = AST->getASTContext();
const auto &SM = Ctx.getSourceManager();
auto GetNameInfoRange = [&SM](const BoundNodes &Match) {
const auto *D = Match.getNodeAs<CXXDestructorDecl>("dtor");
return D->getNameInfo().getSourceRange().printToString(SM);
};
auto Matches = match(findAll(cxxDestructorDecl().bind("dtor")),
*Ctx.getTranslationUnitDecl(), Ctx);
ASSERT_EQ(Matches.size(), 3U);
EXPECT_EQ(GetNameInfoRange(Matches[0]), "<input.cc:3:3, col:4>");
EXPECT_EQ(GetNameInfoRange(Matches[1]), "<input.cc:6:14, col:15>");
EXPECT_EQ(GetNameInfoRange(Matches[2]), "<input.cc:6:14, col:15>");
}
TEST(Decl, CommentsAttachedToDecl1) {
const SmallVector<StringRef> Sources{
R"(
/// Test comment
void f();
)",
R"(
/// Test comment
void f();
)",
R"(
/// Test comment
#if 0
// tralala
#endif
void f();
)",
R"(
/// Test comment
#if 0
// tralala
#endif
void f();
)",
R"(
/// Test comment
#ifdef DOCS
template<typename T>
#endif
void f();
)",
R"(
/// Test comment
#ifdef DOCS
template<typename T>
#endif
void f();
)",
};
for (const auto code : Sources) {
auto AST = tooling::buildASTFromCodeWithArgs(code, /*Args=*/{"-std=c++20"});
ASTContext &Ctx = AST->getASTContext();
auto const *F = selectFirst<FunctionDecl>(
"id", match(functionDecl(hasName("f")).bind("id"), Ctx));
ASSERT_NE(F, nullptr);
auto const *C = Ctx.getRawCommentForDeclNoCache(F);
ASSERT_NE(C, nullptr);
EXPECT_EQ(C->getRawText(Ctx.getSourceManager()), "/// Test comment");
}
}
TEST(Decl, CommentsAttachedToDecl2) {
const SmallVector<StringRef> Sources{
R"(
/** Test comment
*/
void f();
)",
R"(
/** Test comment
*/
void f();
)",
R"(
/** Test comment
*/
#if 0
/* tralala */
#endif
void f();
)",
R"(
/** Test comment
*/
#if 0
/* tralala */
#endif
void f();
)",
R"(
/** Test comment
*/
#ifdef DOCS
template<typename T>
#endif
void f();
)",
R"(
/** Test comment
*/
#ifdef DOCS
template<typename T>
#endif
void f();
)",
};
for (const auto code : Sources) {
auto AST = tooling::buildASTFromCodeWithArgs(code, /*Args=*/{"-std=c++20"});
ASTContext &Ctx = AST->getASTContext();
auto const *F = selectFirst<FunctionDecl>(
"id", match(functionDecl(hasName("f")).bind("id"), Ctx));
ASSERT_NE(F, nullptr);
auto const *C = Ctx.getRawCommentForDeclNoCache(F);
ASSERT_NE(C, nullptr);
EXPECT_EQ(C->getRawText(Ctx.getSourceManager()),
"/** Test comment\n */");
}
}
TEST(Decl, CommentsAttachedToDecl3) {
const SmallVector<StringRef> Sources{
R"(
/// @brief Test comment
void f();
)",
R"(
/// @brief Test comment
void f();
)",
R"(
/// @brief Test comment
#if 0
// tralala
#endif
void f();
)",
R"(
/// @brief Test comment
#if 0
// tralala
#endif
void f();
)",
R"(
/// @brief Test comment
#ifdef DOCS
template<typename T>
#endif
void f();
)",
R"(
/// @brief Test comment
#ifdef DOCS
template<typename T>
#endif
void f();
)",
};
for (const auto code : Sources) {
auto AST = tooling::buildASTFromCodeWithArgs(code, /*Args=*/{"-std=c++20"});
ASTContext &Ctx = AST->getASTContext();
auto const *F = selectFirst<FunctionDecl>(
"id", match(functionDecl(hasName("f")).bind("id"), Ctx));
ASSERT_NE(F, nullptr);
auto const *C = Ctx.getRawCommentForDeclNoCache(F);
ASSERT_NE(C, nullptr);
EXPECT_EQ(C->getRawText(Ctx.getSourceManager()), "/// @brief Test comment");
}
}
TEST(Decl, CommentsAttachedToDecl4) {
const SmallVector<StringRef> Sources{
R"(
/** \brief Test comment
*/
void f();
)",
R"(
/** \brief Test comment
*/
void f();
)",
R"(
/** \brief Test comment
*/
#if 0
/* tralala */
#endif
void f();
)",
R"(
/** \brief Test comment
*/
#if 0
/* tralala */
#endif
void f();
)",
R"(
/** \brief Test comment
*/
#ifdef DOCS
template<typename T>
#endif
void f();
)",
R"(
/** \brief Test comment
*/
#ifdef DOCS
template<typename T>
#endif
void f();
)",
};
for (const auto code : Sources) {
auto AST = tooling::buildASTFromCodeWithArgs(code, /*Args=*/{"-std=c++20"});
ASTContext &Ctx = AST->getASTContext();
auto const *F = selectFirst<FunctionDecl>(
"id", match(functionDecl(hasName("f")).bind("id"), Ctx));
ASSERT_NE(F, nullptr);
auto const *C = Ctx.getRawCommentForDeclNoCache(F);
ASSERT_NE(C, nullptr);
EXPECT_EQ(C->getRawText(Ctx.getSourceManager()),
"/** \\brief Test comment\n */");
}
}
/// This example intentionally inserts characters between a doc comment and the
/// associated declaration to verify that the comment does not become associated
/// with the FunctionDecl.
/// By default, Clang does not allow for other declarations (aside from
/// preprocessor directives, as shown above) to be placed between a doc comment
/// and a declaration.
TEST(Decl, CommentsAttachedToDecl5) {
const SmallVector<StringRef> Sources{
R"(
/// Test comment
;
void f();
)",
R"(
/// Test comment
// @
void f();
)",
R"(
/// Test comment
// {}
void f();
)",
};
for (const auto code : Sources) {
auto AST = tooling::buildASTFromCodeWithArgs(code, /*Args=*/{"-std=c++20"});
ASTContext &Ctx = AST->getASTContext();
auto const *F = selectFirst<FunctionDecl>(
"id", match(functionDecl(hasName("f")).bind("id"), Ctx));
ASSERT_NE(F, nullptr);
auto const *C = Ctx.getRawCommentForDeclNoCache(F);
ASSERT_EQ(C, nullptr);
}
}