本文主要是介绍如何使用clang-format格式化OC代码,并且结合Xcode Extension做成热键。
要实现的功能点:
- clang-format格式化当前xcode停留的.m文件内容
- 一个代码排序功能(生命周期函数位于最顶层)
clang-format介绍:
官网地址:http://clang.llvm.org/docs/ClangFormat.html
clang-format是基于 LibFormat的更高级的封装,可以根据预先的设置来格式化代码。
clang-format获取:
可以按照之前的文章使用clang编写XCode代码检测插件来获取llvm的源码,按照该文章中的内容,直接编译源码为Xcode项目即可。(不用写文章中的自定义插件)
1 2 | cd /User/LQ/llvm/llvm_build cmake -G Xcode -DCMAKE_BUILD_TYPE:STRING=Release ../llvm |
打开Xcode项目,选择

然后选择

截屏2020-08-26 下午5.21.42.png
然后编译,生成可执行文件
clang-format使用:
终端输入
首先,准备一个叫做
然后,终端进入
1 | bogon:getenv 58liqiang$ ./clang-format /Users/58liqiang/Desktop/58Test/OC/getenv/getenv/ViewController.m |
结果:
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 | #import "ViewController.h" @interface ViewController () @end @implementation ViewController { /// 哈哈 unsigned int age; } typedef enum : int { a, b, c, } abc; void test1() {} // 这里是viewDidLoad - (void)viewDidLoad { } - (void)viewDidAppear { } - (void)viewWillAppear { } - (void)loadView { } - (void)viewWillLayoutSubviews { } void test2() {} ... |
Xcode Extension介绍:
Xcode Extension用于替代Xcode插件,关于基础知识可以参考《手把手教你用Source Editor Extension开发Xcode插件-实战import排序的插件开发》。
大体来说,我们要在下面的函数中做处理:
1 | - (void)performCommandWithInvocation:(XCSourceEditorCommandInvocation *)invocation completionHandler:(void (^)(NSError * _Nullable nilOrError))completionHandler |
该属性中最重要的属性是
如此,我们有了基本思路:通过Xcode Extension 获取当前文件的内容,然后使用clang-format 对内容进行格式化,最后再替换数组中的内容即可。
开始工作:
创建
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // 创建任务,执行可执行文件 NSPipe *outputPipe = [NSPipe pipe]; NSPipe *errorPipe = [NSPipe pipe]; NSTask *task = [[NSTask alloc] init]; task.standardOutput = outputPipe; task.standardError = errorPipe; NSString *path = [[NSBundle mainBundle] pathForResource:@"clang-format" ofType:nil]; task.launchPath = path; task.arguments = @[]; // 这里需要传递参数 NSError *error; [task launchAndReturnError:&error]; [task waitUntilExit]; // clang-format输出的格式化代码,转化为NSSting,再根据换行符进行分割,替换buffer.lines中的内容即可 NSData *formatted = [outputPipe.fileHandleForReading readDataToEndOfFileAndReturnError:&error]; |
现在,我们遇到了第一个问题:
改造clang-format程序
在llvm的Xcode项目中,找到
1 2 3 4 5 6 7 8 9 10 11 12 13 | ... if (FileNames.size() != 1 && (!Offsets.empty() || !Lengths.empty() || !LineRanges.empty())) { errs() << "error: -offset, -length and -lines can only be used for " "single file.\n"; return 1; } for (const auto &FileName : FileNames) { if (Verbose) errs() << "Formatting " << FileName << "\n"; Error |= clang::format::format(FileName); } return Error ? 1 : 0; } |
可以看到,原始逻辑为根据传递的文件路径来格式化文件。
我们进入format函数看看(这里为了方便我删除了一些代码,但是大概逻辑是不变的):
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 | static bool format(StringRef FileName) { ... // 1. 获取源代码 ErrorOr<std::unique_ptr<MemoryBuffer>> CodeOrErr = !OutputXML && Inplace ? MemoryBuffer::getFileAsStream(FileName) : MemoryBuffer::getFileOrSTDIN(FileName); if (std::error_code EC = CodeOrErr.getError()) { errs() << EC.message() << "\n"; return true; } std::unique_ptr<llvm::MemoryBuffer> Code = std::move(CodeOrErr.get()); if (Code->getBufferSize() == 0) return false; // Empty files are formatted correctly. std::vector<tooling::Range> Ranges; if (fillRanges(Code.get(), Ranges)) return true; StringRef AssumedFileName = (FileName == "-") ? AssumeFileName : FileName; // 2.获取代码格式 llvm::Expected<FormatStyle> FormatStyle = getStyle(Style, AssumedFileName, FallbackStyle, Code->getBuffer()); if (!FormatStyle) { llvm::errs() << llvm::toString(FormatStyle.takeError()) << "\n"; return true; } if (SortIncludes.getNumOccurrences() != 0) FormatStyle->SortIncludes = SortIncludes; unsigned CursorPosition = Cursor; Replacements Replaces = sortIncludes(*FormatStyle, Code->getBuffer(), Ranges, AssumedFileName, &CursorPosition); auto ChangedCode = tooling::applyAllReplacements(Code->getBuffer(), Replaces); if (!ChangedCode) { llvm::errs() << llvm::toString(ChangedCode.takeError()) << "\n"; return true; } // 3. 开始格式化 Ranges = tooling::calculateRangesAfterReplacements(Replaces, Ranges); FormattingAttemptStatus Status; Replacements FormatChanges = reformat(*FormatStyle, *ChangedCode, Ranges, AssumedFileName, &Status); ... |
获取代码格式
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 | llvm::Expected<FormatStyle> getStyle(StringRef StyleName, StringRef FileName, StringRef FallbackStyleName, StringRef Code, vfs::FileSystem *FS) { if (!FS) { FS = vfs::getRealFileSystem().get(); } // 获取llvm的格式 FormatStyle Style = getLLVMStyle(); // 根据文件名获取语言类型 Style.Language = getLanguageByFileName(FileName); // This is a very crude detection of whether a header contains ObjC code that // should be improved over time and probably be done on tokens, not one the // bare content of the file. /* 粗略的判断是否是oc代码 */ if (Style.Language == FormatStyle::LK_Cpp && FileName.endswith(".h") && (Code.contains("\n- (") || Code.contains("\n+ (") || Code.contains("\n@end\n") || Code.contains("\n@end ") || Code.endswith("@end"))) Style.Language = FormatStyle::LK_ObjC; // 获取一个空格式(实际上是llvm,只是被禁用了) FormatStyle FallbackStyle = getNoStyle(); // 根据传入的FallbackStyleName获取对应的格式,并且将语言类型赋值给它 if (!getPredefinedStyle(FallbackStyleName, Style.Language, &FallbackStyle)) return make_string_error("Invalid fallback style "" + FallbackStyleName); // 如果StyleName是{开始的,那么就认为是YAML或者json格式 if (StyleName.startswith("{")) { // Parse YAML/JSON style from the command line. if (std::error_code ec = parseConfiguration(StyleName, &Style)) return make_string_error("Error parsing -style: " + ec.message()); return Style; } // 如果StyleName不是file,那么获取一个空格式,并且将语言类型赋值给它 if (!StyleName.equals_lower("file")) { if (!getPredefinedStyle(StyleName, Style.Language, &Style)) return make_string_error("Invalid value for -style"); return Style; } // 到这里StyleName只能是file了,那么需要去查找.clang-format或者_clang-format文件 // Look for .clang-format/_clang-format file in the file's parent directories. SmallString<128> UnsuitableConfigFiles; // 获取文件路径 SmallString<128> Path(FileName); // 获取绝对路径 if (std::error_code EC = FS->makeAbsolute(Path)) return make_string_error(EC.message()); // for循环查找.clang-format配置文件 for (StringRef Directory = Path; !Directory.empty(); Directory = llvm::sys::path::parent_path(Directory)) { auto Status = FS->status(Directory); if (!Status || Status->getType() != llvm::sys::fs::file_type::directory_file) { continue; } SmallString<128> ConfigFile(Directory); llvm::sys::path::append(ConfigFile, ".clang-format"); DEBUG(llvm::dbgs() << "Trying " << ConfigFile << "...\n"); Status = FS->status(ConfigFile.str()); bool FoundConfigFile = Status && (Status->getType() == llvm::sys::fs::file_type::regular_file); if (!FoundConfigFile) { // Try _clang-format too, since dotfiles are not commonly used on Windows. ConfigFile = Directory; llvm::sys::path::append(ConfigFile, "_clang-format"); DEBUG(llvm::dbgs() << "Trying " << ConfigFile << "...\n"); Status = FS->status(ConfigFile.str()); FoundConfigFile = Status && (Status->getType() == llvm::sys::fs::file_type::regular_file); } if (FoundConfigFile) { llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> Text = FS->getBufferForFile(ConfigFile.str()); if (std::error_code EC = Text.getError()) { printf("文件读取失败!!!-%s",ConfigFile.c_str()); return make_string_error(EC.message()); } if (std::error_code ec = parseConfiguration(Text.get()->getBuffer(), &Style)) { if (ec == ParseError::Unsuitable) { if (!UnsuitableConfigFiles.empty()) UnsuitableConfigFiles.append(", "); UnsuitableConfigFiles.append(ConfigFile); continue; } return make_string_error("Error reading " + ConfigFile + ": " + ec.message()); } DEBUG(llvm::dbgs() << "Using configuration file " << ConfigFile << "\n"); return Style; } } if (!UnsuitableConfigFiles.empty()) return make_string_error("Configuration file(s) do(es) not support " + getLanguageName(Style.Language) + ": " + UnsuitableConfigFiles); return FallbackStyle; } |
实际上,
如此,我们需要改造
- 源码(用于格式化)
- 语言类型(用于使用指定语言类型的格式化)
-
.clang-format 文件路径 (指定的格式化内容)
改造1,增加命令行参数,并且修改main函数
在ClangFormat.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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | /* 文件内容,因为使用xcodeExtensions的时候,只能拿到代码,不能拿到文件 */ static cl::opt<std::string> FileContent("s", cl::desc("the content of file."), cl::cat(ClangFormatCategory)); /* 语言类型,正常会根据传入的文件名来解析语言,但是因为无法拿到文件,因此这里语言直接让外部指定 */ static cl::opt<std::string> FileLanguage("l", cl::desc("the language of format."), cl::init("objc"), cl::cat(ClangFormatCategory)); /* format文件路径。一般会根据传入的文件所在目录开始查找。但是这个无法取到文件,因此format文件路径也需要外部指定,并且保证是可读的。 */ static cl::opt<std::string> FormatPath("p", cl::desc("the .clang-format file's path."), cl::cat(ClangFormatCategory)); int main(int argc, const char **argv) { ... // -------------改动点:直接处理文件内容 if (!FileContent.empty()) { Error |= clang::format::jr_format(FileContent, FileLanguage, FormatPath); return Error ? 1 : 0; } // ------------ if (FileNames.empty()) { Error = clang::format::format("-"); return Error ? 1 : 0; } if (FileNames.size() != 1 && (!Offsets.empty() || !Lengths.empty() || !LineRanges.empty())) { errs() << "error: -offset, -length and -lines can only be used for " "single file.\n"; return 1; } for (const auto &FileName : FileNames) { if (Verbose) errs() << "Formatting " << FileName << "\n"; Error |= clang::format::format(FileName); } return Error ? 1 : 0; } |
改造2,修改llvm::Expected getStyle(StringRef StyleName, StringRef FileName,StringRef FallbackStyleName, StringRef Code, vfs::FileSystem *FS) 函数。
修改头文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // 原始方法 llvm::Expected<FormatStyle> getStyle(StringRef StyleName, StringRef FileName, StringRef FallbackStyle, StringRef Code = "", vfs::FileSystem *FS = nullptr); // 我们增加的方法,相比于原函数,增加了两个参数:languageKind和formatPath llvm::Expected<FormatStyle> getStyleWithLanguage(StringRef StyleName, StringRef FileName, StringRef FallbackStyle, FormatStyle::LanguageKind languageKind, StringRef formatPath, StringRef Code = "", vfs::FileSystem *FS = nullptr ); |
在
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 | llvm::Expected<FormatStyle> getStyleWithLanguage(StringRef StyleName, StringRef FileName, StringRef FallbackStyleName, FormatStyle::LanguageKind languageKind, StringRef formatPath, StringRef Code, vfs::FileSystem *FS) { if (!FS) { FS = vfs::getRealFileSystem().get(); } // 获取llvm的格式 FormatStyle Style = getLLVMStyle(); // 改动点1:根据文件名获取语言类型 Style.Language = languageKind; // This is a very crude detection of whether a header contains ObjC code that // should be improved over time and probably be done on tokens, not one the // bare content of the file. /* 粗略的判断是否是oc代码 */ if (Style.Language == FormatStyle::LK_Cpp && FileName.endswith(".h") && (Code.contains("\n- (") || Code.contains("\n+ (") || Code.contains("\n@end\n") || Code.contains("\n@end ") || Code.endswith("@end"))) Style.Language = FormatStyle::LK_ObjC; // 获取一个空格式(实际上是llvm,只是被禁用了) FormatStyle FallbackStyle = getNoStyle(); // 根据传入的FallbackStyleName获取对应的格式,并且将语言类型赋值给它 if (!getPredefinedStyle(FallbackStyleName, Style.Language, &FallbackStyle)) return make_string_error("Invalid fallback style "" + FallbackStyleName); // 如果StyleName是{开始的,那么就认为是YAML或者json格式 if (StyleName.startswith("{")) { // Parse YAML/JSON style from the command line. if (std::error_code ec = parseConfiguration(StyleName, &Style)) return make_string_error("Error parsing -style: " + ec.message()); return Style; } // 如果StyleName不是file,那么获取一个空格式,并且将语言类型赋值给它 if (!StyleName.equals_lower("file")) { if (!getPredefinedStyle(StyleName, Style.Language, &Style)) return make_string_error("Invalid value for -style"); return Style; } // 到这里StyleName只能是file了,那么需要去查找.clang-format或者_clang-format文件 // Look for .clang-format/_clang-format file in the file's parent directories. SmallString<128> UnsuitableConfigFiles; // 改动点2:获取文件路径 SmallString<128> Path(formatPath); // 获取绝对路径 if (std::error_code EC = FS->makeAbsolute(Path)) return make_string_error(EC.message()); // for循环查找配置文件 for (StringRef Directory = Path; !Directory.empty(); Directory = llvm::sys::path::parent_path(Directory)) { auto Status = FS->status(Directory); if (!Status || Status->getType() != llvm::sys::fs::file_type::directory_file) { continue; } SmallString<128> ConfigFile(Directory); llvm::sys::path::append(ConfigFile, ".clang-format"); DEBUG(llvm::dbgs() << "Trying " << ConfigFile << "...\n"); Status = FS->status(ConfigFile.str()); bool FoundConfigFile = Status && (Status->getType() == llvm::sys::fs::file_type::regular_file); if (!FoundConfigFile) { // Try _clang-format too, since dotfiles are not commonly used on Windows. ConfigFile = Directory; llvm::sys::path::append(ConfigFile, "_clang-format"); DEBUG(llvm::dbgs() << "Trying " << ConfigFile << "...\n"); Status = FS->status(ConfigFile.str()); FoundConfigFile = Status && (Status->getType() == llvm::sys::fs::file_type::regular_file); } if (FoundConfigFile) { llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> Text = FS->getBufferForFile(ConfigFile.str()); if (std::error_code EC = Text.getError()) { return make_string_error(EC.message()); } if (std::error_code ec = parseConfiguration(Text.get()->getBuffer(), &Style)) { if (ec == ParseError::Unsuitable) { if (!UnsuitableConfigFiles.empty()) UnsuitableConfigFiles.append(", "); UnsuitableConfigFiles.append(ConfigFile); continue; } return make_string_error("Error reading " + ConfigFile + ": " + ec.message()); } DEBUG(llvm::dbgs() << "Using configuration file " << ConfigFile << "\n"); return Style; } } if (!UnsuitableConfigFiles.empty()) return make_string_error("Configuration file(s) do(es) not support " + getLanguageName(Style.Language) + ": " + UnsuitableConfigFiles); return FallbackStyle; } |
这段代码仅仅是在原来的实现上,修改了根据文件名获取语言类型,和根据文件名查找
改造3,修改format函数
在
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 | static bool jr_format(StringRef content, StringRef languageKind, StringRef formatPath) { std::unique_ptr<llvm::MemoryBuffer> Code = llvm::MemoryBuffer::getMemBuffer(content); if (Code->getBufferSize() == 0) return false; // Empty files are formatted correctly. std::vector<tooling::Range> Ranges; if (fillRanges(Code.get(), Ranges)) return true; /* 需要根据文件名来获取.clang-format文件。但是如果该可执行文件在xcode extension中,是没有权限读取文件内容的。因此会导致失败 解决方法: 由外部直接读取.clang-format文件内容,直接传递给可执行文件。 */ FormatStyle::LanguageKind lk; if (languageKind.endswith_lower("objc")) { lk = FormatStyle::LK_ObjC; } else if (languageKind.endswith_lower("cpp")) { lk = FormatStyle::LK_Cpp; } else if (languageKind.endswith_lower("java")) { lk = FormatStyle::LK_Java; } else if (languageKind.endswith_lower("js") || languageKind.endswith_lower("javascript")) { lk = FormatStyle::LK_JavaScript; } else if (languageKind.endswith_lower("proto") || languageKind.endswith_lower("protodevel")) { lk = FormatStyle::LK_Proto; } else if (languageKind.endswith_lower(".textpb") || languageKind.endswith_lower(".pb.txt") || languageKind.endswith_lower(".textproto") || languageKind.endswith_lower(".asciipb")) { lk = FormatStyle::LK_TextProto; } else if (languageKind.endswith_lower(".td")) { lk = FormatStyle::LK_TableGen; } else { lk = FormatStyle::LK_Cpp; } // <stdin>就是AssumedFileName的默认值 StringRef AssumedFileName("<stdin>"); llvm::Expected<FormatStyle> FormatStyle = getStyleWithLanguage(Style, AssumedFileName, FallbackStyle, lk, formatPath, Code->getBuffer()); if (!FormatStyle) { llvm::errs() << llvm::toString(FormatStyle.takeError()) << "\n"; return true; } if (SortIncludes.getNumOccurrences() != 0) FormatStyle->SortIncludes = SortIncludes; unsigned CursorPosition = Cursor; Replacements Replaces = sortIncludes(*FormatStyle, Code->getBuffer(), Ranges, AssumedFileName, &CursorPosition); auto ChangedCode = tooling::applyAllReplacements(Code->getBuffer(), Replaces); if (!ChangedCode) { llvm::errs() << llvm::toString(ChangedCode.takeError()) << "\n"; return true; } // Get new affected ranges after sorting `#includes`. Ranges = tooling::calculateRangesAfterReplacements(Replaces, Ranges); FormattingAttemptStatus Status; Replacements FormatChanges = reformat(*FormatStyle, *ChangedCode, Ranges, AssumedFileName, &Status); Replaces = Replaces.merge(FormatChanges); if (OutputXML) { outs() << "<?xml version='1.0'?>\n<replacements " "xml:space='preserve' incomplete_format='" << (Status.FormatComplete ? "false" : "true") << "'"; if (!Status.FormatComplete) outs() << " line='" << Status.Line << "'"; outs() << ">\n"; if (Cursor.getNumOccurrences() != 0) outs() << "<cursor>" << FormatChanges.getShiftedCodePosition(CursorPosition) << "</cursor>\n"; outputReplacementsXML(Replaces); outs() << "</replacements>\n"; } else { IntrusiveRefCntPtr<vfs::InMemoryFileSystem> InMemoryFileSystem( new vfs::InMemoryFileSystem); FileManager Files(FileSystemOptions(), InMemoryFileSystem); DiagnosticsEngine Diagnostics( IntrusiveRefCntPtr<DiagnosticIDs>(new DiagnosticIDs), new DiagnosticOptions); SourceManager Sources(Diagnostics, Files); FileID ID = createInMemoryFile(AssumedFileName, Code.get(), Sources, Files, InMemoryFileSystem.get()); Rewriter Rewrite(Sources, LangOptions()); tooling::applyAllReplacements(Replaces, Rewrite); if (Inplace) { if (Rewrite.overwriteChangedFiles()) return true; } else { if (Cursor.getNumOccurrences() != 0) { outs() << "{ "Cursor": " << FormatChanges.getShiftedCodePosition(CursorPosition) << ", "IncompleteFormat": " << (Status.FormatComplete ? "false" : "true"); if (!Status.FormatComplete) outs() << ", "Line": " << Status.Line; outs() << " }\n"; } Rewrite.getEditBuffer(ID).write(outs()); } } return false; } |
如此,我们可以通过
新的问题
现在,重新编译
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 | - (NSString *)language:(XCSourceTextBuffer *)buffer { CFStringRef uti = (__bridge CFStringRef)buffer.contentUTI; if (UTTypeEqual(uti, kUTTypeCHeader)) { // C header files could also be Objective-C. We attempt to detect typical Objective-C keywords. for (NSString* line in buffer.lines) { if ([line hasPrefix:@"#import"] || [line hasPrefix:@"@interface"] || [line hasPrefix:@"@protocol"] || [line hasPrefix:@"@property"] || [line hasPrefix:@"@end"]) { return @"objc"; } } } else if (UTTypeEqual(uti, kUTTypeCPlusPlusHeader) || UTTypeEqual(uti, kUTTypeCPlusPlusSource) || UTTypeEqual(uti, kUTTypeCHeader) || UTTypeEqual(uti, kUTTypeCSource)) { return @"cpp"; } else if (UTTypeEqual(uti, kUTTypeObjectiveCSource) || UTTypeEqual(uti, kUTTypeObjectiveCPlusPlusSource)) { return @"objc"; } else if (UTTypeEqual(uti, kUTTypeJavaSource)) { return @"java"; } else if (UTTypeEqual(uti, kUTTypeJavaScript)) { return @"javascript"; } return @"cpp"; } - (void)performCommandWithInvocation:(XCSourceEditorCommandInvocation *)invocation completionHandler:(void (^)(NSError * _Nullable nilOrError))completionHandler { NSString *code = invocation.buffer.completeBuffer; // 创建任务,执行可执行文件 NSPipe *outputPipe = [NSPipe pipe]; NSPipe *errorPipe = [NSPipe pipe]; NSTask *task = [[NSTask alloc] init]; task.standardOutput = outputPipe; task.standardError = errorPipe; NSString *path = [[NSBundle mainBundle] pathForResource:@"clang-format" ofType:nil]; task.launchPath = path; task.arguments = @[ @"-s", // sourceCode code, @"-l", // language [self language:invocation.buffer], @"-p", @"/Users/58liqiang/Desktop/.clang-format" ]; NSError *error; [task launchAndReturnError:&error]; [task waitUntilExit]; } |
如果此时运行项目,实际上并不会成功,会得到以下错误:
1 | Error Domain=NSCocoaErrorDomain Code=257 The file “xxx.m” couldn’t be opened because you don’t have permission to view it. |
这是因为,在macos开发中,不能直接读取沙盒之外的文件,必须经由用户选择才可以。
为了解决这个问题,我们需要在
解决方法(具体UI代码不再赘述):
- 使用NSPathControl让用户选择文件或者文件夹
- 将URL通过
- (nullable NSData *)bookmarkDataWithOptions:(NSURLBookmarkCreationOptions)options includingResourceValuesForKeys:(nullable NSArray 方法转化为NSData,保存在NSUserDefaults中。如此扩展中才有权限可以操作。*)keys relativeToURL:(nullable NSURL *)relativeURL error:(NSError **)error - 关闭主程序
- 扩展中增加从NSUserDefaults中取URL的操作,取出的URL如果是文件,则是可读可写的。如果是文件夹,则自行拼接具体文件路径,此时文件也是可读可写的。
如此一来,我们就解决了
大致代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // 获取.clang-format路径 if (!defaults) { defaults = [[NSUserDefaults alloc] initWithSuiteName:@"JRXFormat"]; } NSData* configurationBookmark = [defaults dataForKey:@"clangFormatPathKey"]; BOOL regularStale = NO; NSError *urlError; NSURL *clangFormatURL = [NSURL URLByResolvingBookmarkData:configurationBookmark options:NSURLBookmarkResolutionWithoutUI relativeToURL:nil bookmarkDataIsStale:®ularStale error:&urlError]; ... task.arguments = @[ @"-s", // sourceCode code, @"-l", // language [self language:invocation.buffer], @"-p", // formatPath [clangFormatURL relativePath] ]; |
代码排序功能实现
基本思路:通过遍历抽象语法树,找到类的实现部分(也就是
这里,我们需要先有几个前提:
- 如果两个OC方法之间有很多非OC方法的内容,那么我们认为它们是第二个方法的一部分。比如注释,比如一个全局变量,比如一个C函数等等。
- 第一个OC方法的开始,应该从Implementation下一行开始;如果有成员变量的声明,则从声明结尾下一行开始;如果有
@sythesizes 或者@dynamic ,那么应该从它的结尾下一行开始。
对于OC方法的注释来说,如果使用
/// 这是一个注释 或者/* * */ 的格式,那么在抽象语法树,这些注释是归于这个方法的。然而对于// 和/**/ 来说却不识别注释。因此这里干脆统一不处理注释,而认为两个方法之间的内容都属于第二个方法。
问题:如何获得抽象语法树,并且找到所有类实现中的方法?
实际上,在llvm项目中已经存在了一个类型的程序:
clang-func-mapping 同样需要接受文件路径作为参数,此时我们无法拿到文件路径。这个问题的解决方法将在下面提出。
改造clang-func-mapping
首先,打开llvm项目,添加
找到
- 修改
MapFunctionNamesConsumer 类如下,并且增加MethodInformation 类:
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 | // 用于保存查找到的OC方法的各种参数,比如方法名,开始行号,结束行号 class MethodInformation { public: std::string methodName; unsigned int startLine; unsigned int endLine; MethodInformation(StringRef name, unsigned int startLine, unsigned int endLine) : methodName(name),startLine(startLine),endLine(endLine) {} }; class MapFunctionNamesConsumer : public ASTConsumer { public: MapFunctionNamesConsumer(ASTContext &Context) : Ctx(Context) {} ~MapFunctionNamesConsumer() { std::ostringstream Result; // 最终输出我们查找到的OC方法 Result << "{"; unsigned int count = 0; for (const auto &E : informations) { std::string impName = E.getKey(); Result << """ << impName << """ << ":["; std::vector<MethodInformation> methods = E.getValue(); for (unsigned long i = 0; i < methods.size(); ++i) { MethodInformation information = methods[i]; if (i != 0) { Result << ","; } Result << "{"; Result << ""methodName":"" << information.methodName << "","; Result << ""startLine":"" << information.startLine << "","; Result << ""endLine":"" << information.endLine << """; Result << "}"; } Result << "]"; if (count < informations.size() - 1) { Result << ","; count++; } } Result << "}"; llvm::outs() << Result.str(); } virtual void HandleTranslationUnit(ASTContext &Ctx) { // 调用我们自己的处理方法 jr_handleDeclaration(Ctx.getTranslationUnitDecl()); } private: void handleDecl(const Decl *D); // 增加自己的处理方法 void jr_handleDeclaration(const Decl *D); // 处理OC实现 void jr_handleObjectImplementation(const ObjCImplementationDecl *D); ASTContext &Ctx; llvm::StringMap<std::string> Index; std::string CurrentFileName; // 增加一个字典,字典中的值是数组,数组中装的是MethodInformation对象 llvm::StringMap<std::vector<MethodInformation>> informations; // 用于记录当前的实现名称,这是因为一个文件中可能有多个类的实现 std::string impName; // 用于记录上一个函数的结尾所在的行号 unsigned int previousEndLine; }; |
实现我们自己添加的两个方法:
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 | void MapFunctionNamesConsumer::jr_handleObjectImplementation(const ObjCImplementationDecl *D) { for (const Decl *DC : D->decls()) { // 如果可以转化为OC的方法 if (const auto *FD = dyn_cast<ObjCMethodDecl>(DC)) { if (FD->isThisDeclarationADefinition()) { // 如果可以获取函数体 if (const Stmt *Body = FD->getBody()) { const SourceManager &SM = Ctx.getSourceManager(); // 如果在当前文件中 if (!SM.isInMainFile(Body->getLocStart())) { continue; } // 获取函数名称 StringRef funcName = FD->getSelector().getAsString(); // 1. 计算函数范围 FullSourceLoc startLoc = Ctx.getFullLoc(FD->Decl::getLocStart()); FullSourceLoc endLoc = Ctx.getFullLoc(FD->Decl::getLocEnd()); unsigned int startLine = startLoc.getSpellingLineNumber(); unsigned int endLine = endLoc.getSpellingLineNumber(); if (previousEndLine != 0) { startLine = previousEndLine + 1; } MethodInformation information = MethodInformation(funcName, startLine, endLine); previousEndLine = endLine; std::vector<MethodInformation> methods = informations[impName]; methods.push_back(information); informations[impName] = methods; } } } else if (const auto *FD = dyn_cast<ObjCPropertyImplDecl>(DC)) { // 获取@synthesize或者dynamic const SourceManager &SM = Ctx.getSourceManager(); if (!SM.isInMainFile(DC->getLocStart())) { continue; } SourceLocation end = DC->getLocEnd(); FullSourceLoc endLoc = Ctx.getFullLoc(end); previousEndLine = endLoc.getSpellingLineNumber(); } } } // 修改方法,找到OC的生命周期函数 void MapFunctionNamesConsumer::jr_handleDeclaration(const Decl *D) { if (!D) return; // dyn_cast操作符是一个检查转换操作。它检测操作是否是指定的类型,如果是,返回一个指向它的指针。如果操作数不是正确的类型,则返回空指针。 if (const auto *FD = dyn_cast<ObjCImplementationDecl>(D)) { // 查找oc实现 // 获取类名定义 const ObjCInterfaceDecl *i = FD->getClassInterface(); // 获取类名 impName = i->getNameAsString(); // 获取实现中的成员变量定义结尾大括号 SourceLocation r = FD->getIvarRBraceLoc(); if (r.isValid()) { // 获取大括号位置 FullSourceLoc rLoc = Ctx.getFullLoc(r); previousEndLine = rLoc.getSpellingLineNumber(); } else { SourceLocation sl = FD->getLocation(); FullSourceLoc impLoc = Ctx.getFullLoc(sl); previousEndLine = impLoc.getSpellingLineNumber(); } // 遍历实现下面的节点 jr_handleObjectImplementation(FD); } else { // 非OC实现,直接遍历子节点 SourceManager &mgr = Ctx.getSourceManager(); if (const auto *DC = dyn_cast<DeclContext>(D)) { for (const Decl *D : DC->decls()) { if(!mgr.isInMainFile(D->getLocStart())) { continue; } jr_handleDeclaration(D); } } } } |
此程序的输出形式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | { "ViewController":[ { "methodName":"viewDidLoad", "startLine":"10", "endLine":"20" }, ... ], "MyObject":[ { "methodName":"init", "startLine":"5", "endLine":"13" }, ... ] } |
因为本程序期望的目的是排序生命周期函数,因此如果在同一个实现文件内,我们忽略了类别的实现。这是因为我们认为不应该在类别中写生命周期函数;
当我们通过
现在,将该可执行文件同样拖入到我们编写的扩展中,使用
这里可以看出,实际上在
clang-format 程序中,我们同样可以将内容写入到IR.m 中,然后使用这个路径作为参数。只是因为该方法是后面才想起来的,此时对clang-format 的改造已经完成,因此也就没有采用这种方法了。
- 主项目创建NSPathControl,让用户选择
IR.m 文件路径; - 将
buffer.completeBuffer 写入到IR.m 中; - 使用NSTask执行
clang-func-mapping 程序,IR.m 路径作为参数;
新的问题
假设我们执行的
1 | Error: 'MyObject.h' file not found. |
并且,假设在
实际上第二个问题是由第一个问题引发的。
显然,这个问题是应该发生的。这是因为
解决方法
前提:
- 主工程增加用户选择
hmap 文件路径,它是一个txt文件,保存了用户头文件的路径; - 在主工程中让用户选择项目根目录,然后遍历目录,将所有头文件路径写入到
hmap 文件中,这个步骤需要递归找到所有子目录; - 主工程增加让用户选择库文件路径(比如
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks ); - 主工程增加让用户选择系统头文件路径(比如
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/include 和/usr/local/lib/clang/9.0.0/include )。
/usr/local/lib/clang/9.0.0/include 路径用于有时会报'stdarg.h' file not found 的错误。
这样,当用户选择完毕后,我们就有了这些路径的读权限。它们可以作为NSTask的参数使用,这样编译器就可以完整编译我们的程序了。
剩下的工作,就是根据返回的内容,来重新排序