-
Notifications
You must be signed in to change notification settings - Fork 161
/
android_clean_app.py
215 lines (173 loc) · 8 KB
/
android_clean_app.py
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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
"""
clean_app
~~~~~~~~~~
Implements methods for removing unused android resources based on Android
Lint results.
:copyright: (c) 2014 by KeepSafe.
:license: Apache, see LICENSE for more details.
"""
import argparse
import os
import re
import subprocess
import distutils.spawn
from lxml import etree
ANDROID_MANIFEST_PATH = 'src/main/AndroidManifest.xml'
ANDROID_MANIFEST_OLD_PATH = 'AndroidManifest.xml'
class Issue:
"""
Stores a single issue reported by Android Lint
"""
def __init__(self, filepath, remove_file):
self.filepath = filepath
self.remove_file = remove_file
self.elements = []
def __str__(self):
return '{0} {1}'.format(self.filepath, self.elements)
def __repr__(self):
return '{0} {1}'.format(self.filepath, self.elements)
def add_element(self, message):
res_all = re.findall(self.pattern, message)
if res_all:
self._process_match(res_all)
else:
print("The pattern '%s' seems to find nothing in the error message '%s'. We can't find the resource and "
"can't remove it. The pattern might have changed, please check and report this in github issues." % (
self.pattern.pattern, message))
class UnusedResourceIssue(Issue):
pattern = re.compile('The resource `?([^`]+)`? appears to be unused')
def _process_match(self, match_result):
bits = match_result[0].split('.')[-2:]
self.elements.append((bits[0], bits[1]))
class ExtraTranslationIssue(Issue):
pattern = re.compile('The resource string \"`([^`]+)`\" has been marked as `translatable=\"false')
def _process_match(self, match_result):
self.elements.append(('string', match_result[0]))
def parse_args():
"""
Parse command line arguments.
"""
parser = argparse.ArgumentParser()
parser.add_argument('--lint',
help='Path to the ADT lint tool. If not specified it assumes lint tool is in your path',
default='lint')
parser.add_argument('--app',
help='Path to the Android app. If not specifies it assumes current directory is your Android '
'app directory',
default='.')
parser.add_argument('--xml',
help='Path to the lint result. If not specifies linting will be done by the script',
default=None)
parser.add_argument('--ignore-layouts',
help='Should ignore layouts',
action='store_true')
args = parser.parse_args()
return args.lint, args.app, args.xml, args.ignore_layouts
def run_lint_command():
"""
Run lint command in the shell and save results to lint-result.xml
"""
lint, app_dir, lint_result, ignore_layouts = parse_args()
if not lint_result:
if not distutils.spawn.find_executable(lint):
raise Exception(
'`%s` executable could not be found and path to lint result not specified. See --help' % lint)
lint_result = os.path.join(app_dir, 'lint-result.xml')
call_result = subprocess.call([lint, app_dir, '--xml', lint_result])
if call_result > 0:
print('Running the command failed with result %s. Try running it from the console.'
' Arguments for subprocess.call: %s' % (call_result, [lint, app_dir, '--xml', lint_result]))
else:
if not os.path.isabs(lint_result):
lint_result = os.path.join(app_dir, lint_result)
lint_result = os.path.abspath(lint_result)
return lint_result, app_dir, ignore_layouts
def get_manifest_path(app_dir):
manifest_path = os.path.abspath(os.path.join(app_dir, ANDROID_MANIFEST_PATH))
if os.path.isfile(manifest_path):
return manifest_path
else:
return os.path.abspath(os.path.join(app_dir, ANDROID_MANIFEST_OLD_PATH))
def get_manifest_string_refs(manifest_path):
pattern = re.compile('="@string/([^"]+)"')
with open(manifest_path, 'r') as f:
data = f.read()
refs = set(re.findall(pattern, data))
return [x.replace('/', '.') for x in refs]
def _get_issues_from_location(issue_class, locations, message):
issues = []
for location in locations:
filepath = location.get('file')
# if the location contains line and/or column attribute not the entire resource is unused.
# that's a guess ;)
# TODO stop guessing
remove_entire_file = (location.get('line') or location.get('column')) is None
issue = issue_class(filepath, remove_entire_file)
issue.add_element(message)
issues.append(issue)
return issues
def parse_lint_result(lint_result_path, manifest_path):
"""
Parse lint-result.xml and create Issue for every problem found except unused strings referenced in AndroidManifest
"""
unused_string_pattern = re.compile('The resource `R\.string\.([^`]+)` appears to be unused')
mainfest_string_refs = get_manifest_string_refs(manifest_path)
root = etree.parse(lint_result_path).getroot()
issues = []
for issue_xml in root.findall('.//issue[@id="UnusedResources"]'):
message = issue_xml.get('message')
unused_string = re.match(unused_string_pattern, issue_xml.get('message'))
has_string_in_manifest = unused_string and unused_string.group(1) in mainfest_string_refs
if not has_string_in_manifest:
issues.extend(_get_issues_from_location(UnusedResourceIssue,
issue_xml.findall('location'),
message))
for issue_xml in root.findall('.//issue[@id="ExtraTranslation"]'):
message = issue_xml.get('message')
if re.findall(ExtraTranslationIssue.pattern, message):
issues.extend(_get_issues_from_location(ExtraTranslationIssue,
issue_xml.findall('location'),
message))
return issues
def remove_resource_file(issue, filepath, ignore_layouts):
"""
Delete a file from the filesystem
"""
if os.path.exists(filepath) and (ignore_layouts is False or issue.elements[0][0] != 'layout'):
print('removing resource: {0}'.format(filepath))
os.remove(os.path.abspath(filepath))
def remove_resource_value(issue, filepath):
"""
Read an xml file and remove an element which is unused, then save the file back to the filesystem
"""
if os.path.exists(filepath):
for element in issue.elements:
print('removing {0} from resource {1}'.format(element, filepath))
parser = etree.XMLParser(remove_blank_text=False, remove_comments=False,
remove_pis=False, strip_cdata=False, resolve_entities=False)
tree = etree.parse(filepath, parser)
root = tree.getroot()
for unused_value in root.findall('.//{0}[@name="{1}"]'.format(element[0], element[1])):
root.remove(unused_value)
with open(filepath, 'wb') as resource:
tree.write(resource, encoding='utf-8', xml_declaration=True)
def remove_unused_resources(issues, app_dir, ignore_layouts):
"""
Remove the file or the value inside the file depending if the whole file is unused or not.
"""
for issue in issues:
filepath = os.path.join(app_dir, issue.filepath)
if issue.remove_file:
remove_resource_file(issue, filepath, ignore_layouts)
else:
remove_resource_value(issue, filepath)
def main():
lint_result_path, app_dir, ignore_layouts = run_lint_command()
if os.path.exists(lint_result_path):
manifest_path = get_manifest_path(app_dir)
issues = parse_lint_result(lint_result_path, manifest_path)
remove_unused_resources(issues, app_dir, ignore_layouts)
else:
print('the file with lint results could not be found: %s' % lint_result_path)
if __name__ == '__main__':
main()