[clang] [clang][RAV] Visit components of __builtin_offsetof designators (PR #194122)

Jakob Linke via cfe-commits cfe-commits at lists.llvm.org
Sat Apr 25 00:00:03 PDT 2026


https://github.com/schuay created https://github.com/llvm/llvm-project/pull/194122

`RecursiveASTVisitor` previously only traversed the type operand of an `OffsetOfExpr,` ignoring the field/identifier/base/array-index components of the designator. This meant tools built on RAV (clang-tidy, clangd, indexers, ...) silently missed every field reference inside `__builtin_offsetof(T, a.b.c)`.

Add a `TraverseOffsetOfNode / VisitOffsetOfNode` pair following the same pattern used for `ConceptReference`, `ObjCProtocolLoc`, and friends. The `DEF_TRAVERSE_STMT` for `OffsetOfExpr` now invokes `TraverseOffsetOfNode` for each component; array index expressions continue to be reached via the existing children() traversal. Default visitation is a no-op, so the change is opt-in for consumers and behavior-preserving otherwise.

Also expose `OffsetOfNode` as a `DynTypedNode` kind via `ASTTypeTraits` so that downstream machinery (`SelectionTree`, parent maps, matchers) can reference these nodes uniformly.

The same gap exists for Designator inside `DesignatedInitExpr` and can follow this recipe in a follow-up.

Related: https://github.com/llvm/llvm-project/pull/192953. The current PR enables precise component resolves in clangd (support to be added in a followup).

>From 9d1a47575d68978de18c5e05653a566070565377 Mon Sep 17 00:00:00 2001
From: Jakob Linke <jakob at linke.cx>
Date: Sat, 25 Apr 2026 07:37:13 +0200
Subject: [PATCH] [clang][RAV] Visit components of __builtin_offsetof
 designators

RecursiveASTVisitor previously only traversed the type operand of an
OffsetOfExpr, ignoring the field/identifier/base/array-index components
of the designator. This meant tools built on RAV (clang-tidy, clangd,
indexers, ...) silently missed every field reference inside
__builtin_offsetof(T, a.b.c).

Add a TraverseOffsetOfNode / VisitOffsetOfNode pair following the same
pattern used for ConceptReference, ObjCProtocolLoc, and friends. The
DEF_TRAVERSE_STMT for OffsetOfExpr now invokes TraverseOffsetOfNode for
each component; array index expressions continue to be reached via the
existing children() traversal. Default visitation is a no-op, so the
change is opt-in for consumers and behavior-preserving otherwise.

Also expose OffsetOfNode as a DynTypedNode kind via ASTTypeTraits so
that downstream machinery (SelectionTree, parent maps, matchers) can
reference these nodes uniformly.

The same gap exists for Designator inside DesignatedInitExpr and can
follow this recipe in a follow-up.
---
 clang/include/clang/AST/ASTFwd.h              |   1 +
 clang/include/clang/AST/ASTTypeTraits.h       |   6 +
 .../clang/AST/DynamicRecursiveASTVisitor.h    |   5 +
 clang/include/clang/AST/RecursiveASTVisitor.h |  26 ++-
 clang/lib/AST/ASTTypeTraits.cpp               |   3 +
 clang/lib/AST/DynamicRecursiveASTVisitor.cpp  |   9 +
 clang/unittests/Tooling/CMakeLists.txt        |   1 +
 .../RecursiveASTVisitorTests/OffsetOfExpr.cpp | 155 ++++++++++++++++++
 8 files changed, 202 insertions(+), 4 deletions(-)
 create mode 100644 clang/unittests/Tooling/RecursiveASTVisitorTests/OffsetOfExpr.cpp

diff --git a/clang/include/clang/AST/ASTFwd.h b/clang/include/clang/AST/ASTFwd.h
index 8823663386ea1..440ae954ebe46 100644
--- a/clang/include/clang/AST/ASTFwd.h
+++ b/clang/include/clang/AST/ASTFwd.h
@@ -35,6 +35,7 @@ class Attr;
 #include "clang/Basic/AttrList.inc"
 class ObjCProtocolLoc;
 class ConceptReference;
+class OffsetOfNode;
 
 } // end namespace clang
 
