[clang-tools-extra] Add Objective-C support to include-cleaner's AST walker. (PR #211119)
via cfe-commits
cfe-commits at lists.llvm.org
Thu Jul 23 22:16:20 PDT 2026
https://github.com/dmaclach updated https://github.com/llvm/llvm-project/pull/211119
>From f2520b1581449fb1e0db2aa24dbcb450e523e6f5 Mon Sep 17 00:00:00 2001
From: Dave MacLachlan <dmaclach at gmail.com>
Date: Tue, 21 Jul 2026 14:56:35 -0700
Subject: [PATCH] Add Objective-C support to include-cleaner's AST walker.
Extend WalkAST to recognize and report references in Objective-C constructs,
including interfaces, protocols, message expressions, properties, categories,
compatible aliases, and instance variables. Also update the test helper to
support custom compiler arguments and add corresponding unit tests.
---
.../include-cleaner/lib/WalkAST.cpp | 166 +++++++++-
.../include-cleaner/unittests/WalkASTTest.cpp | 296 +++++++++++++++++-
2 files changed, 457 insertions(+), 5 deletions(-)
diff --git a/clang-tools-extra/include-cleaner/lib/WalkAST.cpp b/clang-tools-extra/include-cleaner/lib/WalkAST.cpp
index d444ddd90839d..e3e610b8c33d8 100644
--- a/clang-tools-extra/include-cleaner/lib/WalkAST.cpp
+++ b/clang-tools-extra/include-cleaner/lib/WalkAST.cpp
@@ -15,7 +15,6 @@
#include "clang/AST/DeclTemplate.h"
#include "clang/AST/Expr.h"
#include "clang/AST/ExprCXX.h"
-#include "clang/AST/NestedNameSpecifier.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/AST/TemplateBase.h"
#include "clang/AST/TemplateName.h"
@@ -25,7 +24,6 @@
#include "clang/Basic/OperatorKinds.h"
#include "clang/Basic/SourceLocation.h"
#include "clang/Basic/Specifiers.h"
-#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/STLFunctionalExtras.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/Support/Casting.h"
@@ -395,6 +393,170 @@ class ASTWalker : public RecursiveASTVisitor<ASTWalker> {
report(E->getExprLoc(), E->getOperatorDelete(), RefType::Ambiguous);
return true;
}
+
+ // Objective-C support
+
+ bool VisitObjCInterfaceTypeLoc(ObjCInterfaceTypeLoc TL) {
+ reportType(TL.getNameLoc(), TL.getIFaceDecl());
+ return true;
+ }
+
+ // Protocols are odd in that they are covered by Traverse instead of Visit.
+ bool TraverseObjCProtocolLoc(ObjCProtocolLoc ProtocolLoc) {
+ if (auto *Proto = ProtocolLoc.getProtocol()) {
+ report(ProtocolLoc.getLocation(), Proto);
+ }
+ return true;
+ }
+
+ bool VisitObjCImplementationDecl(ObjCImplementationDecl *D) {
+ if (auto *Interface = D->getClassInterface()) {
+ report(D->getLocation(), Interface);
+ }
+ return true;
+ }
+
+ bool VisitObjCMessageExpr(ObjCMessageExpr *E) {
+ // Identify the selector and the method declaration
+ if (auto *Method = E->getMethodDecl()) {
+ // Report the method as a used symbol
+ report(E->getSelectorStartLoc(), Method);
+ }
+
+ // If it's a class message, report the interface/class as used
+ if (E->getReceiverKind() == ObjCMessageExpr::Class) {
+ if (auto *Interface = E->getReceiverInterface()) {
+ report(E->getReceiverRange().getBegin(), Interface);
+ }
+ }
+ return true;
+ }
+
+ bool VisitObjCPropertyDecl(clang::ObjCPropertyDecl *PD) {
+ reportType(PD->getLocation(), PD);
+ return true;
+ }
+
+ bool VisitObjCPropertyRefExpr(ObjCPropertyRefExpr *E) {
+ // Unconditionally report property declarations and their backing accessor
+ // methods. Dot-notation or pseudo-object references (`foo.bar`) require
+ // the underlying property definition or getters/setters to compile.
+ // Bypassing transient compiler state flags (isMessagingGetter/
+ // isMessagingSetter) guarantees that the declaring
+ // header keeps the properties recorded as used.
+ if (E->isExplicitProperty()) {
+ if (auto *Prop = E->getExplicitProperty()) {
+ report(E->getLocation(), Prop);
+ if (auto *Getter = Prop->getGetterMethodDecl())
+ report(E->getLocation(), Getter);
+ if (auto *Setter = Prop->getSetterMethodDecl())
+ report(E->getLocation(), Setter);
+ }
+ } else {
+ if (auto *Getter = E->getImplicitPropertyGetter())
+ report(E->getLocation(), Getter);
+ if (auto *Setter = E->getImplicitPropertySetter())
+ report(E->getLocation(), Setter);
+ }
+ return true;
+ }
+
+ bool VisitObjCProtocolExpr(ObjCProtocolExpr *E) {
+ if (auto *Proto = E->getProtocol()) {
+ report(E->getProtocolIdLoc(), Proto);
+ }
+ return true;
+ }
+
+ bool VisitCastExpr(CastExpr *E) {
+ // Handle implicit or explicit casts between Objective-C object pointers
+ // aimed towards protocol-qualification (e.g., `ClassName *` to
+ // `id<Proto>`).
+ QualType SourceType = E->getSubExpr()->getType();
+ QualType DestType = E->getType();
+
+ const auto *SrcPtr = SourceType->getAs<ObjCObjectPointerType>();
+ const auto *DestPtr = DestType->getAs<ObjCObjectPointerType>();
+
+ // If we're casting from a known class pointer to protocol conformance.
+ if (SrcPtr && DestPtr && SrcPtr->getInterfaceDecl()) {
+ const ObjCInterfaceDecl *Class = SrcPtr->getInterfaceDecl();
+ ASTContext &Ctx = Class->getASTContext();
+
+ // For every protocol required by the destination type.
+ for (const ObjCProtocolDecl *Proto : DestPtr->quals()) {
+ const ObjCInterfaceDecl *Current = Class;
+ // Search the inheritance hierarchy for the provider of conformance.
+ while (Current) {
+ bool ConformsDirectly = false;
+ for (const auto *PI : Current->protocols()) {
+ if (Ctx.ProtocolCompatibleWithProtocol(
+ const_cast<ObjCProtocolDecl *>(Proto),
+ const_cast<ObjCProtocolDecl *>(PI))) {
+ ConformsDirectly = true;
+ break;
+ }
+ }
+ // If the class itself provides the conformance directly, we don't
+ // need to keep searching Categories.
+ if (ConformsDirectly)
+ break;
+
+ // If the class doesn't declare direct conformance but conformance is
+ // injected via a visible Category attached to this class, note that
+ // the category header is required by recording an Implicit reference
+ // to it.
+ for (const auto *Cat : Current->visible_categories()) {
+ for (auto *PI : Cat->protocols()) {
+ if (Ctx.ProtocolCompatibleWithProtocol(
+ const_cast<ObjCProtocolDecl *>(Proto),
+ const_cast<ObjCProtocolDecl *>(PI))) {
+ report(E->getExprLoc(), const_cast<ObjCCategoryDecl *>(Cat),
+ RefType::Implicit);
+ }
+ }
+ }
+ Current = Current->getSuperClass();
+ }
+ }
+ }
+ return true;
+ }
+
+ bool VisitObjCCategoryDecl(ObjCCategoryDecl *D) {
+ // A category declaration depends on its base interface.
+ if (auto *Interface = D->getClassInterface()) {
+ report(D->getLocation(), Interface);
+ }
+ return true;
+ }
+
+ bool VisitObjCCategoryImplDecl(ObjCCategoryImplDecl *D) {
+ // Implementation requires the base interface.
+ if (auto *Interface = D->getClassInterface()) {
+ report(D->getLocation(), Interface);
+ }
+ // Implementation requires the category declaration.
+ if (auto *Category = D->getCategoryDecl()) {
+ report(D->getCategoryNameLoc(), Category);
+ }
+ return true;
+ }
+
+ bool VisitObjCCompatibleAliasDecl(ObjCCompatibleAliasDecl *D) {
+ // An alias declaration requires the underlying class.
+ if (auto *Aliased = D->getClassInterface()) {
+ report(D->getLocation(), Aliased);
+ }
+ return true;
+ }
+
+ bool VisitObjCIvarRefExpr(ObjCIvarRefExpr *E) {
+ if (auto *Ivar = E->getDecl()) {
+ report(E->getLocation(), Ivar);
+ }
+ return true;
+ }
};
} // namespace
diff --git a/clang-tools-extra/include-cleaner/unittests/WalkASTTest.cpp b/clang-tools-extra/include-cleaner/unittests/WalkASTTest.cpp
index 3487f24f2af8f..6bf0bde9cfa10 100644
--- a/clang-tools-extra/include-cleaner/unittests/WalkASTTest.cpp
+++ b/clang-tools-extra/include-cleaner/unittests/WalkASTTest.cpp
@@ -41,8 +41,9 @@ using testing::ElementsAre;
// Referencing: int x = ^foo();
// There must be exactly one referencing location marked.
// Returns target decls.
-std::vector<Decl::Kind> testWalk(llvm::StringRef TargetCode,
- llvm::StringRef ReferencingCode) {
+std::vector<Decl::Kind>
+testWalk(llvm::StringRef TargetCode, llvm::StringRef ReferencingCode,
+ std::vector<std::string> ExtraArgs = {"-std=c++20"}) {
llvm::Annotations Target(TargetCode);
llvm::Annotations Referencing(ReferencingCode);
@@ -50,7 +51,8 @@ std::vector<Decl::Kind> testWalk(llvm::StringRef TargetCode,
Inputs.ExtraFiles["target.h"] = Target.code().str();
Inputs.ExtraArgs.push_back("-include");
Inputs.ExtraArgs.push_back("target.h");
- Inputs.ExtraArgs.push_back("-std=c++20");
+ for (const auto &Arg : ExtraArgs)
+ Inputs.ExtraArgs.push_back(Arg);
TestAST AST(Inputs);
const auto &SM = AST.sourceManager();
@@ -576,5 +578,293 @@ TEST(WalkAST, CleanupAttr) {
"void foo() { __attribute__((__cleanup__(^freep))) char* x = 0; }");
}
+TEST(WalkAST, ObjCInterfaceTypeLoc) {
+ testWalk(R"objc(
+ @interface $explicit^MyClass
+ @end
+ )objc",
+ R"objc(
+ void test() {
+ ^MyClass *obj;
+ }
+ )objc",
+ {"-x", "objective-c"});
+}
+
+TEST(WalkAST, ObjCImplementationDeclDependsOnInterface) {
+ testWalk(R"objc(
+ @interface $explicit^MyClass
+ @end
+ )objc",
+ R"objc(
+ @implementation ^MyClass
+ @end
+ )objc",
+ {"-x", "objective-c"});
+}
+
+TEST(WalkAST, ObjCMessageExprSelectorLoc) {
+ testWalk(R"objc(
+ @interface MyClass
+ $explicit^- (void)doSomething;
+ @end
+ )objc",
+ R"objc(
+ void test(MyClass *obj) {
+ [obj ^doSomething];
+ }
+ )objc",
+ {"-x", "objective-c"});
+}
+
+TEST(WalkAST, ObjCMessageExprClassReceiver) {
+ testWalk(R"objc(
+ @interface $explicit^MyClass
+ + (void)classMethod;
+ @end
+ )objc",
+ R"objc(
+ void test() {
+ [^MyClass classMethod];
+ }
+ )objc",
+ {"-x", "objective-c"});
+}
+
+TEST(WalkAST, ObjCPropertyRefExprExplicit) {
+ testWalk(R"objc(
+ @interface MyClass
+ @property(nonatomic) int $explicit^foo;
+ @end
+ )objc",
+ R"objc(
+ void test(MyClass *obj) {
+ int x = obj.^foo;
+ }
+ )objc",
+ {"-x", "objective-c"});
+}
+
+TEST(WalkAST, ObjCPropertyRefExprImplicitGetter) {
+ testWalk(R"objc(
+ @interface MyClass
+ $explicit^- (int)foo;
+ @end
+ )objc",
+ R"objc(
+ void test(MyClass *obj) {
+ int x = obj.^foo;
+ }
+ )objc",
+ {"-x", "objective-c"});
+}
+
+TEST(WalkAST, ObjCPropertyRefExprImplicitSetter) {
+ testWalk(R"objc(
+ @interface MyClass
+ $explicit^- (void)setFoo:(int)val;
+ @end
+ )objc",
+ R"objc(
+ void test(MyClass *obj) {
+ obj.^foo = 42;
+ }
+ )objc",
+ {"-x", "objective-c"});
+}
+
+TEST(WalkAST, ObjCPropertyRefExprExplicitSetter) {
+ testWalk(R"objc(
+ @interface MyClass
+ @property(nonatomic) int $explicit^foo;
+ @end
+ )objc",
+ R"objc(
+ void test(MyClass *obj) {
+ obj.^foo = 42;
+ }
+ )objc",
+ {"-x", "objective-c"});
+}
+
+TEST(WalkAST, ObjCPropertyRefExprProtocol) {
+ testWalk(R"objc(
+ @protocol MyProtocol
+ @property(nonatomic) int $explicit^foo;
+ @end
+ )objc",
+ R"objc(
+ void test(id<MyProtocol> obj) {
+ int x = obj.^foo;
+ }
+ )objc",
+ {"-x", "objective-c"});
+}
+
+TEST(WalkAST, ObjCProtocolInType) {
+ testWalk(R"objc(
+ @protocol $explicit^MyProtocol
+ @end
+ )objc",
+ R"objc(
+ void test() {
+ id<^MyProtocol> obj;
+ }
+ )objc",
+ {"-x", "objective-c"});
+}
+
+TEST(WalkAST, ObjCProtocolInClassInterface) {
+ testWalk(R"objc(
+ @protocol $explicit^MyProtocol
+ @end
+ )objc",
+ R"objc(
+ @interface MyClass <^MyProtocol>
+ @end
+ )objc",
+ {"-x", "objective-c"});
+}
+
+TEST(WalkAST, ObjCProtocolInProtocolInheritance) {
+ testWalk(R"objc(
+ @protocol $explicit^ParentProtocol
+ @end
+ )objc",
+ R"objc(
+ @protocol MyProtocol <^ParentProtocol>
+ @end
+ )objc",
+ {"-x", "objective-c"});
+}
+
+TEST(WalkAST, ObjCProtocolExpr) {
+ testWalk(R"objc(
+ @protocol $explicit^MyProtocol
+ @end
+ )objc",
+ R"objc(
+ void test() {
+ Protocol* p = @protocol(^MyProtocol);
+ }
+ )objc",
+ {"-x", "objective-c"});
+}
+
+TEST(WalkAST, ObjCCategoryDeclDependsOnInterface) {
+ testWalk(R"objc(
+ @interface $explicit^MyClass
+ @end
+ )objc",
+ R"objc(
+ @interface ^MyClass (Category)
+ @end
+ )objc",
+ {"-x", "objective-c"});
+}
+
+TEST(WalkAST, ObjCCategoryImplDependsOnInterface) {
+ testWalk(R"objc(
+ @interface $explicit^MyClass
+ @end
+ )objc",
+ R"objc(
+ @interface MyClass (Category)
+ @end
+ @implementation ^MyClass (Category)
+ @end
+ )objc",
+ {"-x", "objective-c"});
+}
+
+TEST(WalkAST, ObjCCategoryImplDependsOnCategoryDecl) {
+ testWalk(R"objc(
+ @interface MyClass
+ @end
+ @interface $explicit^MyClass (Category)
+ @end
+ )objc",
+ R"objc(
+ @implementation MyClass (^Category)
+ @end
+ )objc",
+ {"-x", "objective-c"});
+}
+
+TEST(WalkAST, ObjCImplicitCastToProtocolConformingCategory) {
+ testWalk(R"objc(
+ @protocol MyProtocol
+ @end
+ @interface MyClass
+ @end
+ @interface $implicit^MyClass (MyCategory) <MyProtocol>
+ @end
+ )objc",
+ R"objc(
+ void test(MyClass *obj) {
+ id<MyProtocol> p = ^obj;
+ }
+ )objc",
+ {"-x", "objective-c"});
+}
+
+TEST(WalkAST, ObjCCompatibleAliasDecl) {
+ testWalk(R"objc(
+ @interface $explicit^MyClass
+ @end
+ )objc",
+ R"objc(
+ ^@compatibility_alias AliasName MyClass;
+ )objc",
+ {"-x", "objective-c"});
+}
+
+TEST(WalkAST, ObjCCompatibleAliasUsage) {
+ testWalk(R"objc(
+ @interface $explicit^MyClass
+ @end
+ @compatibility_alias AliasName MyClass;
+ )objc",
+ R"objc(
+ void test() {
+ ^AliasName *obj;
+ }
+ )objc",
+ {"-x", "objective-c"});
+}
+
+TEST(WalkAST, ObjCIvarRefExprExplicit) {
+ testWalk(R"objc(
+ @interface MyClass {
+ @public
+ int $explicit^foo;
+ }
+ @end
+ )objc",
+ R"objc(
+ void test(MyClass *obj) {
+ int x = obj->^foo;
+ }
+ )objc",
+ {"-x", "objective-c"});
+}
+
+TEST(WalkAST, ObjCIvarRefExprFree) {
+ testWalk(R"objc(
+ @interface MyClass {
+ int $explicit^foo;
+ }
+ @end
+ )objc",
+ R"objc(
+ @implementation MyClass
+ - (void)test {
+ int x = ^foo;
+ }
+ @end
+ )objc",
+ {"-x", "objective-c"});
+}
+
} // namespace
} // namespace clang::include_cleaner
More information about the cfe-commits
mailing list