[clang] [clang-tools-extra] [clang] Retain unrecognized C++ [[attributes]] in the AST (PR #209224)
Vassil Vassilev via cfe-commits
cfe-commits at lists.llvm.org
Tue Jul 21 06:13:17 PDT 2026
https://github.com/vgvassilev updated https://github.com/llvm/llvm-project/pull/209224
>From 090ad551d5e5f0b51734acdc6b18398e55f3e81c Mon Sep 17 00:00:00 2001
From: Vassil Vassilev <v.g.vassilev at gmail.com>
Date: Mon, 13 Jul 2026 15:34:13 +0000
Subject: [PATCH] [clang] Retain unrecognized C++ [[attributes]] in the AST
Clang diagnoses an unrecognized C++ [[...]] attribute with -Wunknown-attributes
and then drops it, leaving no trace in the AST. Downstream consumers such as
source-to-source tools, linters, and clang plugins that read a parsed AST cannot
recover the attribute, pretty-print it, or do their own argument parsing.
Retain such an attribute as a new semantic attribute, UnknownAttr, with a
type-position counterpart UnknownType. The attribute name, scope and syntax come
from the AttributeCommonInfo base; the argument clause "(...)" is stored as its
source text, interned into the ASTContext. Storing text rather than a source
range keeps the node self-contained: it can be cloned for template
instantiation, serialized to a PCH or module, imported across ASTContexts, and
printed without a SourceManager, and it keeps non-expression argument grammars
representable (for example the C++ profiles [[profiles::suppress(type.1)]]).
The parser captures the balanced argument clause. Sema retains the attribute on
the declaration, statement or type it appertains to, wrapping the type in an
AttributedType for the type case. Retention adds no diagnostic beyond
-Wunknown-attributes: an unrecognized attribute-token is ignored per
[dcl.attr.grammar]/8 (https://eel.is/c++draft/dcl.attr.grammar#8), so it is never
reported as misapplied. -ast-print reconstructs "[[scope::name(args)]]" and
-ast-dump shows the retained name and argument text.
Retention is limited to C++ [[...]] spellings for now. GNU __attribute__ and
__declspec, and sliding an unknown attribute onto a different node, are
follow-ups.
---
.../clangd/unittests/SelectionTests.cpp | 8 ++--
.../modernize/use-trailing-return-type.cpp | 3 +-
clang/include/clang/AST/Attr.h | 7 +++
clang/include/clang/Basic/Attr.td | 39 ++++++++++++++++
clang/include/clang/Parse/Parser.h | 3 +-
clang/include/clang/Sema/ParsedAttr.h | 9 ++++
clang/include/clang/Sema/Sema.h | 9 ++++
clang/lib/AST/AttrImpl.cpp | 13 ++++++
clang/lib/AST/DeclPrinter.cpp | 12 +++++
clang/lib/AST/StmtPrinter.cpp | 7 ++-
clang/lib/AST/TextNodeDumper.cpp | 6 +++
clang/lib/AST/TypePrinter.cpp | 10 ++++
clang/lib/Parse/ParseDeclCXX.cpp | 38 +++++++++------
clang/lib/Sema/Sema.cpp | 31 +++++++++++++
clang/lib/Sema/SemaDeclAttr.cpp | 8 ++++
clang/lib/Sema/SemaStmtAttr.cpp | 8 ++++
clang/lib/Sema/SemaType.cpp | 10 ++++
.../AST/ast-dump-unknown-attr-negative.cpp | 16 +++++++
clang/test/AST/ast-dump-unknown-attr-stmt.cpp | 46 +++++++++++++++++++
.../AST/ast-dump-unknown-attr-template.cpp | 15 ++++++
clang/test/AST/ast-dump-unknown-attr-type.cpp | 18 ++++++++
clang/test/AST/ast-dump-unknown-attr.cpp | 14 ++++++
clang/test/AST/ast-print-unknown-attr.cpp | 43 +++++++++++++++++
clang/test/PCH/unknown-attr.cpp | 22 +++++++++
clang/test/SemaCXX/unknown-attr-retain.cpp | 43 +++++++++++++++++
clang/unittests/AST/ASTImporterTest.cpp | 10 ++++
clang/unittests/AST/AttrTest.cpp | 33 +++++++++++++
.../ASTMatchers/ASTMatchersNodeTest.cpp | 7 ++-
28 files changed, 464 insertions(+), 24 deletions(-)
create mode 100644 clang/test/AST/ast-dump-unknown-attr-negative.cpp
create mode 100644 clang/test/AST/ast-dump-unknown-attr-stmt.cpp
create mode 100644 clang/test/AST/ast-dump-unknown-attr-template.cpp
create mode 100644 clang/test/AST/ast-dump-unknown-attr-type.cpp
create mode 100644 clang/test/AST/ast-dump-unknown-attr.cpp
create mode 100644 clang/test/AST/ast-print-unknown-attr.cpp
create mode 100644 clang/test/PCH/unknown-attr.cpp
create mode 100644 clang/test/SemaCXX/unknown-attr-retain.cpp
diff --git a/clang-tools-extra/clangd/unittests/SelectionTests.cpp b/clang-tools-extra/clangd/unittests/SelectionTests.cpp
index 396990e6ea929..056675ba90b73 100644
--- a/clang-tools-extra/clangd/unittests/SelectionTests.cpp
+++ b/clang-tools-extra/clangd/unittests/SelectionTests.cpp
@@ -515,13 +515,15 @@ TEST(SelectionTest, CommonAncestor) {
)cpp",
"BuiltinTypeLoc"},
- // This case used to crash - AST has a null Attr
+ // This case used to crash - the AST had a null Attr. The unrecognized
+ // [[...]] attribute is now retained as an UnknownAttr, so selecting it
+ // lands on that node.
{R"cpp(
@interface I
- [[@property(retain, nonnull) <:[My^Object2]:> *x]]; // error-ok
+ @property(retain, nonnull) <:[ [[My^Object2]] ]:> *x; // error-ok
@end
)cpp",
- "ObjCPropertyDecl"},
+ "UnknownAttr"},
{R"cpp(
typedef int Foo;
diff --git a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-trailing-return-type.cpp b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-trailing-return-type.cpp
index 3b59860e2f7c8..ee1c4cfeabfed 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-trailing-return-type.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-trailing-return-type.cpp
@@ -332,9 +332,8 @@ struct D : B {
// Functions with attributes
//
-int g1() [[asdf]];
+int g1() [[asdf]]; // FunctionTypeLoc is null: the retained unknown attribute wraps the function type.
// CHECK-MESSAGES: :[[@LINE-1]]:5: warning: use a trailing return type for this function [modernize-use-trailing-return-type]
-// CHECK-FIXES: auto g1() -> int {{[[][[]}}asdf{{[]][]]}};
[[noreturn]] int g2();
// CHECK-MESSAGES: :[[@LINE-1]]:18: warning: use a trailing return type for this function [modernize-use-trailing-return-type]
// CHECK-FIXES: {{[[][[]}}noreturn{{[]][]]}} auto g2() -> int;
diff --git a/clang/include/clang/AST/Attr.h b/clang/include/clang/AST/Attr.h
index f204bb4faa9b7..db87ce64847f5 100644
--- a/clang/include/clang/AST/Attr.h
+++ b/clang/include/clang/AST/Attr.h
@@ -42,6 +42,13 @@ class OMPTraitInfo;
class OpenACCClause;
struct StructuralEquivalenceContext;
+/// Print a retained unknown attribute as "[[scope::name(args)]]", where
+/// \p ArgsText is the argument-clause text (with parentheses) or empty. Shared
+/// by the declaration and statement printers; needs no SourceManager because
+/// UnknownAttr stores the argument text rather than a source range.
+void printUnknownAttrPretty(raw_ostream &OS, const AttributeCommonInfo &Info,
+ StringRef ArgsText);
+
/// Attr - This represents one attribute.
class Attr : public AttributeCommonInfo {
private:
diff --git a/clang/include/clang/Basic/Attr.td b/clang/include/clang/Basic/Attr.td
index 1a5bd2301dfc8..02606ea7f7a99 100644
--- a/clang/include/clang/Basic/Attr.td
+++ b/clang/include/clang/Basic/Attr.td
@@ -3824,6 +3824,45 @@ def PointerAuth : TypeAttr {
let Documentation = [PtrAuthDocs];
}
+def Unknown : DeclOrStmtAttr {
+ // Retains a C++ [[...]] attribute that Clang does not recognize, so that it
+ // survives in the AST (for -ast-print round-tripping and plugin inspection)
+ // instead of being dropped after -Wunknown-attributes. Created only by the
+ // parser when it meets an unrecognized attribute; it has no spelling of its
+ // own and is never matched by name.
+ //
+ // The attribute name, scope and syntax come from the AttributeCommonInfo
+ // base. The only extra payload is ArgsText, the source text of the argument
+ // clause "(...)" as written (empty when there is none), interned into the
+ // ASTContext. It is stored as text rather than a SourceRange so the node is
+ // self-contained: it can be cloned for template instantiation, serialized to
+ // a module, and created implicitly, none of which have a live source range,
+ // and it prints without a SourceManager. The argument tokens are deliberately
+ // not parsed into expressions, which keeps non-expression argument grammars
+ // (e.g. the C++ profiles [[profiles::suppress(type.1)]]) representable; a
+ // consumer that wants structure re-lexes ArgsText itself.
+ //
+ // The DeclOrStmtAttr base lets an unknown attribute be retained in both
+ // declaration and statement positions (e.g. [[suppress(...)]] on a block).
+ // A type-position attribute cannot share this base and is retained as the
+ // separate UnknownType attribute below.
+ let Spellings = [];
+ let Args = [StringArgument<"ArgsText">];
+ // Synthesized by the compiler to retain an unrecognized attribute; it has no
+ // user-facing spelling, so it is internal rather than documented.
+ let Documentation = [InternalOnly];
+}
+
+def UnknownType : TypeAttr {
+ // Type-position counterpart of Unknown (see it for the rationale): retains an
+ // unrecognized [[...]] attribute that appertains to a type, wrapped in an
+ // AttributedType. It carries the same interned argument text, but cannot share
+ // Unknown's DeclOrStmtAttr base, so it is a separate TypeAttr.
+ let Spellings = [];
+ let Args = [StringArgument<"ArgsText">];
+ let Documentation = [InternalOnly];
+}
+
def Unused : InheritableAttr {
let Spellings = [CXX11<"", "maybe_unused", 201603>, GCC<"unused">,
C23<"", "maybe_unused", 202106>];
diff --git a/clang/include/clang/Parse/Parser.h b/clang/include/clang/Parse/Parser.h
index 163aa483a84e3..caab8e510db80 100644
--- a/clang/include/clang/Parse/Parser.h
+++ b/clang/include/clang/Parse/Parser.h
@@ -3095,7 +3095,8 @@ class Parser : public CodeCompletionHandler {
ParsedAttributes &Attrs, SourceLocation *EndLoc,
IdentifierInfo *ScopeName,
SourceLocation ScopeLoc,
- CachedTokens &OpenMPTokens);
+ CachedTokens &OpenMPTokens,
+ SourceRange *UnknownArgsRange = nullptr);
/// Parse the argument to C++23's [[assume()]] attribute. Returns true on
/// error.
diff --git a/clang/include/clang/Sema/ParsedAttr.h b/clang/include/clang/Sema/ParsedAttr.h
index 5387f9fad6cd2..34dce8d49d2ea 100644
--- a/clang/include/clang/Sema/ParsedAttr.h
+++ b/clang/include/clang/Sema/ParsedAttr.h
@@ -182,6 +182,10 @@ class ParsedAttr final
/// availability attribute.
SourceLocation UnavailableLoc;
+ /// For an unknown attribute retained in the AST as an UnknownAttr, the source
+ /// range of its argument clause "(...)". Invalid when there was no clause.
+ SourceRange UnknownAttrArgsRange;
+
const Expr *MessageExpr;
const ParsedAttrInfo &Info;
@@ -367,6 +371,11 @@ class ParsedAttr final
bool isPackExpansion() const { return EllipsisLoc.isValid(); }
SourceLocation getEllipsisLoc() const { return EllipsisLoc; }
+ /// The source range of the argument clause "(...)" of an unknown attribute
+ /// retained as an UnknownAttr; invalid when there was no clause.
+ void setUnknownAttrArgsRange(SourceRange R) { UnknownAttrArgsRange = R; }
+ SourceRange getUnknownAttrArgsRange() const { return UnknownAttrArgsRange; }
+
/// getNumArgs - Return the number of actual arguments to this attribute.
unsigned getNumArgs() const { return NumArgs; }
diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h
index 8a30f6319bcef..ca9a78816ba46 100644
--- a/clang/include/clang/Sema/Sema.h
+++ b/clang/include/clang/Sema/Sema.h
@@ -5081,6 +5081,15 @@ class Sema final : public SemaBase {
MutableArrayRef<Expr *> Args);
Attr *CreateAnnotationAttr(const ParsedAttr &AL);
+ /// CreateUnknownAttr - Retain an unrecognized attribute \p AL as an
+ /// UnknownAttr, interning its argument-clause text so the node is
+ /// self-contained (see UnknownAttr in Attr.td).
+ Attr *CreateUnknownAttr(const ParsedAttr &AL);
+
+ /// CreateUnknownTypeAttr - Retain an unrecognized type-position attribute
+ /// \p AL as an UnknownTypeAttr (the TypeAttr counterpart of UnknownAttr).
+ Attr *CreateUnknownTypeAttr(const ParsedAttr &AL);
+
bool checkMSInheritanceAttrOnDefinition(CXXRecordDecl *RD, SourceRange Range,
bool BestCase,
MSInheritanceModel SemanticSpelling);
diff --git a/clang/lib/AST/AttrImpl.cpp b/clang/lib/AST/AttrImpl.cpp
index cfd47e82b04b5..f4350adffaa92 100644
--- a/clang/lib/AST/AttrImpl.cpp
+++ b/clang/lib/AST/AttrImpl.cpp
@@ -15,10 +15,23 @@
#include "clang/AST/Attr.h"
#include "clang/AST/Expr.h"
#include "clang/AST/Type.h"
+#include "clang/Basic/IdentifierTable.h"
#include <optional>
#include <type_traits>
using namespace clang;
+void clang::printUnknownAttrPretty(raw_ostream &OS,
+ const AttributeCommonInfo &Info,
+ StringRef ArgsText) {
+ OS << "[[";
+ if (const IdentifierInfo *Scope = Info.getScopeName())
+ OS << Scope->getName() << "::";
+ if (const IdentifierInfo *Name = Info.getAttrName())
+ OS << Name->getName();
+ // ArgsText already includes the surrounding parentheses, or is empty.
+ OS << ArgsText << "]]";
+}
+
void LoopHintAttr::printPrettyPragma(raw_ostream &OS,
const PrintingPolicy &Policy) const {
unsigned SpellingIndex = getAttributeSpellingListIndex();
diff --git a/clang/lib/AST/DeclPrinter.cpp b/clang/lib/AST/DeclPrinter.cpp
index 4be3e977b815e..a714ed2dd3d1e 100644
--- a/clang/lib/AST/DeclPrinter.cpp
+++ b/clang/lib/AST/DeclPrinter.cpp
@@ -276,6 +276,18 @@ DeclPrinter::prettyPrintAttributes(const Decl *D,
#define PRAGMA_SPELLING_ATTR(X) case attr::X:
#include "clang/Basic/AttrList.inc"
break;
+ case attr::Unknown: {
+ // UnknownAttr retains an attribute Clang did not recognize. It has no
+ // spelling, so the generated printPretty emits nothing; reconstruct
+ // "[[scope::name(args)]]" from the retained name/scope and argument text.
+ const auto *UA = cast<UnknownAttr>(A);
+ AttrPosAsWritten APos = getPosAsWritten(A, D);
+ if (Pos == AttrPosAsWritten::Default || Pos == APos) {
+ AOut << LS;
+ printUnknownAttrPretty(AOut, *UA, UA->getArgsText());
+ }
+ break;
+ }
default:
AttrPosAsWritten APos = getPosAsWritten(A, D);
assert(APos != AttrPosAsWritten::Default &&
diff --git a/clang/lib/AST/StmtPrinter.cpp b/clang/lib/AST/StmtPrinter.cpp
index 4b709156cecad..9b65de07e4a2e 100644
--- a/clang/lib/AST/StmtPrinter.cpp
+++ b/clang/lib/AST/StmtPrinter.cpp
@@ -300,7 +300,12 @@ void StmtPrinter::VisitLabelStmt(LabelStmt *Node) {
void StmtPrinter::VisitAttributedStmt(AttributedStmt *Node) {
ArrayRef<const Attr *> Attrs = Node->getAttrs();
for (const auto *Attr : Attrs) {
- Attr->printPretty(OS, Policy);
+ // UnknownAttr has no spelling, so printPretty emits nothing; reconstruct it
+ // from the retained name/scope and argument text (see DeclPrinter).
+ if (const auto *UA = dyn_cast<UnknownAttr>(Attr))
+ printUnknownAttrPretty(OS, *UA, UA->getArgsText());
+ else
+ Attr->printPretty(OS, Policy);
if (Attr != Attrs.back())
OS << ' ';
}
diff --git a/clang/lib/AST/TextNodeDumper.cpp b/clang/lib/AST/TextNodeDumper.cpp
index 4bc7607a87d92..f3210a967d9ac 100644
--- a/clang/lib/AST/TextNodeDumper.cpp
+++ b/clang/lib/AST/TextNodeDumper.cpp
@@ -109,6 +109,12 @@ void TextNodeDumper::Visit(const Attr *A) {
if (A->isImplicit())
OS << " Implicit";
+ // A retained unknown attribute (UnknownAttr / UnknownTypeAttr) has no
+ // spelling, so show its name and scope to identify which attribute was kept.
+ if ((A->getKind() == attr::Unknown || A->getKind() == attr::UnknownType) &&
+ A->getAttrName())
+ OS << " " << A->getNormalizedFullName();
+
ConstAttrVisitor<TextNodeDumper>::Visit(A);
}
diff --git a/clang/lib/AST/TypePrinter.cpp b/clang/lib/AST/TypePrinter.cpp
index 244781c168839..4630085e97853 100644
--- a/clang/lib/AST/TypePrinter.cpp
+++ b/clang/lib/AST/TypePrinter.cpp
@@ -1946,6 +1946,15 @@ void TypePrinter::printAttributedAfter(const AttributedType *T,
->getExtInfo().getProducesResult())
return;
+ if (T->getAttrKind() == attr::UnknownType) {
+ // A retained unrecognized type attribute has no spelling; reconstruct
+ // "[[scope::name(args)]]" from its interned name/scope and argument text.
+ OS << ' ';
+ if (const auto *UA = dyn_cast_or_null<UnknownTypeAttr>(T->getAttr()))
+ printUnknownAttrPretty(OS, *UA, UA->getArgsText());
+ return;
+ }
+
if (T->getAttrKind() == attr::LifetimeBound) {
OS << " [[clang::lifetimebound]]";
return;
@@ -2069,6 +2078,7 @@ void TypePrinter::printAttributedAfter(const AttributedType *T,
case attr::PreserveMost:
case attr::PreserveNone:
case attr::OverflowBehavior:
+ case attr::UnknownType:
llvm_unreachable("This attribute should have been handled already");
case attr::NSReturnsRetained:
diff --git a/clang/lib/Parse/ParseDeclCXX.cpp b/clang/lib/Parse/ParseDeclCXX.cpp
index 150987dae332d..a518581483143 100644
--- a/clang/lib/Parse/ParseDeclCXX.cpp
+++ b/clang/lib/Parse/ParseDeclCXX.cpp
@@ -4451,7 +4451,8 @@ bool Parser::ParseCXXAssumeAttributeArg(
bool Parser::ParseCXX11AttributeArgs(
IdentifierInfo *AttrName, SourceLocation AttrNameLoc,
ParsedAttributes &Attrs, SourceLocation *EndLoc, IdentifierInfo *ScopeName,
- SourceLocation ScopeLoc, CachedTokens &OpenMPTokens) {
+ SourceLocation ScopeLoc, CachedTokens &OpenMPTokens,
+ SourceRange *UnknownArgsRange) {
assert(Tok.is(tok::l_paren) && "Not a C++11 attribute argument list");
SourceLocation LParenLoc = Tok.getLocation();
const LangOptions &LO = getLangOpts();
@@ -4492,9 +4493,14 @@ bool Parser::ParseCXX11AttributeArgs(
!hasAttribute(LO.CPlusPlus ? AttributeCommonInfo::Syntax::AS_CXX11
: AttributeCommonInfo::Syntax::AS_C23,
ScopeName, AttrName, getTargetInfo(), getLangOpts())) {
- // Eat the left paren, then skip to the ending right paren.
+ // Eat the left paren, then skip to the ending right paren. SkipUntil tracks
+ // nested ()/[]/{} so this consumes the whole balanced argument clause.
ConsumeParen();
SkipUntil(tok::r_paren);
+ // Remember the "(...)" range so the attribute can be retained as an
+ // UnknownAttr; PrevTokLocation is the matching ')' just consumed.
+ if (UnknownArgsRange)
+ *UnknownArgsRange = SourceRange(LParenLoc, PrevTokLocation);
return false;
}
@@ -4687,20 +4693,26 @@ void Parser::ParseCXX11AttributeSpecifierInternal(ParsedAttributes &Attrs,
}
// Parse attribute arguments
+ SourceRange UnknownArgsRange;
if (Tok.is(tok::l_paren))
- AttrParsed = ParseCXX11AttributeArgs(AttrName, AttrLoc, Attrs, EndLoc,
- ScopeName, ScopeLoc, OpenMPTokens);
+ AttrParsed =
+ ParseCXX11AttributeArgs(AttrName, AttrLoc, Attrs, EndLoc, ScopeName,
+ ScopeLoc, OpenMPTokens, &UnknownArgsRange);
if (!AttrParsed) {
- Attrs.addNew(AttrName,
- SourceRange(ScopeLoc.isValid() && CommonScopeLoc.isInvalid()
- ? ScopeLoc
- : AttrLoc,
- AttrLoc),
- AttributeScopeInfo(ScopeName, ScopeLoc, CommonScopeLoc),
- nullptr, 0,
- getLangOpts().CPlusPlus ? ParsedAttr::Form::CXX11()
- : ParsedAttr::Form::C23());
+ ParsedAttr *Attr = Attrs.addNew(
+ AttrName,
+ SourceRange(ScopeLoc.isValid() && CommonScopeLoc.isInvalid()
+ ? ScopeLoc
+ : AttrLoc,
+ AttrLoc),
+ AttributeScopeInfo(ScopeName, ScopeLoc, CommonScopeLoc), nullptr, 0,
+ getLangOpts().CPlusPlus ? ParsedAttr::Form::CXX11()
+ : ParsedAttr::Form::C23());
+ // Carry the captured argument-clause range (if any) so Sema can retain it
+ // as an UnknownAttr.
+ if (UnknownArgsRange.isValid())
+ Attr->setUnknownAttrArgsRange(UnknownArgsRange);
AttrParsed = true;
}
diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp
index 9f962912148ab..7606dce743eac 100644
--- a/clang/lib/Sema/Sema.cpp
+++ b/clang/lib/Sema/Sema.cpp
@@ -30,6 +30,7 @@
#include "clang/Basic/TargetInfo.h"
#include "clang/Lex/HeaderSearch.h"
#include "clang/Lex/HeaderSearchOptions.h"
+#include "clang/Lex/Lexer.h"
#include "clang/Lex/Preprocessor.h"
#include "clang/Sema/CXXFieldCollector.h"
#include "clang/Sema/EnterExpressionEvaluationContext.h"
@@ -3096,3 +3097,33 @@ Attr *Sema::CreateAnnotationAttr(const ParsedAttr &AL) {
return CreateAnnotationAttr(AL, Str, Args);
}
+
+// The argument-clause text captured by the parser for an unknown attribute.
+// Interning it (UnknownAttr/UnknownTypeAttr use a StringArgument) makes the
+// retained attribute self-contained: unlike a SourceRange, interned text can be
+// cloned for template instantiation, serialized, and created implicitly, and it
+// prints without a SourceManager.
+static StringRef unknownAttrArgsText(Sema &S, const ParsedAttr &AL) {
+ SourceRange R = AL.getUnknownAttrArgsRange();
+ if (!R.isValid())
+ return StringRef();
+ // The range spans the '(' to the ')' of the argument clause, so getSourceText
+ // returns that source span verbatim. Because those delimiters are ordinary
+ // source locations in the common case, the clause is reproduced exactly as
+ // written: a macro used as an argument (e.g. `[[vendor::attr(M)]]`) is kept
+ // unexpanded as `M`, and any comments within the clause are preserved. Only
+ // when the parentheses themselves come from a macro expansion (the whole
+ // attribute is macro-generated) does makeFileCharRange return an empty range;
+ // the attribute is still retained, just without its argument text, which is
+ // preferable to reproducing tokens the user never wrote.
+ return Lexer::getSourceText(CharSourceRange::getTokenRange(R),
+ S.getSourceManager(), S.getLangOpts());
+}
+
+Attr *Sema::CreateUnknownAttr(const ParsedAttr &AL) {
+ return UnknownAttr::Create(Context, unknownAttrArgsText(*this, AL), AL);
+}
+
+Attr *Sema::CreateUnknownTypeAttr(const ParsedAttr &AL) {
+ return UnknownTypeAttr::Create(Context, unknownAttrArgsText(*this, AL), AL);
+}
diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp
index 1b272b5416860..9bfac94fa3d59 100644
--- a/clang/lib/Sema/SemaDeclAttr.cpp
+++ b/clang/lib/Sema/SemaDeclAttr.cpp
@@ -7583,6 +7583,14 @@ ProcessDeclAttribute(Sema &S, Scope *scope, Decl *D, const ParsedAttr &AL,
<< AL.getAttrName() << AL.getRange();
} else {
S.DiagnoseUnknownAttribute(AL);
+ // Retain a genuinely unknown attribute in the AST so tooling and plugins
+ // can recover it. Target-specific attributes that merely do not exist on
+ // this target are left dropped. Limited to C++ [[...]] syntax for now.
+ // An unrecognized attribute-token is ignored per [dcl.attr.grammar]/8
+ // (https://eel.is/c++draft/dcl.attr.grammar#8).
+ if (D && AL.getKind() == ParsedAttr::UnknownAttribute &&
+ AL.isCXX11Attribute())
+ D->addAttr(S.CreateUnknownAttr(AL));
}
return;
}
diff --git a/clang/lib/Sema/SemaStmtAttr.cpp b/clang/lib/Sema/SemaStmtAttr.cpp
index 3e9dc9c5e7e08..cddfd436cc000 100644
--- a/clang/lib/Sema/SemaStmtAttr.cpp
+++ b/clang/lib/Sema/SemaStmtAttr.cpp
@@ -712,6 +712,14 @@ static Attr *ProcessStmtAttribute(Sema &S, Stmt *St, const ParsedAttr &A,
<< A << A.getRange();
} else {
S.DiagnoseUnknownAttribute(A);
+ // Retain a genuinely unknown attribute on the statement so tooling and
+ // plugins can recover it, mirroring the declaration path. This never
+ // emits an applicability diagnostic: an unrecognized attribute-token is
+ // ignored per [dcl.attr.grammar]/8
+ // (https://eel.is/c++draft/dcl.attr.grammar#8), so it is not "invalid on
+ // a statement". Limited to C++ [[...]] syntax for now.
+ if (A.getKind() == ParsedAttr::UnknownAttribute && A.isCXX11Attribute())
+ return S.CreateUnknownAttr(A);
}
return nullptr;
}
diff --git a/clang/lib/Sema/SemaType.cpp b/clang/lib/Sema/SemaType.cpp
index f7789b7475e8e..e99a40be4c469 100644
--- a/clang/lib/Sema/SemaType.cpp
+++ b/clang/lib/Sema/SemaType.cpp
@@ -9079,6 +9079,16 @@ static void processTypeAttrs(TypeProcessingState &state, QualType &type,
case ParsedAttr::UnknownAttribute:
if (attr.isStandardAttributeSyntax()) {
state.getSema().DiagnoseUnknownAttribute(attr);
+ // Retain the unknown type-position attribute on an AttributedType so
+ // tooling can recover it, mirroring the declaration and statement
+ // paths. An unrecognized attribute-token is ignored per
+ // [dcl.attr.grammar]/8 (https://eel.is/c++draft/dcl.attr.grammar#8), so
+ // this is not an error. Limited to C++ [[...]] syntax for now.
+ if (attr.isCXX11Attribute()) {
+ Attr *A = state.getSema().CreateUnknownTypeAttr(attr);
+ type = state.getAttributedType(A, type, type);
+ attr.setUsedAsTypeAttr();
+ }
// Mark the attribute as invalid so we don't emit the same diagnostic
// multiple times.
attr.setInvalid();
diff --git a/clang/test/AST/ast-dump-unknown-attr-negative.cpp b/clang/test/AST/ast-dump-unknown-attr-negative.cpp
new file mode 100644
index 0000000000000..4d2fe1e5a9666
--- /dev/null
+++ b/clang/test/AST/ast-dump-unknown-attr-negative.cpp
@@ -0,0 +1,16 @@
+// Retention is limited to C++ [[...]] spellings. GNU __attribute__ and MS
+// __declspec unknown attributes are dropped after their usual diagnostic, as
+// before, not retained as UnknownAttr. This pins the scope of the feature.
+
+// RUN: %clang_cc1 -std=c++20 -fms-extensions -Wno-unknown-attributes \
+// RUN: -Wno-ignored-attributes -ast-dump %s | FileCheck %s
+
+__attribute__((unknown_gnu)) int a;
+__declspec(unknown_ms) int b;
+// Neither GNU nor __declspec unknowns are retained.
+// CHECK-NOT: UnknownAttr
+
+// A C++ [[...]] spelling is retained (positive control).
+[[unknown_cxx]] int c;
+// CHECK: VarDecl {{.*}} c 'int'
+// CHECK-NEXT: UnknownAttr {{.*}} unknown_cxx
diff --git a/clang/test/AST/ast-dump-unknown-attr-stmt.cpp b/clang/test/AST/ast-dump-unknown-attr-stmt.cpp
new file mode 100644
index 0000000000000..4d4ef7b554a55
--- /dev/null
+++ b/clang/test/AST/ast-dump-unknown-attr-stmt.cpp
@@ -0,0 +1,46 @@
+// An unknown [[...]] attribute is retained on a statement too, not just a
+// declaration: UnknownAttr is a DeclOrStmtAttr, so it lands on the AttributedStmt
+// the attribute appertains to. Retaining it never emits an "attribute cannot be
+// applied to a statement" diagnostic, because an unrecognized attribute-token is
+// ignored per [dcl.attr.grammar]/8. Retention is consistent across statement
+// kinds, carries the argument text just like the declaration path, and keeps
+// every attribute when several appear on one statement. Exercised across
+// standard modes.
+
+// RUN: %clang_cc1 -std=c++17 -Wno-unknown-attributes -ast-dump %s | FileCheck %s
+// RUN: %clang_cc1 -std=c++20 -Wno-unknown-attributes -ast-dump %s | FileCheck %s
+// RUN: %clang_cc1 -std=c++23 -Wno-unknown-attributes -ast-dump %s | FileCheck %s
+
+void f() {
+ // On a compound statement, with an argument clause retained verbatim, exactly
+ // as on a declaration.
+ [[ns::transient(a, b)]] { }
+ // CHECK: AttributedStmt
+ // CHECK-NEXT: UnknownAttr {{.*}} ns::transient "(a, b)"
+ // CHECK-NEXT: CompoundStmt
+
+ // On a loop statement.
+ [[ns::loop]] for (;;) { break; }
+ // CHECK: AttributedStmt
+ // CHECK-NEXT: UnknownAttr {{.*}} ns::loop ""
+ // CHECK-NEXT: ForStmt
+
+ // On an expression statement.
+ [[vendor::note]] 1 + 1;
+ // CHECK: AttributedStmt
+ // CHECK-NEXT: UnknownAttr {{.*}} vendor::note ""
+ // CHECK-NEXT: BinaryOperator
+
+ // On a null statement.
+ [[vendor::skip]];
+ // CHECK: AttributedStmt
+ // CHECK-NEXT: UnknownAttr {{.*}} vendor::skip ""
+ // CHECK-NEXT: NullStmt
+
+ // Several unknown attributes on one statement are all retained, in order.
+ [[ns::a]] [[ns::b]] { }
+ // CHECK: AttributedStmt
+ // CHECK-NEXT: UnknownAttr {{.*}} ns::a ""
+ // CHECK-NEXT: UnknownAttr {{.*}} ns::b ""
+ // CHECK-NEXT: CompoundStmt
+}
diff --git a/clang/test/AST/ast-dump-unknown-attr-template.cpp b/clang/test/AST/ast-dump-unknown-attr-template.cpp
new file mode 100644
index 0000000000000..9a0b63be82a6b
--- /dev/null
+++ b/clang/test/AST/ast-dump-unknown-attr-template.cpp
@@ -0,0 +1,15 @@
+// An unknown attribute on a template member survives instantiation: the cloned
+// attribute on the specialization carries the interned argument text. Because
+// UnknownAttr stores the argument text (not a source range), it can be created
+// implicitly for the instantiation, which has no source of its own.
+
+// RUN: %clang_cc1 -std=c++17 -Wno-unknown-attributes -ast-dump %s | FileCheck %s
+
+template <class T> struct S {
+ T x [[ns::transient(a, b)]];
+};
+template struct S<int>;
+
+// CHECK: ClassTemplateSpecializationDecl {{.*}} struct S definition
+// CHECK: FieldDecl {{.*}} x 'int'
+// CHECK-NEXT: UnknownAttr {{.*}} ns::transient "(a, b)"
diff --git a/clang/test/AST/ast-dump-unknown-attr-type.cpp b/clang/test/AST/ast-dump-unknown-attr-type.cpp
new file mode 100644
index 0000000000000..bf86ab755a55f
--- /dev/null
+++ b/clang/test/AST/ast-dump-unknown-attr-type.cpp
@@ -0,0 +1,18 @@
+// An unknown [[...]] attribute that appertains to a type is retained on an
+// AttributedType as an UnknownTypeAttr (the TypeAttr counterpart of
+// UnknownAttr), instead of being dropped, so it shows up in the declared type
+// and round-trips under -ast-print.
+
+// RUN: %clang_cc1 -std=c++17 -Wno-unknown-attributes -ast-dump %s | FileCheck %s
+// RUN: %clang_cc1 -std=c++17 -Wno-unknown-attributes -ast-print %s \
+// RUN: | FileCheck --check-prefix=PRINT %s
+
+int *[[ns::transient(a, b)]] p;
+
+// The retained attribute is part of the pointer's (sugared) type.
+// CHECK: VarDecl {{.*}} p 'int * {{\[\[}}ns::transient(a, b){{\]\]}}':'int *'
+
+// -ast-print reproduces the attribute and its arguments, but prints a trailing
+// type attribute after the declarator, so the exact written position is not
+// preserved (harmless for an ignored attribute; the content round-trips).
+// PRINT: int *p {{\[\[}}ns::transient(a, b){{\]\]}};
diff --git a/clang/test/AST/ast-dump-unknown-attr.cpp b/clang/test/AST/ast-dump-unknown-attr.cpp
new file mode 100644
index 0000000000000..8871a0414c097
--- /dev/null
+++ b/clang/test/AST/ast-dump-unknown-attr.cpp
@@ -0,0 +1,14 @@
+// An otherwise-unknown C++ [[...]] attribute is retained in the AST as an
+// UnknownAttr instead of being dropped after the -Wunknown-attributes
+// diagnostic, so tooling and plugins can recover it.
+
+// RUN: %clang_cc1 -std=c++17 -Wno-unknown-attributes -ast-dump %s \
+// RUN: | FileCheck %s
+
+struct X {
+ int x [[ns::transient(a, b)]];
+};
+
+// CHECK: FieldDecl {{.*}} x 'int'
+// The dump identifies the retained attribute by its scope::name.
+// CHECK: UnknownAttr {{.*}} ns::transient
diff --git a/clang/test/AST/ast-print-unknown-attr.cpp b/clang/test/AST/ast-print-unknown-attr.cpp
new file mode 100644
index 0000000000000..655a322365fae
--- /dev/null
+++ b/clang/test/AST/ast-print-unknown-attr.cpp
@@ -0,0 +1,43 @@
+// An unknown C++ [[...]] attribute, retained as an UnknownAttr, prints back to
+// equivalent source under -ast-print: the scope, name and the source text of
+// the argument clause are reproduced. This is the round-trip oracle for the
+// retention feature.
+
+// RUN: %clang_cc1 -std=c++17 -Wno-unknown-attributes -ast-print %s | FileCheck %s
+
+struct X {
+ int x [[ns::transient(a, b)]];
+};
+// CHECK: struct X {
+// CHECK-NEXT: int x {{\[\[}}ns::transient(a, b){{\]\]}};
+// CHECK-NEXT: };
+
+[[frobble]] void g();
+// CHECK: {{\[\[}}frobble{{\]\]}} void g();
+
+[[ns::plain]] void h();
+// CHECK: {{\[\[}}ns::plain{{\]\]}} void h();
+
+// The argument clause is retained as the verbatim source span between its
+// parentheses, so a macro used as an argument stays unexpanded and comments are
+// kept.
+#define M 1 + 2
+[[vendor::attr(M)]] int a;
+// CHECK: {{\[\[}}vendor::attr(M){{\]\]}} int a;
+
+[[vendor::attr(1 /*c*/ + 2)]] int b;
+// CHECK: {{\[\[}}vendor::attr(1 /*c*/ + 2){{\]\]}} int b;
+
+// When the parentheses themselves come from a macro expansion, the clause maps
+// to no file range, so the argument text is dropped; the attribute is still
+// retained.
+#define WHOLE [[vendor::attr(9)]]
+WHOLE int c;
+// CHECK: {{\[\[}}vendor::attr{{\]\]}} int c;
+
+// A statement attribute round-trips too, consistent with the declaration form.
+void fn() {
+ [[vendor::note]] 1 + 1;
+}
+// CHECK: void fn() {
+// CHECK: {{\[\[}}vendor::note{{\]\]}} 1 + 1;
diff --git a/clang/test/PCH/unknown-attr.cpp b/clang/test/PCH/unknown-attr.cpp
new file mode 100644
index 0000000000000..6183e83d7b7cf
--- /dev/null
+++ b/clang/test/PCH/unknown-attr.cpp
@@ -0,0 +1,22 @@
+// A retained UnknownAttr, including its interned argument text, survives PCH
+// serialization and deserialization. This works because the argument is stored
+// as text (a StringArgument), which TableGen serializes automatically; a source
+// range would not survive being read back into a fresh compilation.
+
+// RUN: %clang_cc1 -std=c++17 -Wno-unknown-attributes -emit-pch -o %t %s
+// RUN: %clang_cc1 -std=c++17 -Wno-unknown-attributes -include-pch %t %s \
+// RUN: -ast-dump-all 2>&1 | FileCheck %s
+
+#ifndef HEADER
+#define HEADER
+
+struct X {
+ int x [[ns::transient(a, b)]];
+};
+
+#else
+
+// CHECK: FieldDecl {{.*}} x 'int'
+// CHECK-NEXT: UnknownAttr {{.*}} ns::transient "(a, b)"
+
+#endif
diff --git a/clang/test/SemaCXX/unknown-attr-retain.cpp b/clang/test/SemaCXX/unknown-attr-retain.cpp
new file mode 100644
index 0000000000000..8df34847f0f38
--- /dev/null
+++ b/clang/test/SemaCXX/unknown-attr-retain.cpp
@@ -0,0 +1,43 @@
+// Retaining an unknown [[...]] attribute must not add any diagnostic beyond the
+// existing -Wunknown-attributes warning. In particular an unknown attribute on a
+// statement must NOT be reported as "invalid on a statement": an unrecognized
+// attribute-token is ignored, [dcl.attr.grammar]/8
+// (https://eel.is/c++draft/dcl.attr.grammar#8), not misapplied. Checked across
+// standard modes, since attribute appertainment is language-version sensitive.
+
+// RUN: %clang_cc1 -std=c++17 -fsyntax-only -verify %s
+// RUN: %clang_cc1 -std=c++20 -fsyntax-only -verify %s
+// RUN: %clang_cc1 -std=c++23 -fsyntax-only -verify %s
+
+// On a declaration.
+struct X {
+ int x [[ns::transient(a, b)]]; // expected-warning {{unknown attribute 'ns::transient' ignored}}
+};
+
+void f() {
+ // On a statement: only the unknown-attribute warning, no appertainment error.
+ [[ns::transient(a, b)]] { // expected-warning {{unknown attribute 'ns::transient' ignored}}
+ }
+
+ // Unknown attribute with no argument clause.
+ [[frobble]] while (false) {} // expected-warning {{unknown attribute 'frobble' ignored}}
+}
+
+// Negative case: a recognized attribute is unaffected by retention -- it is
+// applied normally and produces no unknown-attribute warning (and so is not
+// turned into an UnknownAttr).
+[[nodiscard]] int g();
+
+void neg() {
+ // Consistency at the boundary: a recognized attribute misapplied to a
+ // statement still gets its normal diagnostic and is not retained. Only
+ // genuinely unknown attributes take the retention path, on statements just as
+ // on declarations.
+ [[fallthrough]]; // expected-error {{fallthrough annotation is outside switch statement}}
+}
+
+// Same boundary in type position: a recognized type attribute that fails its
+// own validation still diagnoses normally and is not retained as an
+// UnknownTypeAttr. Retention is limited to genuinely unknown attributes across
+// declaration, statement, and type positions alike.
+typedef int BadVec [[gnu::vector_size(3)]]; // expected-error {{vector size not an integral multiple of component size}}
diff --git a/clang/unittests/AST/ASTImporterTest.cpp b/clang/unittests/AST/ASTImporterTest.cpp
index 503f5da8af90f..34b54a49ba850 100644
--- a/clang/unittests/AST/ASTImporterTest.cpp
+++ b/clang/unittests/AST/ASTImporterTest.cpp
@@ -8106,6 +8106,16 @@ TEST_P(ImportAttributes, ImportEnableIf) {
EXPECT_EQ(FromAttr->getMessage(), ToAttr->getMessage());
}
+TEST_P(ImportAttributes, ImportUnknown) {
+ UnknownAttr *FromAttr, *ToAttr;
+ importAttr<VarDecl>("int test [[ns::transient(a, b)]];", FromAttr, ToAttr);
+ // The retained argument text survives import and is re-interned into the
+ // destination context: equal contents at a different address. A source-range
+ // payload could not be imported into a fresh context this way.
+ EXPECT_EQ(FromAttr->getArgsText(), ToAttr->getArgsText());
+ EXPECT_NE(FromAttr->getArgsText().data(), ToAttr->getArgsText().data());
+}
+
TEST_P(ImportAttributes, ImportGuardedVar) {
GuardedVarAttr *FromAttr, *ToAttr;
importAttr<VarDecl>("int test __attribute__((guarded_var));", FromAttr,
diff --git a/clang/unittests/AST/AttrTest.cpp b/clang/unittests/AST/AttrTest.cpp
index cbdc3a1cc59a9..5b51ad65fa161 100644
--- a/clang/unittests/AST/AttrTest.cpp
+++ b/clang/unittests/AST/AttrTest.cpp
@@ -11,8 +11,10 @@
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Basic/AttrKinds.h"
+#include "clang/Basic/AttributeCommonInfo.h"
#include "clang/Basic/AttributeScopeInfo.h"
#include "clang/Basic/IdentifierTable.h"
+#include "clang/Basic/SourceLocation.h"
#include "clang/Tooling/Tooling.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
@@ -272,4 +274,35 @@ TEST(Attr, UnknownScopedAttributeSpellingIndex) {
EXPECT_EQ(CI.getAttributeSpellingListIndex(), 0u);
}
+// Stage 1: an UnknownAttr can be constructed from an AttributeCommonInfo plus
+// the argument-clause text, and round-trips name / scope / syntax / args. The
+// argument text is interned into the ASTContext, so the node is self-contained.
+TEST(Attr, UnknownConstruction) {
+ auto AST = clang::tooling::buildASTFromCode("");
+ auto &Ctx = AST->getASTContext();
+
+ // Model what the parser will hand us for [[ns::transient(x, y)]].
+ const clang::IdentifierInfo *Name = &Ctx.Idents.get("transient");
+ const clang::IdentifierInfo *Scope = &Ctx.Idents.get("ns");
+ clang::AttributeScopeInfo ScopeInfo(Scope, clang::SourceLocation());
+ clang::AttributeCommonInfo CommonInfo(
+ Name, ScopeInfo, clang::SourceRange(),
+ clang::AttributeCommonInfo::Form::CXX11());
+
+ auto *A = clang::UnknownAttr::Create(Ctx, "(x, y)", CommonInfo);
+ ASSERT_NE(A, nullptr);
+
+ EXPECT_EQ(A->getKind(), clang::attr::Unknown);
+ EXPECT_EQ(A->getSyntax(), clang::AttributeCommonInfo::AS_CXX11);
+ ASSERT_NE(A->getAttrName(), nullptr);
+ EXPECT_EQ(A->getAttrName()->getName(), "transient");
+ ASSERT_NE(A->getScopeName(), nullptr);
+ EXPECT_EQ(A->getScopeName()->getName(), "ns");
+ EXPECT_EQ(A->getArgsText(), "(x, y)");
+
+ // With no argument clause the text is empty.
+ auto *B = clang::UnknownAttr::Create(Ctx, "", CommonInfo);
+ EXPECT_TRUE(B->getArgsText().empty());
+}
+
} // namespace
diff --git a/clang/unittests/ASTMatchers/ASTMatchersNodeTest.cpp b/clang/unittests/ASTMatchers/ASTMatchersNodeTest.cpp
index 4190d4703e37d..8144ebfa47e52 100644
--- a/clang/unittests/ASTMatchers/ASTMatchersNodeTest.cpp
+++ b/clang/unittests/ASTMatchers/ASTMatchersNodeTest.cpp
@@ -2100,10 +2100,9 @@ TEST_P(ASTMatchersTest, Attr) {
if (GetParam().isCXX11OrLater()) {
EXPECT_TRUE(matches("struct [[clang::warn_unused_result]] F{};", attr()));
- // Unknown attributes are not parsed into an AST node.
- if (!AutomaticAttributes) {
- EXPECT_TRUE(notMatches("int x [[unknownattr]];", attr()));
- }
+ // An unrecognized C++ attribute is retained in the AST as an UnknownAttr
+ // instead of being dropped, so it now matches attr().
+ EXPECT_TRUE(matches("int x [[unknownattr]];", attr()));
}
if (GetParam().isCXX17OrLater()) {
EXPECT_TRUE(matches("struct [[nodiscard]] F{};", attr()));
More information about the cfe-commits
mailing list