This tutorial describes how to create Clang plugin and covers the next things:
- environment setup
- basic plugin setup
- setup Xcode project for plugin development
- warnings reporting
- errors reporting
- Xcode integration
- interactive hints for errors/warnings riddance
tl;dr
Clang Rocks!!!You can find the plugin here.Intro
While working on BloodMagic, I realised that it’d be nice to have a tool for checking semantic errors related to BM usage. For example: in the interface property marked aslazy
, but not defined as @dynamic
in the implementation, or property marked as lazy
, but class container doesn’t support injections.I concluded that I need to work with and I need a full-featured parser.I’ve tried different approaches: flex+bison, libclang, but ultimately I decided to write a Clang plugin.Just for testing purposes I’ve started a simple plugin with the following goals:- use Xcode for development
- integrate ready plugin into Xcode and use it workaday
- plugin should report warnings, errors and propose interactive hints for fixes (via Xcode UI)
- report warning in case of class’ name starts with lowercase letter
- report error in case of class’ name contains underscore
_
- propose hints for fixes
Environment setup
For plugin development we need llvm/clang, built from source1 2 3 4 5 | cd /opt sudo mkdir llvm sudo chown `whoami` llvm cd llvm export LLVM_HOME=`pwd` |
clang
version on my system – 3.3.1, so let’s build respective version:1 2 3 4 5 6 7 8 9 | git clone -b release_33 https://github.com/llvm-mirror/llvm.git llvm git clone -b release_33 https://github.com/llvm-mirror/clang.git llvm/tools/clang git clone -b release_33 https://github.com/llvm-mirror/clang-tools-extra.git llvm/tools/clang/tools/extra git clone -b release_33 https://github.com/llvm-mirror/compiler-rt.git llvm/projects/compiler-rt mkdir llvm_build cd llvm_build cmake ../llvm -DCMAKE_BUILD_TYPE:STRING=Release make -j`sysctl -n hw.logicalcpu` |
Basic plugin setup
Create directory for plugin1 2 | cd $LLVM_HOME mkdir toy_clang_plugin; cd toy_clang_plugin |
1 2 3 | ToyClangPlugin.exports CMakeLists.txt ToyClangPlugin.cpp |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | #include "clang/Frontend/FrontendPluginRegistry.h" #include "clang/AST/AST.h" #include "clang/AST/ASTConsumer.h" #include "clang/Frontend/CompilerInstance.h" using namespace clang; namespace { class ToyConsumer : public ASTConsumer { }; class ToyASTAction : public PluginASTAction { public: virtual clang::ASTConsumer *CreateASTConsumer(CompilerInstance &Compiler, llvm::StringRef InFile) { return new ToyConsumer; } bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string>& args) { return true; } }; } static clang::FrontendPluginRegistry::Add<ToyASTAction> X("ToyClangPlugin", "Toy Clang Plugin"); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | cmake_minimum_required (VERSION 2.6) project (ToyClangPlugin) set( CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin ) set( CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib ) set( CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib ) set( LLVM_HOME /opt/llvm ) set( LLVM_SRC_DIR ${LLVM_HOME}/llvm ) set( CLANG_SRC_DIR ${LLVM_HOME}/llvm/tools/clang ) set( LLVM_BUILD_DIR ${LLVM_HOME}/llvm_build ) set( CLANG_BUILD_DIR ${LLVM_HOME}/llvm_build/tools/clang) add_definitions (-D__STDC_LIMIT_MACROS -D__STDC_CONSTANT_MACROS) add_definitions (-D_GNU_SOURCE -DHAVE_CLANG_CONFIG_H) set (CMAKE_CXX_COMPILER "${LLVM_BUILD_DIR}/bin/clang++") set (CMAKE_CC_COMPILER "${LLVM_BUILD_DIR}/bin/clang") set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC -fno-common -Woverloaded-virtual -Wcast-qual -fno-strict-aliasing -pedantic -Wno-long-long -Wall -Wno-unused-parameter -Wwrite-strings -fno-exceptions -fno-rtti") set (CMAKE_MODULE_LINKER_FLAGS "-Wl,-flat_namespace -Wl,-undefined -Wl,suppress") set (LLVM_LIBS LLVMJIT LLVMX86CodeGen LLVMX86AsmParser LLVMX86Disassembler LLVMExecutionEngine LLVMAsmPrinter LLVMSelectionDAG LLVMX86AsmPrinter LLVMX86Info LLVMMCParser LLVMCodeGen LLVMX86Utils LLVMScalarOpts LLVMInstCombine LLVMTransformUtils LLVMipa LLVMAnalysis LLVMTarget LLVMCore LLVMMC LLVMSupport LLVMBitReader LLVMOption ) macro(add_clang_plugin name) set (srcs ${ARGN}) include_directories( "${LLVM_SRC_DIR}/include" "${CLANG_SRC_DIR}/include" "${LLVM_BUILD_DIR}/include" "${CLANG_BUILD_DIR}/include" ) link_directories( "${LLVM_BUILD_DIR}/lib" ) add_library( ${name} SHARED ${srcs} ) if (SYMBOL_FILE) set_target_properties( ${name} PROPERTIES LINK_FlAGS "-exported_symbols_list ${SYMBOL_FILE}") endif() foreach (clang_lib ${CLANG_LIBS}) target_link_libraries( ${name} ${clang_lib} ) endforeach() foreach (llvm_lib ${LLVM_LIBS}) target_link_libraries( ${name} ${llvm_lib} ) endforeach() foreach (user_lib ${USER_LIBS}) target_link_libraries( ${name} ${user_lib} ) endforeach() endmacro(add_clang_plugin) set(SYMBOL_FILE ToyClangPlugin.exports) set (CLANG_LIBS clang clangFrontend clangAST clangAnalysis clangBasic clangCodeGen clangDriver clangFrontendTool clangLex clangParse clangSema clangEdit clangSerialization clangStaticAnalyzerCheckers clangStaticAnalyzerCore clangStaticAnalyzerFrontend ) set (USER_LIBS pthread curses ) add_clang_plugin(ToyClangPlugin ToyClangPlugin.cpp ) set_target_properties(ToyClangPlugin PROPERTIES LINKER_LANGUAGE CXX PREFIX "") |
1 | __ZN4llvm8Registry* |
CMakeLists.txt
1 2 3 | mkdir build; cd build cmake -G Xcode .. open ToyClangPlugin.xcodeproj |
ALL_BUILD
target, and you’ll see dynamic library at lib/Debug/ToyCLangPlugin.dylib
.RecursiveASTVisitor
Clang AST module provides RecursiveASTVisitor, which allows us to traverse via AST. We just need to create a subclass and implement interesting methods. For test we’ll print all the found class names.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class ToyClassVisitor : public RecursiveASTVisitor<ToyClassVisitor> { public: bool VisitObjCInterfaceDecl(ObjCInterfaceDecl *declaration) { printf("ObjClass: %s\n", declaration->getNameAsString().c_str()); return true; } }; class ToyConsumer : public ASTConsumer { public: void HandleTranslationUnit(ASTContext &context) { visitor.TraverseDecl(context.getTranslationUnitDecl()); } private: ToyClassVisitor visitor; }; |
1 2 3 4 5 6 7 8 9 | #import <Foundation/Foundation.h> @interface ToyObject : NSObject @end @implementation ToyObject @end |
1 2 3 4 5 | /opt/llvm/toy_clang_plugin/build $ $LLVM_HOME/llvm_build/bin/clang ../test.m \ -Xclang -load \ -Xclang lib/Debug/ToyClangPlugin.dylib \ -Xclang -plugin \ -Xclang ToyClangPlugin |
Report warnings
Let’s report warning in case if class’ name starts with lowercase letterAddASTContext
to ToyClassVisitor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | class ToyClassVisitor : public RecursiveASTVisitor<ToyClassVisitor> { private: ASTContext *context; public: void setContext(ASTContext &context) { this->context = &context; } // ... }; // ... void HandleTranslationUnit(ASTContext &context) { visitor.setContext(context); visitor.TraverseDecl(context.getTranslationUnitDecl()); } // ... |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | bool VisitObjCInterfaceDecl(ObjCInterfaceDecl *declaration) { checkForLowercasedName(declaration); return true; } // ... void checkForLowercasedName(ObjCInterfaceDecl *declaration) { StringRef name = declaration->getName(); char c = name[0]; if (isLowercase(c)) { DiagnosticsEngine &diagEngine = context->getDiagnostics(); unsigned diagID = diagEngine.getCustomDiagID(DiagnosticsEngine::Warning, "Class name should not start with lowercase letter"); SourceLocation location = declaration->getLocation(); diagEngine.Report(location, diagID); } } |
1 2 3 4 5 6 7 | @interface bad_ToyObject : NSObject @end @implementation bad_ToyObject @end |
1 2 3 4 5 6 7 8 9 10 | /opt/llvm/toy_clang_plugin/build $ $LLVM_HOME/llvm_build/bin/clang ../test.m \ -Xclang -load \ -Xclang lib/Debug/ToyClangPlugin.dylib \ -Xclang -plugin \ -Xclang ToyClangPlugin ../test.m:11:12: warning: Class name should not start with lowercase letter @interface bad_ToyObject : NSObject ^ 1 warning generated. |
Report error
Let’s generate error in case of class’ name contains_
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | void checkForUnderscoreInName(ObjCInterfaceDecl *declaration) { size_t underscorePos = declaration->getName().find('_'); if (underscorePos != StringRef::npos) { DiagnosticsEngine &diagEngine = context->getDiagnostics(); unsigned diagID = diagEngine.getCustomDiagID(DiagnosticsEngine::Error, "Class name with `_` forbidden"); SourceLocation location = declaration->getLocation().getLocWithOffset(underscorePos); diagEngine.Report(location, diagID); } } bool VisitObjCInterfaceDecl(ObjCInterfaceDecl *declaration) { // disable this check temporary // checkForLowercasedName(declaration); checkForUnderscoreInName(declaration); return true; } |
1 2 3 4 5 6 7 8 9 10 | /opt/llvm/toy_clang_plugin/build $ $LLVM_HOME/llvm_build/bin/clang ../test.m \ -Xclang -load \ -Xclang lib/Debug/ToyClangPlugin.dylib \ -Xclang -plugin \ -Xclang ToyClangPlugin ../test.m:11:15: error: Class name with `_` forbidden @interface bad_ToyObject : NSObject ^ 1 error generated. |
checkForLowercasedName
and you’ll see both error and warning in the output1 2 3 4 5 6 7 8 9 10 11 12 13 | /opt/llvm/toy_clang_plugin/build $ $LLVM_HOME/llvm_build/bin/clang ../test.m \ -Xclang -load \ -Xclang lib/Debug/ToyClangPlugin.dylib \ -Xclang -plugin \ -Xclang ToyClangPlugin ../test.m:11:12: warning: Class name should not start with lowercase letter @interface bad_ToyObject : NSObject ^ ../test.m:11:15: error: Class name with `_` forbidden @interface bad_ToyObject : NSObject ^ 1 warning and 1 error generated. |
Xcode integration
Unfortunately, system (under ‘system’ I mean Xcode’s clang) clang doesn’t support plugins, so we need to hack Xcode a bit, to allow using of custom compiler.Unzip this archive and run following commands:1 2 | sudo mv HackedClang.xcplugin `xcode-select -print-path`/../PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins sudo mv HackedBuildSystem.xcspec `xcode-select -print-path`/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Specifications |

Build settings
To enable plugin add following parameters to the OTHER_CFLAGS
section1 | -Xclang -load -Xclang /opt/llvm/toy_clang_plugin/build/lib/Debug/ToyClangPlugin.dylib -Xclang -add-plugin -Xclang ToyClangPlugin |

-add-plugin
here, because we want to add our ASTAction
, not to replace the existingAlso, you should disable modules for this target/build
test.m
to this project, or create new one, with class names that match plugin criteriaAfter build you’ll see error and warnings in a more familiar form
Interactive hints
It’s time to addFixItHints
for both warning and error1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | void checkForLowercasedName(ObjCInterfaceDecl *declaration) { StringRef name = declaration->getName(); char c = name[0]; if (isLowercase(c)) { std::string tempName = name; tempName[0] = toUppercase(c); StringRef replacement(tempName); SourceLocation nameStart = declaration->getLocation(); SourceLocation nameEnd = nameStart.getLocWithOffset(name.size()); FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement); DiagnosticsEngine &diagEngine = context->getDiagnostics(); unsigned diagID = diagEngine.getCustomDiagID(DiagnosticsEngine::Warning, "Class name should not start with lowercase letter"); SourceLocation location = declaration->getLocation(); diagEngine.Report(location, diagID).AddFixItHint(fixItHint); } } void checkForUnderscoreInName(ObjCInterfaceDecl *declaration) { StringRef name = declaration->getName(); size_t underscorePos = name.find('_'); if (underscorePos != StringRef::npos) { std::string tempName = name; std::string::iterator end_pos = std::remove(tempName.begin(), tempName.end(), '_'); tempName.erase(end_pos, tempName.end()); StringRef replacement(tempName); SourceLocation nameStart = declaration->getLocation(); SourceLocation nameEnd = nameStart.getLocWithOffset(name.size()); FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement); DiagnosticsEngine &diagEngine = context->getDiagnostics(); unsigned diagID = diagEngine.getCustomDiagID(DiagnosticsEngine::Error, "Class name with `_` forbidden"); SourceLocation location = declaration->getLocation().getLocWithOffset(underscorePos); diagEngine.Report(location, diagID).AddFixItHint(fixItHint); } } |

