clang-format和Xcode Extension使用

本文主要是介绍如何使用clang-format格式化OC代码,并且结合Xcode Extension做成热键。

要实现的功能点:

  1. clang-format格式化当前xcode停留的.m文件内容
  2. 一个代码排序功能(生命周期函数位于最顶层)

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项目,选择Manually Manage Schemes

然后选择Product->Scheme->New Scheme...增加新的Scheme,这里我们Target选择clang-format即可。

截屏2020-08-26 下午5.21.42.png

然后编译,生成可执行文件clang-format即可使用。

clang-format使用:

终端输入clang-format -help可以查看可供选择的选项,我们这里简单使用下。

首先,准备一个叫做.clang-format或者_clang-format的文件,文件内容为IndentWidth: 4。保存到需要格式化的文件的当前目录或者上级目录。

然后,终端进入clang-format可执行文件所在的文件夹,输入./clang-format xxxxx.m即可看到格式化后的内容:

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

invocation对象有属性@property (readonly, strong) XCSourceTextBuffer *buffer;
该属性中最重要的属性是@property (readonly, strong) NSMutableArray *lines;。我们从数组中删除或者添加都会影响最后的结果。

如此,我们有了基本思路:通过Xcode Extension获取当前文件的内容,然后使用clang-format对内容进行格式化,最后再替换数组中的内容即可。


开始工作:
创建Xcode Extension项目,然后将clang-format可执行程序拖到项目中,我们通过NSTask来执行可执行文件。如下:

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接受文件路径为参数,然而我们只能拿到文件内容。这怎么处理?

改造clang-format程序

在llvm的Xcode项目中,找到ClangFormat.cpp文件,我们关注main函数以下内容:

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);
  ...

获取代码格式FormatStyle的函数为:

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程序使用文件路径,不仅仅获取文件中的内容,还会根据路径查找.clang-format文件,并且会根据文件后缀名来判断编程语言类型。

如此,我们需要改造clang-format程序,增加三个参数:

  • 源码(用于格式化)
  • 语言类型(用于使用指定语言类型的格式化)
  • .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)函数。

修改头文件/Users/58liqiang/llvm/llvm/tools/clang/include/clang/Format/Format.h,增加如下方法:

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
                                                 );

Format.cpp中实现新函数(可通过cmd+Mouse Left在代码中找到该文件):

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;
}

这段代码仅仅是在原来的实现上,修改了根据文件名获取语言类型,和根据文件名查找.clang-format文件的部分。

改造3,修改format函数

ClangFormat.cpp中,增加static bool jr_format(StringRef content, StringRef languageKind, StringRef formatPath)函数:

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;
}

如此,我们可以通过-s传入源代码,通过-l传入语言,通过-p传入.clang-format文件路径,即可达到格式化代码的目的。


新的问题

现在,重新编译clang-format程序,将它拖入到Xcode Extension项目中,我们设置NSTask的参数如下:

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开发中,不能直接读取沙盒之外的文件,必须经由用户选择才可以。

为了解决这个问题,我们需要在Xcode Extension的宿主程序中做一个简单的界面,由用户来选择相应的文件,这样我们在扩展中才能有读取和修改文件的权限。

解决方法(具体UI代码不再赘述):

  • 使用NSPathControl让用户选择文件或者文件夹
  • 将URL通过- (nullable NSData *)bookmarkDataWithOptions:(NSURLBookmarkCreationOptions)options includingResourceValuesForKeys:(nullable NSArray *)keys relativeToURL:(nullable NSURL *)relativeURL error:(NSError **)error方法转化为NSData,保存在NSUserDefaults中。如此扩展中才有权限可以操作。
  • 关闭主程序
  • 扩展中增加从NSUserDefaults中取URL的操作,取出的URL如果是文件,则是可读可写的。如果是文件夹,则自行拼接具体文件路径,此时文件也是可读可写的。

如此一来,我们就解决了.clang-format文件的读权限问题了。

大致代码如下:

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:&regularStale
                  error:&urlError];
...
    task.arguments = @[
        @"-s", // sourceCode
        code,
        @"-l", // language
        [self language:invocation.buffer],
        @"-p", // formatPath
        [clangFormatURL relativePath]
    ];


代码排序功能实现

