0%

Python输出iOS工程中未使用的宏定义和变量声明

背景

随着业务的迭代,iOS工程中定义的埋点数量越来越多,新旧交替,但老埋点却一直没清理过,导致埋点定义的头文件代码数达到一万多行,而通过人工方式去一个个全局搜索移除不太现实,基于此,想到通过python书写脚本的方式把未使用的宏定义和变量声明查找出来并自动删除。

实现

环境:python 2.7.4
核心思想是:

  1. 分析源码,使用正则表达式把指定源文件中的宏和变量声明抠出来,代码中define_regex是查找宏定义,static_regex是查找变量声明;
  2. 枚举工程目录中指定后缀的源文件,为了提高效率加入了忽略目录选项;
  3. AdvancedJSONEncoder是为了能把Set转换成json格式,python 2.7.x版本默认不支持;

可以按需修改正则表达式,查找不同的未使用代码并移除,也不藏私了,下面是完整的执行脚本

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
#!/usr/bin/python
# -*- coding: UTF-8 -*-

# Heller Chan
# 2018-08-15
# heller.chan@vipshop.com

"""
查找文件中未使用的宏定义和静态变量,支持样式如下:
#define DEF @"xxxx"
static NSString * const STR = @"xxxx";

Input: 包含宏定义和静态变量的源文件路径和工程根目录路径

Output: 未使用的宏定义或者静态变量列表,以json格式输出

Typical usage: $ python get_ref.py /Users/heller/Development/VSStatisticsConstants.h /Users/heller/Development/VIP -i Pods Carthage > refs.json
"""

import sys
import os
from sets import Set, ImmutableSet
import re
from os.path import basename
import argparse
import json

define_regex = re.compile("^\s*#(?:define)\s+(?P<definename>\S*)\s+(?P<definevalue>\S*)")
static_regex = re.compile("^\s*(?:static\s+NSString\s+\*\s+const)\s+(?P<definename>\S*)\s+=\s+(?P<definevalue>\S*)")
objc_extensions = ['.pch', '.h', '.hh', '.hpp', '.m', '.mm', '.c', '.cc', '.cpp']

class AdvancedJSONEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Set):
return list(obj)
if isinstance(obj, ImmutableSet):
return list(obj)
return json.JSONEncoder.default(self, obj)

def get_defines_in_file(path):
defines = Set()
for line in open(path):
# print line
results = re.search(define_regex, line)
if results:
definename = results.group('definename')
# definevalue = results.group('definevalue')
defines.add(definename)

results = re.search(static_regex, line)
if results:
definename = results.group('definename')
defines.add(definename)
return defines

def get_all_files_with_exts(files, exts):
result = list()
for ext in exts:
objc_files = (f for f in files if f.endswith(ext))
result.extend(objc_files)
return result

def find_pattern(file_path, defines, root, ignore, exts):
if root is None or root == '':
return

matchs = Set()

# 枚举所有文件
for root, dirs, files in os.walk(root):

if ignore:
for subfolder in ignore:
if subfolder in dirs:
dirs.remove(subfolder)

objc_files = get_all_files_with_exts(files, exts)

for f in objc_files:
path = os.path.join(root, f)
if path == file_path:
continue

lines = open(path).readlines()
for line in lines:
for define in defines:
if define in line:
matchs.add(define)

return Set(defines).difference(matchs)

def del_lines(path, defines):
with open(path, "r") as f:
lines = f.readlines()
with open(path, "w") as f:
for line in lines:
delete = False
for define in defines:
if define in line:
delete = True
break
if not delete:
f.write(line)
pass

def get_references(path, project_path, exts, ignore):
defines = get_defines_in_file(path)
result = find_pattern(path, defines, project_path, ignore, exts)
del_lines(path, result)

return json.dumps(result, cls=AdvancedJSONEncoder)

def main():
parser = argparse.ArgumentParser()
parser.add_argument("-i", "--ignore", nargs='*', help="需要忽略的目录名列表")
parser.add_argument("file_path", help="文件路径")
parser.add_argument("project_path", help="工程路径")
args= parser.parse_args()

result = get_references(args.file_path, args.project_path, objc_extensions, args.ignore)
print result

if __name__=='__main__':
main()