[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 13:53:40 PDT 2026
https://github.com/dmaclach updated https://github.com/llvm/llvm-project/pull/211119
>From 64395057d2563aaf1db82b2bf4fa24a786756193 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 | 103 +++++++
.../include-cleaner/unittests/WalkASTTest.cpp | 265 +++++++++++++++++-
2 files changed, 365 insertions(+), 3 deletions(-)
diff --git a/clang-tools-extra/include-cleaner/lib/WalkAST.cpp b/clang-tools-extra/include-cleaner/lib/WalkAST.cpp
index d444ddd90839d..71eb89dfae5ea 100644
--- a/clang-tools-extra/include-cleaner/lib/WalkAST.cpp
+++ b/clang-tools-extra/include-cleaner/lib/WalkAST.cpp
@@ -395,6 +395,109 @@ 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) {
+ if (E->isMessagingGetter()) {
+ if (auto *Getter = E->isExplicitProperty()
+ ? E->getExplicitProperty()->getGetterMethodDecl()
+ : E->getImplicitPropertyGetter()) {
+ report(E->getLocation(), Getter);
+ }
+ }
+ if (E->isMessagingSetter()) {
+ if (auto *Setter = E->isExplicitProperty()
+ ? E->getExplicitProperty()->getSetterMethodDecl()
+ : E->getImplicitPropertySetter()) {
+ report(E->getLocation(), Setter);
+ }
+ }
+ return true;
+ }
+
+ bool VisitObjCProtocolExpr(ObjCProtocolExpr *E) {
+ if (auto *Proto = E->getProtocol()) {
+ report(E->getProtocolIdLoc(), Proto);
+ }
+ 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..db8948926a344 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,262 @@ 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, 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, 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