基本思路:通过遍历抽象语法树,找到类的实现部分(也就是Implementation),找到实现中的所有OC方法,根据指定的顺序(这里假设规定了生命周期函数的在顶部,其他方法在底部),重新排列方法。将排列后的结果替换buffer.lines中的内容即可。

这里,我们需要先有几个前提:

  1. 如果两个OC方法之间有很多非OC方法的内容,那么我们认为它们是第二个方法的一部分。比如注释,比如一个全局变量,比如一个C函数等等。
  2. 第一个OC方法的开始,应该从Implementation下一行开始;如果有成员变量的声明,则从声明结尾下一行开始;如果有@sythesizes或者@dynamic,那么应该从它的结尾下一行开始。

对于OC方法的注释来说,如果使用/// 这是一个注释 或者/* * */的格式,那么在抽象语法树,这些注释是归于这个方法的。然而对于///**/来说却不识别注释。因此这里干脆统一不处理注释,而认为两个方法之间的内容都属于第二个方法。

问题:如何获得抽象语法树,并且找到所有类实现中的方法?

实际上,在llvm项目中已经存在了一个类型的程序:clang-func-mapping。该程序的作用的遍历语法树,找出所有C或者C++函数。我们使用它,需要一定的改造。

clang-func-mapping同样需要接受文件路径作为参数,此时我们无法拿到文件路径。这个问题的解决方法将在下面提出。

改造clang-func-mapping

首先,打开llvm项目,添加clang-func-mapping的target。

找到ClangFnMapGen.cpp文件,其中void MapFunctionNamesConsumer::handleDecl(const Decl *D)函数是处理C和C++方法的。我们仿照它来找OC的方法。

  1. 修改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"
                    },
                    ...
             ]
}

因为本程序期望的目的是排序生命周期函数,因此如果在同一个实现文件内,我们忽略了类别的实现。这是因为我们认为不应该在类别中写生命周期函数;

当我们通过NSTask获取到返回后的值时,即可以根据信息对原来的buffer.lines进行重排。

现在,将该可执行文件同样拖入到我们编写的扩展中,使用NSTask来运行的时候,我们需要传递文件路径,我的解决方法是:提前生成一个空的.m文件,我称为为IR.m。它将保存当前buffer.completeBuffer的内容,然后我们使用对IR.m进行语法树的遍历即可。

这里可以看出,实际上在clang-format程序中,我们同样可以将内容写入到IR.m中,然后使用这个路径作为参数。只是因为该方法是后面才想起来的,此时对clang-format的改造已经完成,因此也就没有采用这种方法了。

  • 主项目创建NSPathControl,让用户选择IR.m文件路径;
  • buffer.completeBuffer写入到IR.m中;
  • 使用NSTask执行clang-func-mapping程序,IR.m路径作为参数;

新的问题

假设我们执行的IR.m中的内容为MyObject的实现,它引用了MyObject.h,并且继承自NSObject。那么运行后会报错:

1
Error: 'MyObject.h' file not found.

并且,假设在@implementation MyObject下面有@sythesizes _hash = hash; 我们发现第一个函数的起始行应该从@sythesizes _hash = hash;的下一行开始,然而返回的结果表明程序并没有识别出@sythesizes _hash = hash;。(假如@sythesizes在第10行,则第一个方法应该从第11行开始,然而结果为10)

实际上第二个问题是由第一个问题引发的。

显然,这个问题是应该发生的。这是因为clang-func-mapping在编译IR.m的时候,是无法找到MyObject.h文件的(同级目录下,没有该文件)。并且往深了一步想,它应该也无法找到Foundation库。

解决方法
前提:-iframework:指定库的搜索路径;-isystem:指定系统头文件的搜索路径;-I:指定用户头文件的搜索路径。

  1. 主工程增加用户选择hmap文件路径,它是一个txt文件,保存了用户头文件的路径;
  2. 在主工程中让用户选择项目根目录,然后遍历目录,将所有头文件路径写入到hmap文件中,这个步骤需要递归找到所有子目录;
  3. 主工程增加让用户选择库文件路径(比如/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks);
  4. 主工程增加让用户选择系统头文件路径(比如/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的参数使用,这样编译器就可以完整编译我们的程序了。

剩下的工作,就是根据返回的内容,来重新排序buffer.lines了,这一部分就是纯逻辑性的代码了,不再过多介绍了。