diff --git a/clang/include/clang/AST/ASTTypeTraits.h b/clang/include/clang/AST/ASTTypeTraits.h
index 6f40705fb7436..e0925551ef1a2 100644
--- a/clang/include/clang/AST/ASTTypeTraits.h
+++ b/clang/include/clang/AST/ASTTypeTraits.h
@@ -164,6 +164,7 @@ class ASTNodeKind {
 #include "clang/Basic/AttrList.inc"
     NKI_ObjCProtocolLoc,
     NKI_ConceptReference,
+    NKI_OffsetOfNode,
     NKI_NumberOfKinds
   };
 
@@ -224,6 +225,7 @@ KIND_TO_KIND_ID(Attr)
 KIND_TO_KIND_ID(ObjCProtocolLoc)
 KIND_TO_KIND_ID(CXXBaseSpecifier)
 KIND_TO_KIND_ID(ConceptReference)
+KIND_TO_KIND_ID(OffsetOfNode)
 #define DECL(DERIVED, BASE) KIND_TO_KIND_ID(DERIVED##Decl)
 #include "clang/AST/DeclNodes.inc"
 #define STMT(DERIVED, BASE) KIND_TO_KIND_ID(DERIVED)
@@ -589,6 +591,10 @@ template <>
 struct DynTypedNode::BaseConverter<ConceptReference, void>
     : public PtrConverter<ConceptReference> {};
 
+template <>
+struct DynTypedNode::BaseConverter<OffsetOfNode, void>
+    : public PtrConverter<OffsetOfNode> {};
+
 // The only operation we allow on unsupported types is \c get.
 // This allows to conveniently use \c DynTypedNode when having an arbitrary
 // AST node that is not supported, but prevents misuse - a user cannot create
diff --git a/clang/include/clang/AST/DynamicRecursiveASTVisitor.h b/clang/include/clang/AST/DynamicRecursiveASTVisitor.h
index 7b5bdca318348..67b87171719e8 100644
--- a/clang/include/clang/AST/DynamicRecursiveASTVisitor.h
+++ b/clang/include/clang/AST/DynamicRecursiveASTVisitor.h
@@ -213,6 +213,11 @@ template <bool IsConst> class DynamicRecursiveASTVisitorBase {
     return true;
   }
 
+  /// Recursively visit a single component of an __builtin_offsetof
+  /// designator (a field, identifier, base-class, or array-index node).
+  virtual bool TraverseOffsetOfNode(const OffsetOfNode *Node);
+  virtual bool VisitOffsetOfNode(const OffsetOfNode *Node) { return true; }
+
   /// Visit a node.
   virtual bool VisitAttr(MaybeConst<Attr> *A) { return true; }
   virtual bool VisitDecl(MaybeConst<Decl> *D) { return true; }
