https://github.com/vgvassilev updated https://github.com/llvm/llvm-project/pull/209224
>From 4c722a93cd1882b4d4e6d5469d45bf43788090e3 Mon Sep 17 00:00:00 2001 From: Vassil Vassilev <[email protected]> Date: Mon, 13 Jul 2026 15:34:13 +0000 Subject: [PATCH 1/4] [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 +- .../PrintFunctionNames/PrintFunctionNames.cpp | 25 ++++++++-- 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/Frontend/plugin-unknown-attr.cpp | 23 ++++++++++ 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 ++- 30 files changed, 508 insertions(+), 28 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/Frontend/plugin-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/examples/PrintFunctionNames/PrintFunctionNames.cpp b/clang/examples/PrintFunctionNames/PrintFunctionNames.cpp index b2b785b87c25c..955beb5702923 100644 --- a/clang/examples/PrintFunctionNames/PrintFunctionNames.cpp +++ b/clang/examples/PrintFunctionNames/PrintFunctionNames.cpp @@ -6,8 +6,15 @@ // //===----------------------------------------------------------------------===// // -// Example clang plugin which simply prints the names of all the top-level decls -// in the input file. +// Example clang plugin which prints the names of all the top-level decls in the +// input file, and, for each, any unrecognized C++ [[...]] attribute it carries. +// +// The latter illustrates what retaining unknown attributes in the AST unlocks +// for a plugin: an attribute Clang does not implement is kept as an UnknownAttr +// (after the -Wunknown-attributes diagnostic) instead of being dropped, so a +// plugin can recover a vendor or domain-specific attribute from the AST and act +// on it, rather than re-lexing the source. This example only reports them; a +// real plugin would give the attribute meaning by reading its argument text. // //===----------------------------------------------------------------------===// @@ -34,8 +41,18 @@ class PrintFunctionsConsumer : public ASTConsumer { bool HandleTopLevelDecl(DeclGroupRef DG) override { for (DeclGroupRef::iterator i = DG.begin(), e = DG.end(); i != e; ++i) { const Decl *D = *i; - if (const NamedDecl *ND = dyn_cast<NamedDecl>(D)) - llvm::errs() << "top-level-decl: \"" << ND->getNameAsString() << "\"\n"; + const NamedDecl *ND = dyn_cast<NamedDecl>(D); + if (!ND) + continue; + llvm::errs() << "top-level-decl: \"" << ND->getNameAsString() << "\"\n"; + + // Recover any unrecognized [[...]] attribute that Clang retained on this + // declaration as an UnknownAttr. getNormalizedFullName() reproduces the + // scope::name, and getArgsText() the verbatim argument clause (or empty). + for (const Attr *A : ND->attrs()) + if (const auto *UA = dyn_cast<UnknownAttr>(A)) + llvm::errs() << " unknown-attribute: \"" << UA->getNormalizedFullName() + << UA->getArgsText() << "\"\n"; } return true; 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 61ef3fb612440..975740b6d5238 100644 --- a/clang/include/clang/Basic/Attr.td +++ b/clang/include/clang/Basic/Attr.td @@ -3840,6 +3840,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 6913c42884a36..77a0f91b415b4 100644 --- a/clang/include/clang/Parse/Parser.h +++ b/clang/include/clang/Parse/Parser.h @@ -3102,7 +3102,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 4ff4c669a6b70..0b8f47ea49750 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -5092,6 +5092,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 7272ad0de9a2c..968e536f2a4ab 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 77b0c017f6ceb..d941c7ed43b34 100644 --- a/clang/lib/AST/DeclPrinter.cpp +++ b/clang/lib/AST/DeclPrinter.cpp @@ -281,6 +281,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 a3da70962f4f5..66a5e251c07e0 100644 --- a/clang/lib/AST/StmtPrinter.cpp +++ b/clang/lib/AST/StmtPrinter.cpp @@ -301,7 +301,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 70008d531177d..1b3aae3dab519 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 80d63434c28b1..5d9431be0f888 100644 --- a/clang/lib/AST/TypePrinter.cpp +++ b/clang/lib/AST/TypePrinter.cpp @@ -1962,6 +1962,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; @@ -2086,6 +2095,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 dbc1b85acd143..aeaf5190e136a 100644 --- a/clang/lib/Parse/ParseDeclCXX.cpp +++ b/clang/lib/Parse/ParseDeclCXX.cpp @@ -4508,7 +4508,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(); @@ -4549,9 +4550,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; } @@ -4737,20 +4743,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 21f71d7f8b40e..d035d07dd1d60 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -31,6 +31,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" @@ -3119,3 +3120,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 eb4a8c2ab9ae0..9ced841bd57bd 100644 --- a/clang/lib/Sema/SemaDeclAttr.cpp +++ b/clang/lib/Sema/SemaDeclAttr.cpp @@ -7592,6 +7592,14 @@ ProcessDeclAttribute(Sema &S, 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 bad5a3e0fb919..647308878734f 100644 --- a/clang/lib/Sema/SemaStmtAttr.cpp +++ b/clang/lib/Sema/SemaStmtAttr.cpp @@ -770,6 +770,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 9de4f12aabf68..c105bdc3026ee 100644 --- a/clang/lib/Sema/SemaType.cpp +++ b/clang/lib/Sema/SemaType.cpp @@ -9097,6 +9097,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/Frontend/plugin-unknown-attr.cpp b/clang/test/Frontend/plugin-unknown-attr.cpp new file mode 100644 index 0000000000000..2fe536c8f7b41 --- /dev/null +++ b/clang/test/Frontend/plugin-unknown-attr.cpp @@ -0,0 +1,23 @@ +// Tests that a plugin can recover an unrecognized C++ [[...]] attribute from the +// AST. Clang does not implement these attributes, so it retains them as +// UnknownAttr (after -Wunknown-attributes); the PrintFunctionNames example +// plugin reads them back and reports the scope::name and argument text, instead +// of losing them. + +// RUN: %clang_cc1 -std=c++17 -load %llvmshlibdir/PrintFunctionNames%pluginext \ +// RUN: -plugin print-fns -Wno-unknown-attributes %s 2>&1 | FileCheck %s + +// REQUIRES: plugins, examples + +[[vendor::transient(a, b)]] int x; +// CHECK: top-level-decl: "x" +// CHECK-NEXT: unknown-attribute: "vendor::transient(a, b)" + +[[frobble]] void g(); +// CHECK: top-level-decl: "g" +// CHECK-NEXT: unknown-attribute: "frobble" + +// A recognized attribute is not retained as unknown, so it is not reported. +[[nodiscard]] int h(); +// CHECK: top-level-decl: "h" +// CHECK-NOT: unknown-attribute 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 f3b4c9ca7fa9a..8dbd8cdeec7d8 100644 --- a/clang/unittests/AST/ASTImporterTest.cpp +++ b/clang/unittests/AST/ASTImporterTest.cpp @@ -8142,6 +8142,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 33cc5c491db93..c6fd2f9e96cff 100644 --- a/clang/unittests/AST/AttrTest.cpp +++ b/clang/unittests/AST/AttrTest.cpp @@ -12,8 +12,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" @@ -338,4 +340,35 @@ TEST(Attr, OMPInvariantPredicateBoundOnIntraTileLoop) { EXPECT_FALSE(Full->getPredicate()); } +// 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())); >From 022aa4ceddd35a290b6c249b00c5d8bbcf93f476 Mon Sep 17 00:00:00 2001 From: Vassil Vassilev <[email protected]> Date: Fri, 24 Jul 2026 08:15:57 +0000 Subject: [PATCH 2/4] [clang][NFC] Trim redundant unknown-attribute test cases The statement ast-dump test exercised five statement kinds (compound, loop, expression, null, and multiple-attribute), but retention in ProcessStmtAttribute does not branch on the statement kind, so those cases cover one code path. Keep two: a compound statement carrying an argument clause (the argument-retention behavior) and several attributes on a non-compound statement. Drop the ast-print statement case as well, since printing goes through the shared printUnknownAttrPretty already covered by the declaration case. --- clang/test/AST/ast-dump-unknown-attr-stmt.cpp | 40 +++++-------------- clang/test/AST/ast-print-unknown-attr.cpp | 7 ---- 2 files changed, 11 insertions(+), 36 deletions(-) diff --git a/clang/test/AST/ast-dump-unknown-attr-stmt.cpp b/clang/test/AST/ast-dump-unknown-attr-stmt.cpp index 4d4ef7b554a55..92f54223ec7b4 100644 --- a/clang/test/AST/ast-dump-unknown-attr-stmt.cpp +++ b/clang/test/AST/ast-dump-unknown-attr-stmt.cpp @@ -1,46 +1,28 @@ // 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. +// the attribute appertains to, with its argument text preserved just like the +// declaration path, and every attribute is kept when several appear on one +// statement. 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. 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. + // The argument clause is 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]] { } + // Several unknown attributes on one statement are all retained, in order. The + // retention path does not depend on the statement kind (here an expression + // statement, not a compound one). + [[ns::a]] [[ns::b]] 1 + 1; // CHECK: AttributedStmt // CHECK-NEXT: UnknownAttr {{.*}} ns::a "" // CHECK-NEXT: UnknownAttr {{.*}} ns::b "" - // CHECK-NEXT: CompoundStmt + // CHECK-NEXT: BinaryOperator } diff --git a/clang/test/AST/ast-print-unknown-attr.cpp b/clang/test/AST/ast-print-unknown-attr.cpp index 655a322365fae..f931882d6a85b 100644 --- a/clang/test/AST/ast-print-unknown-attr.cpp +++ b/clang/test/AST/ast-print-unknown-attr.cpp @@ -34,10 +34,3 @@ struct X { #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; >From 2e111dc690ca385ec5d81242dd23c480081cfd6a Mon Sep 17 00:00:00 2001 From: Vassil Vassilev <[email protected]> Date: Fri, 24 Jul 2026 08:18:05 +0000 Subject: [PATCH 3/4] [clang] Test dependent-argument behavior from the attribute review Two points came up reviewing unknown-attribute retention. First, an unknown attribute's dependent argument is retained verbatim through instantiation and is never substituted or checked, since the attribute is ignored; add that case to the template ast-dump test. Second, making such an argument semantically live is a plugin-attribute feature, not a retention one: a plugin registers the attribute so Clang parses the argument in context, and the Attribute example lowers it onto an AnnotateAttr whose dependent argument the existing machinery substitutes. Add a test demonstrating that substitution, and its boundary -- a substitution failure is a hard error, not SFINAE, which no attribute does. --- clang/examples/Attribute/Attribute.cpp | 6 +++ .../AST/ast-dump-unknown-attr-template.cpp | 14 ++++++ .../plugin-attribute-instantiation.cpp | 49 +++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 clang/test/Frontend/plugin-attribute-instantiation.cpp diff --git a/clang/examples/Attribute/Attribute.cpp b/clang/examples/Attribute/Attribute.cpp index 625f1645afbff..da81dcdeb5b6a 100644 --- a/clang/examples/Attribute/Attribute.cpp +++ b/clang/examples/Attribute/Attribute.cpp @@ -9,6 +9,12 @@ // Example clang plugin which adds an an annotation to file-scope declarations // with the 'example' attribute. // +// Because the attribute is registered (ParsedAttrInfo), Clang parses its +// arguments as expressions in context, and this plugin lowers them onto an +// Expr-carrying attribute (AnnotateAttr). A dependent argument therefore +// participates in template instantiation through the normal machinery, with no +// special support -- see clang/test/Frontend/plugin-attribute-instantiation.cpp. +// // This plugin is used by clang/test/Frontend/plugin-attribute tests. // //===----------------------------------------------------------------------===// diff --git a/clang/test/AST/ast-dump-unknown-attr-template.cpp b/clang/test/AST/ast-dump-unknown-attr-template.cpp index 9a0b63be82a6b..2d2ce8a56372f 100644 --- a/clang/test/AST/ast-dump-unknown-attr-template.cpp +++ b/clang/test/AST/ast-dump-unknown-attr-template.cpp @@ -13,3 +13,17 @@ template struct S<int>; // CHECK: ClassTemplateSpecializationDecl {{.*}} struct S definition // CHECK: FieldDecl {{.*}} x 'int' // CHECK-NEXT: UnknownAttr {{.*}} ns::transient "(a, b)" + +// A dependent argument is retained verbatim. The attribute is ignored, so its +// arguments are never parsed, substituted, or checked: the specialization keeps +// the same text as the template, and instantiating with a type that has no such +// member is not an error. +template <class T> struct D { + int y [[vendor::attr(T::value + 1)]]; +}; +struct NoValue {}; +template struct D<NoValue>; + +// CHECK: ClassTemplateSpecializationDecl {{.*}} struct D definition +// CHECK: FieldDecl {{.*}} y 'int' +// CHECK-NEXT: UnknownAttr {{.*}} vendor::attr "(T::value + 1)" diff --git a/clang/test/Frontend/plugin-attribute-instantiation.cpp b/clang/test/Frontend/plugin-attribute-instantiation.cpp new file mode 100644 index 0000000000000..acb3a2b52908e --- /dev/null +++ b/clang/test/Frontend/plugin-attribute-instantiation.cpp @@ -0,0 +1,49 @@ +// Addresses the dependent-argument case raised on the unknown-attribute review: +// what it takes for a plugin to make Clang "accept" an attribute whose (possibly +// dependent) expression argument participates in template instantiation. +// +// The key is that the plugin registers the attribute (ParsedAttrInfo), so Clang +// parses the argument as an expression in context at definition time, and the +// Attribute example lowers it onto an Expr-carrying attribute (AnnotateAttr). +// Clang's existing attribute-instantiation machinery then substitutes the +// dependent argument with no special support: retaining the tokens is not +// required, and re-parsing them after the fact is not either. +// +// Boundary: the substitution runs when the specialization is formed, after +// deduction, so a substitution failure is a hard error, not SFINAE. No attribute +// (enable_if included) does SFINAE on an argument's substitution failure, so that +// piece is a separate core feature, independent of plugins and of how the +// argument is stored. + +// RUN: split-file %s %t +// RUN: %clang_cc1 -std=c++20 -load %llvmshlibdir/Attribute%pluginext \ +// RUN: -ast-dump %t/subst.cpp | FileCheck %s +// RUN: not %clang_cc1 -std=c++20 -load %llvmshlibdir/Attribute%pluginext \ +// RUN: -fsyntax-only %t/fail.cpp 2>&1 | FileCheck --check-prefix=HARD-ERROR %s + +// REQUIRES: plugins, examples + +//--- subst.cpp +template <class T> [[example("tag", T::value + 1)]] void f() {} +struct Foo { static constexpr int value = 41; }; +template void f<Foo>(); + +// On the primary template the argument is a dependent expression. +// CHECK: FunctionTemplateDecl {{.*}} f +// CHECK: AnnotateAttr {{.*}} "example" +// CHECK: DependentScopeDeclRefExpr + +// On the instantiation T::value + 1 has been substituted to 42, bound to +// Foo::value: the plugin attribute's argument is live. +// CHECK: FunctionDecl {{.*}} f 'void ()' explicit_instantiation_definition +// CHECK: AnnotateAttr {{.*}} "example" +// CHECK: ConstantExpr {{.*}} 'int' +// CHECK-NEXT: value: Int 42 + +//--- fail.cpp +template <class T> [[example("tag", T::value + 1)]] void f() {} +struct NoValue {}; +template void f<NoValue>(); + +// The substitution failure is a hard error, not SFINAE. +// HARD-ERROR: error: no member named 'value' in 'NoValue' >From 29f72425d0a5fab1a1f6d15209437f8a6f7e5756 Mon Sep 17 00:00:00 2001 From: Vassil Vassilev <[email protected]> Date: Fri, 11 Sep 2026 06:59:38 +0000 Subject: [PATCH 4/4] [clang] Test that a retained type attribute survives instantiation The RFC review asked whether an unknown attribute wrapped in an AttributedType is lost during template instantiation. It is not: TreeTransform rebuilds the AttributedType, the same path [[clang::annotate_type]] takes, so the specialization keeps the attribute with the substituted underlying type. Pin that in the type-position ast-dump test. --- clang/test/AST/ast-dump-unknown-attr-type.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/clang/test/AST/ast-dump-unknown-attr-type.cpp b/clang/test/AST/ast-dump-unknown-attr-type.cpp index bf86ab755a55f..38036a63bac27 100644 --- a/clang/test/AST/ast-dump-unknown-attr-type.cpp +++ b/clang/test/AST/ast-dump-unknown-attr-type.cpp @@ -16,3 +16,15 @@ int *[[ns::transient(a, b)]] p; // 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){{\]\]}}; + +// The wrapper survives template instantiation: TreeTransform rebuilds the +// AttributedType, so the specialization's member keeps the attribute with the +// substituted underlying type. This is the same path [[clang::annotate_type]] +// takes, so no special support is needed. +template <class T> struct S { + T *[[ns::transient(a, b)]] q; +}; +template struct S<int>; + +// CHECK: ClassTemplateSpecializationDecl {{.*}} struct S definition +// CHECK: FieldDecl {{.*}} q 'int * {{\[\[}}ns::transient(a, b){{\]\]}}':'int *' _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
