- PVSM.RU - https://www.pvsm.ru -
Готовый плагин можно найти здесь [1]
В процессе разработки BloodMagic [2] я решил что было бы здорово иметь инструмент для поиска семантических ошибок при использовании BM. К примеру, свойство отмечено как lazy
в интерфейсе, но в имплементации не отмечено как @dynamic, или отмечено как lazy
, но класс контейнер не поддерживает инъекции. Я пришел к выводу что прийдется работать с AST [3], а потому нужен полноценный парсер.
Я пробовал разные варианты: flex [4] + bison [5], libclang [6], но в конце концов решил написать плагин для Clang.
Для тестового плагина я задался следующими целями:
Фичи для тестового плагина:
Для разработки плагина нам нужен llvm/clang, собранный из исходников
cd /opt
sudo mkdir llvm
sudo chown `whoami` llvm
cd llvm
export LLVM_HOME=`pwd`
Текущая версия clang на моей машине — 3.3.1, потому я использую соответствующую версию:
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`
Создайте директорию для плагина
cd $LLVM_HOME
mkdir toy_clang_plugin; cd toy_clang_plugin
Наш плагин основан на примере из репозитория Clang'а и имеет следующую структуру:
ToyClangPlugin.exports
CMakeLists.txt
ToyClangPlugin.cpp
Мы будем использовать один файл для упрощения:
// ToyClangPlugin.cpp
#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");
Данные необходимые для сборки:
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 "")
__ZN4llvm8Registry*
Теперь мы можем сгенерировать Xcode-проект на основе `CMakeLists.txt`
mkdir build; cd build
cmake -G Xcode ..
open ToyClangPlugin.xcodeproj
Запустите 'ALL_BUILD', в случае успеха готовая библиотека будет лежать здесь: `lib/Debug/ToyCLangPlugin.dylib`.
Модуль AST предоставляет RecursiveASTVisitor [7], который позволяет проходить по синтаксическому дереву. Все что нам нужно, это отнаследоваться и реализовать интересующие методы.
В качестве небольшого теста выведем на экран все встретившиеся классы:
class ToyClassVisitor : public RecursiveASTVisitor<ToyClassVisitor>
{
public:
bool VisitObjCInterfaceDecl(ObjCInterfaceDecl *declaration)
{
printf("ObjClass: %sn", declaration->getNameAsString().c_str());
return true;
}
};
class ToyConsumer : public ASTConsumer
{
public:
void HandleTranslationUnit(ASTContext &context) {
visitor.TraverseDecl(context.getTranslationUnitDecl());
}
private:
ToyClassVisitor visitor;
};
Создадим тестовый класс и проверим работу плагина
#import <Foundation/Foundation.h>
@interface ToyObject : NSObject
@end
@implementation ToyObject
@end
Запуск плагина
/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
На выходе должен быть огромный список классов.
Если имя класса начинается с буквы в нижнем регистре, то пользователь будет видеть предупреждение.
Для генерирования предупреждений нужен контекст
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());
}
// ...
Валидация имени класса:
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);
}
}
Теперь нужно добавить класс с «плохим» именем
@interface bad_ToyObject : NSObject
@end
@implementation bad_ToyObject
@end
и проверить работу плагина
/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.
Если имя класса содержит подчеркивание ('_'), то пользователь будет видеть ошибку.
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;
}
Вывод после запуска
/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.
Раскоментируйте первую проверку и на выходе будут и ошибка и предупреждение
/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.
К сожалению, системный (под системным я понимаю clang из поставки Xcode) clang не поддерживает плагины, потому нужно немного похачить Xcode, чтобы можно было пользоваться кастомным компилятором
Распакуйте этот [8] архив и выполните следующие команды:
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
Эти хаки добавят новый компилятор в Xcode и позволят собирать им проекты для OSX и iPhoneSimulator.
После перезапуска Xcode вы будете видеть новый clang в списке
Создайте новый проект и выберите наш кастомный clang в 'Build settings'.
Чтобы включить плагин нужно добавить следующие параметры в 'Other C Flags'
-Xclang -load -Xclang /opt/llvm/toy_clang_plugin/build/lib/Debug/ToyClangPlugin.dylib -Xclang -add-plugin -Xclang ToyClangPlugin
Обратите внимание, что здесь мы используем `-add-plugin`, потому как хотим добавить наш `ASTAction`, а не заменить существующий.
Также нужно отключить модули для этой сборки:
Добавьте в этот проект наш `test.m` или создайте новый класс, с именами подходящими под критерии плагина.
После сборки вы должны увидеть предупреждения и ошибки в более привычной форме:
Теперь стоит добавить и интерактивные подсказки для исправления ошибок и предупреждений
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);
}
}
Пересоберите плагин и запустите сборку тестового проекта
Как видите, создание плагина для clang относительно простое занятие, но требует грязных хаков с Xcode, и нужно собирать свой clang, потому я бы не рекомендовал использовать кастомный компилятор для сборки приложений в production. Apple предоставляет патченую версию clang'а, и мы не можем знать в чем отличие. Кроме того Clang-плагин для Xcode требует немало усилий для того чтобы сделать его работоспособным, что не делает его особо юзабельным.
Есть еще одна проблема, с которой можно столкнуться при разработке, — нестабильный и постоянно изменяющийся API.
Вы можете использовать подобные плагины на вашей системе, но, пожалуйста, не заставляйте других людей зависеть от таких тяжелых штук.
Ели у вас есть какие-то комментарии, вопросы или предложения пишите в twitter [9], GitHub [10] или просто оставьте комментарий здесь.
Happy hacking!
Автор: 1101_debian
Источник [11]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-3/56249
Ссылки в тексте:
[1] здесь: https://github.com/AlexDenisov/ToyClangPlugin
[2] BloodMagic: https://github.com/railsware/BloodMagic
[3] AST: http://ru.wikipedia.org/wiki/%D0%90%D0%B1%D1%81%D1%82%D1%80%D0%B0%D0%BA%D1%82%D0%BD%D0%BE%D0%B5_%D1%81%D0%B8%D0%BD%D1%82%D0%B0%D0%BA%D1%81%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5_%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE
[4] flex: http://ru.wikipedia.org/wiki/Flex_(%D0%B3%D0%B5%D0%BD%D0%B5%D1%80%D0%B0%D1%82%D0%BE%D1%80_%D0%BB%D0%B5%D0%BA%D1%81%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D1%85_%D0%B0%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%82%D0%BE%D1%80%D0%BE%D0%B2)
[5] bison: http://ru.wikipedia.org/wiki/GNU_bison
[6] libclang: http://clang.llvm.org/doxygen/group__CINDEX.html
[7] RecursiveASTVisitor: http://clang.llvm.org/doxygen/classclang_1_1RecursiveASTVisitor.html
[8] этот: https://github.com/AlexDenisov/ToyClangPlugin/releases/download/0.0.1/XcodeHacking.zip
[9] twitter: https://twitter.com/1101_debian
[10] GitHub: https://github.com/AlexDenisov/ToyClangPlugin/issues/new
[11] Источник: http://habrahabr.ru/post/214275/
Нажмите здесь для печати.