diff --git a/clang/include/clang/AST/RecursiveASTVisitor.h b/clang/include/clang/AST/RecursiveASTVisitor.h
index 27f31efdb1f31..8b1a7bae94d46 100644
--- a/clang/include/clang/AST/RecursiveASTVisitor.h
+++ b/clang/include/clang/AST/RecursiveASTVisitor.h
@@ -323,6 +323,16 @@ template <typename Derived> class RecursiveASTVisitor {
 
   // Visit concept reference.
   bool VisitConceptReference(ConceptReference *CR) { return true; }
+
+  /// Recursively visit a single component of an __builtin_offsetof
+  /// designator (a field, identifier, base-class, or array-index node).
+  ///
+  /// \returns false if the visitation was terminated early, true otherwise.
+  bool TraverseOffsetOfNode(const OffsetOfNode *Node);
+
+  /// Visit a single component of an __builtin_offsetof designator.
+  bool VisitOffsetOfNode(const OffsetOfNode *Node) { return true; }
+
   // ---- Methods on Attrs ----
 
   // Visit an attribute.
@@ -2729,6 +2739,13 @@ bool RecursiveASTVisitor<Derived>::TraverseConceptReference(
   return true;
 }
 
+template <typename Derived>
+bool RecursiveASTVisitor<Derived>::TraverseOffsetOfNode(
+    const OffsetOfNode *Node) {
+  TRY_TO(VisitOffsetOfNode(Node));
+  return true;
+}
+
 // If shouldVisitImplicitCode() returns false, this method traverses only the
 // syntactic form of InitListExpr.
 // If shouldVisitImplicitCode() return true, this method is called once for
@@ -2800,11 +2817,12 @@ DEF_TRAVERSE_STMT(CXXNewExpr, {
 })
 
 DEF_TRAVERSE_STMT(OffsetOfExpr, {
-  // The child-iterator will pick up the expression representing
-  // the field.
-  // FIXME: for code like offsetof(Foo, a.b.c), should we get
-  // making a MemberExpr callbacks for Foo.a, Foo.a.b, and Foo.a.b.c?
   TRY_TO(TraverseTypeLoc(S->getTypeSourceInfo()->getTypeLoc()));
+  // Visit each designator component (e.g. the `a`, `b`, `c` in
+  // offsetof(Foo, a.b.c)). Array index expressions are reached through the
+  // child-iterator, which DEF_TRAVERSE_STMT walks automatically.
+  for (unsigned I = 0, E = S->getNumComponents(); I != E; ++I)
+    TRY_TO(TraverseOffsetOfNode(&S->getComponent(I)));
 })
 
 DEF_TRAVERSE_STMT(UnaryExprOrTypeTraitExpr, {
diff --git a/clang/lib/AST/ASTTypeTraits.cpp b/clang/lib/AST/ASTTypeTraits.cpp
index 84eb77730b1cb..c717fd77d6ba6 100644
--- a/clang/lib/AST/ASTTypeTraits.cpp
+++ b/clang/lib/AST/ASTTypeTraits.cpp
@@ -56,6 +56,7 @@ const ASTNodeKind::KindInfo ASTNodeKind::AllKindInfo[] = {
 #include "clang/Basic/AttrList.inc"
     {NKI_None, "ObjCProtocolLoc"},
     {NKI_None, "ConceptReference"},
+    {NKI_None, "OffsetOfNode"},
 };
 
 bool ASTNodeKind::isBaseOf(ASTNodeKind Other) const {
@@ -279,5 +280,7 @@ SourceRange DynTypedNode::getSourceRange(bool IncludeQualifier) const {
     return P->getSourceRange();
   if (const ConceptReference *C = get<ConceptReference>())
     return C->getSourceRange();
+  if (const OffsetOfNode *O = get<OffsetOfNode>())
+    return O->getSourceRange();
   return SourceRange();
 }
diff --git a/clang/lib/AST/DynamicRecursiveASTVisitor.cpp b/clang/lib/AST/DynamicRecursiveASTVisitor.cpp
index 6d7925b437b04..dde18aba4fcf7 100644
--- a/clang/lib/AST/DynamicRecursiveASTVisitor.cpp
+++ b/clang/lib/AST/DynamicRecursiveASTVisitor.cpp
@@ -163,6 +163,13 @@ template <bool Const> struct Impl : RecursiveASTVisitor<Impl<Const>> {
     return Visitor.TraverseConceptReference(CR);
   }
 
+  bool TraverseOffsetOfNode(const OffsetOfNode *Node) {
+    return Visitor.TraverseOffsetOfNode(Node);
+  }
+  bool VisitOffsetOfNode(const OffsetOfNode *Node) {
+    return Visitor.VisitOffsetOfNode(Node);
+  }
+
   bool TraverseCXXBaseSpecifier(const CXXBaseSpecifier &Base) {
     return Visitor.TraverseCXXBaseSpecifier(Base);
   }
@@ -311,6 +318,8 @@ FORWARD_TO_BASE(TraverseConceptReference, ConceptReference, *)
 FORWARD_TO_BASE(TraverseConceptNestedRequirement,
                 concepts::NestedRequirement, *)
 
+FORWARD_TO_BASE_EXACT(TraverseOffsetOfNode, const OffsetOfNode *)
+
 FORWARD_TO_BASE_EXACT(TraverseCXXBaseSpecifier, const CXXBaseSpecifier &)
 FORWARD_TO_BASE_EXACT(TraverseDeclarationNameInfo, DeclarationNameInfo)
 FORWARD_TO_BASE_EXACT(TraverseTemplateArgument, const TemplateArgument &)
diff --git a/clang/unittests/Tooling/CMakeLists.txt b/clang/unittests/Tooling/CMakeLists.txt
index 8c8b22250cd83..4de300fa86385 100644
--- a/clang/unittests/Tooling/CMakeLists.txt
+++ b/clang/unittests/Tooling/CMakeLists.txt
@@ -42,6 +42,7 @@ add_clang_unittest(ToolingTests
   RecursiveASTVisitorTests/LambdaTemplateParams.cpp
   RecursiveASTVisitorTests/MemberPointerTypeLoc.cpp
   RecursiveASTVisitorTests/NestedNameSpecifiers.cpp
+  RecursiveASTVisitorTests/OffsetOfExpr.cpp
   RecursiveASTVisitorTests/ParenExpr.cpp
   RecursiveASTVisitorTests/TemplateArgumentLocTraverser.cpp
   RecursiveASTVisitorTests/TraversalScope.cpp
diff --git a/clang/unittests/Tooling/RecursiveASTVisitorTests/OffsetOfExpr.cpp b/clang/unittests/Tooling/RecursiveASTVisitorTests/OffsetOfExpr.cpp
new file mode 100644
index 0000000000000..742989303c43c
--- /dev/null
+++ b/clang/unittests/Tooling/RecursiveASTVisitorTests/OffsetOfExpr.cpp
@@ -0,0 +1,155 @@
+//===- unittest/Tooling/RecursiveASTVisitorTests/OffsetOfExpr.cpp ---------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "TestVisitor.h"
+#include "clang/AST/Expr.h"
+
+using namespace clang;
+
+namespace {
+
+struct OffsetOfNodeRecorder : TestVisitor {
+  struct Component {
+    OffsetOfNode::Kind Kind;
+    // Field name for Field/Identifier kinds; empty otherwise.
+    std::string Name;
+    // For Array kind: the slot in OffsetOfExpr's trailing index-expression
+    // array, NOT the literal subscript value. Slots are assigned in source
+    // order (so e.g. `a[2].b[7].c` produces Array nodes referring to slots
+    // 0 and 1, whose Expr*s evaluate to 2 and 7 respectively).
+    unsigned ArrayExprSlot = ~0u;
+    // Whether the pointer received in TraverseOffsetOfNode equals the one
+    // that the enclosing OffsetOfExpr exposes via getComponent().
+    bool PointerStable = false;
+  };
+
+  bool TraverseOffsetOfNode(const OffsetOfNode *Node) override {
+    ++Traversed;
+    return TestVisitor::TraverseOffsetOfNode(Node);
+  }
+
+  bool VisitOffsetOfNode(const OffsetOfNode *Node) override {
+    Component C;
+    C.Kind = Node->getKind();
+    if (C.Kind == OffsetOfNode::Field)
+      C.Name = Node->getField()->getNameAsString();
+    else if (C.Kind == OffsetOfNode::Identifier)
+      C.Name = Node->getFieldName()->getName().str();
+    else if (C.Kind == OffsetOfNode::Array)
+      C.ArrayExprSlot = Node->getArrayExprIndex();
+    C.PointerStable = StableAddresses.count(Node) != 0;
+    Visited.push_back(std::move(C));
+    return true;
+  }
+
+  bool VisitOffsetOfExpr(OffsetOfExpr *E) override {
+    for (unsigned I = 0, N = E->getNumComponents(); I != N; ++I)
+      StableAddresses.insert(&E->getComponent(I));
+    return true;
+  }
+
+  llvm::DenseSet<const OffsetOfNode *> StableAddresses;
+  std::vector<Component> Visited;
+  int Traversed = 0;
+};
+
+TEST(RecursiveASTVisitor, OffsetOfFlatField) {
+  OffsetOfNodeRecorder Recorder;
+  EXPECT_TRUE(Recorder.runOver(
+      "struct Foo { int bar; };\n"
+      "unsigned long x = __builtin_offsetof(struct Foo, bar);\n",
+      OffsetOfNodeRecorder::Lang_C));
+  ASSERT_EQ(1u, Recorder.Visited.size());
+  EXPECT_EQ(1, Recorder.Traversed);
+  EXPECT_EQ(OffsetOfNode::Field, Recorder.Visited[0].Kind);
+  EXPECT_EQ("bar", Recorder.Visited[0].Name);
+  EXPECT_TRUE(Recorder.Visited[0].PointerStable);
+}
+
+TEST(RecursiveASTVisitor, OffsetOfNestedFields) {
+  OffsetOfNodeRecorder Recorder;
+  EXPECT_TRUE(Recorder.runOver(
+      "struct Inner { int c; };\n"
+      "struct Mid { struct Inner b; };\n"
+      "struct Outer { struct Mid a; };\n"
+      "unsigned long x = __builtin_offsetof(struct Outer, a.b.c);\n",
+      OffsetOfNodeRecorder::Lang_C));
+  ASSERT_EQ(3u, Recorder.Visited.size());
+  EXPECT_EQ(3, Recorder.Traversed);
+  EXPECT_EQ(OffsetOfNode::Field, Recorder.Visited[0].Kind);
+  EXPECT_EQ("a", Recorder.Visited[0].Name);
+  EXPECT_EQ(OffsetOfNode::Field, Recorder.Visited[1].Kind);
+  EXPECT_EQ("b", Recorder.Visited[1].Name);
+  EXPECT_EQ(OffsetOfNode::Field, Recorder.Visited[2].Kind);
+  EXPECT_EQ("c", Recorder.Visited[2].Name);
+  for (const auto &C : Recorder.Visited)
+    EXPECT_TRUE(C.PointerStable);
+}
+
+TEST(RecursiveASTVisitor, OffsetOfArrayAndField) {
+  OffsetOfNodeRecorder Recorder;
+  // Two array subscripts mean two Array OffsetOfNodes; their getArrayExprIndex
+  // values should be 0 and 1 in source order, indexing into OffsetOfExpr's
+  // trailing Expr* array (which holds the literals `2` and `7`).
+  EXPECT_TRUE(Recorder.runOver(
+      "struct Inner { int c; };\n"
+      "struct Mid { struct Inner b[10]; };\n"
+      "struct Outer { struct Mid a[5]; };\n"
+      "unsigned long x = __builtin_offsetof(struct Outer, a[2].b[7].c);\n",
+      OffsetOfNodeRecorder::Lang_C));
+  ASSERT_EQ(5u, Recorder.Visited.size());
+  EXPECT_EQ(5, Recorder.Traversed);
+  EXPECT_EQ(OffsetOfNode::Field, Recorder.Visited[0].Kind);
+  EXPECT_EQ("a", Recorder.Visited[0].Name);
+  EXPECT_EQ(OffsetOfNode::Array, Recorder.Visited[1].Kind);
+  EXPECT_EQ(0u, Recorder.Visited[1].ArrayExprSlot);
+  EXPECT_EQ(OffsetOfNode::Field, Recorder.Visited[2].Kind);
+  EXPECT_EQ("b", Recorder.Visited[2].Name);
+  EXPECT_EQ(OffsetOfNode::Array, Recorder.Visited[3].Kind);
+  EXPECT_EQ(1u, Recorder.Visited[3].ArrayExprSlot);
+  EXPECT_EQ(OffsetOfNode::Field, Recorder.Visited[4].Kind);
+  EXPECT_EQ("c", Recorder.Visited[4].Name);
+}
+
+TEST(RecursiveASTVisitor, OffsetOfDependentIdentifier) {
+  OffsetOfNodeRecorder Recorder;
+  EXPECT_TRUE(Recorder.runOver(
+      "template <typename T>\n"
+      "unsigned long off() { return __builtin_offsetof(T, x.y); }\n",
+      OffsetOfNodeRecorder::Lang_CXX11));
+  // Inside a dependent type the components are recorded as Identifier nodes
+  // because the field decls are not yet known. We expect both `x` and `y`.
+  ASSERT_EQ(2u, Recorder.Visited.size());
+  EXPECT_EQ(OffsetOfNode::Identifier, Recorder.Visited[0].Kind);
+  EXPECT_EQ("x", Recorder.Visited[0].Name);
+  EXPECT_EQ(OffsetOfNode::Identifier, Recorder.Visited[1].Kind);
+  EXPECT_EQ("y", Recorder.Visited[1].Name);
+}
+
+// Verifies that overriding only Visit (the typical use case) suffices: the
+// default TraverseOffsetOfNode in (Dynamic)RecursiveASTVisitor must dispatch
+// to VisitOffsetOfNode.
+struct VisitOnly : TestVisitor {
+  int Visits = 0;
+  bool VisitOffsetOfNode(const OffsetOfNode *Node) override {
+    ++Visits;
+    return true;
+  }
+};
+
+TEST(RecursiveASTVisitor, OffsetOfDefaultTraverseDispatchesToVisit) {
+  VisitOnly Recorder;
+  EXPECT_TRUE(Recorder.runOver(
+      "struct Inner { int c; };\n"
+      "struct Outer { struct Inner a; };\n"
+      "unsigned long x = __builtin_offsetof(struct Outer, a.c);\n",
+      VisitOnly::Lang_C));
+  EXPECT_EQ(2, Recorder.Visits);
+}
+
+} // namespace



More information about the cfe-commits mailing list