Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add more options to clean unused strings only, ignore drawables or ig… #29

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,18 @@ Use existing lint result. If provided lint won't be run.

Ignore layout directory

#### --ignore-drawables

Ignore drawables

#### --ignore-dimens

Ignore dimensions

#### --strings-only

Clean unused strings only

## Expected behavior
### Resource ID in code not found

Expand Down
71 changes: 45 additions & 26 deletions android_clean_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@


class Issue:

"""
Stores a single issue reported by Android Lint
"""
Expand All @@ -41,7 +40,8 @@ def add_element(self, message):
bits = res.split('.')[-2:]
self.elements.append((bits[0], bits[1]))
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." % (
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." % (
Issue.pattern, message))


Expand All @@ -63,22 +63,32 @@ def parse_args():
parser.add_argument('--ignore-layouts',
help='Should ignore layouts',
action='store_true')
parser.add_argument('--ignore-drawables',
help='Should ignore drawables',
action='store_true')
parser.add_argument('--ignore-dimens',
help='Should ignore dimensions',
action='store_true')
parser.add_argument('--strings-only',
help='Should remove unused strings only',
action='store_true')
args = parser.parse_args()
return args.lint, args.app, args.xml, args.ignore_layouts
return args.lint, args.app, args.xml, args.ignore_layouts, args.ignore_drawables, args.ignore_dimens, args.strings_only


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()
lint, app_dir, lint_result, ignore_layouts, ignore_drawables, ignore_dimens, strings_only = parse_args()
if not lint_result:
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 {}. Try running it from the console. Arguments for subprocess.call: {}'.format(
print(
'Running the command failed with result {}. Try running it from the console. Arguments for subprocess.call: {}'.format(
call_result, [lint, app_dir, '--xml', lint_result]))
return lint_result, app_dir, ignore_layouts
return lint_result, app_dir, ignore_layouts, ignore_drawables, ignore_dimens, strings_only


def parse_lint_result(lint_result_path):
Expand All @@ -99,48 +109,57 @@ def parse_lint_result(lint_result_path):
return issues


def remove_resource_file(issue, filepath, ignore_layouts):
def remove_resource_file(issue, filepath, ignore_layouts, ignore_drawables=False):
"""
Delete a file from the filesystem
:param ignore_layouts: True to ignore removing layout files, False to remove them
:param ignore_drawables: True to ignore removing drawable files, False to remove them
"""
if os.path.exists(filepath) and (ignore_layouts is False or issue.elements[0][0] != 'layout'):

if os.path.exists(filepath) and (ignore_layouts is False or issue.elements[0][0] != 'layout') and (
ignore_drawables is False or issue.elements[0][0] != 'drawable'):
print('removing resource: {0}'.format(filepath))
os.remove(os.path.abspath(filepath))


def remove_resource_value(issue, filepath):
def remove_resource_value(issue, filepath, ignore_dimens=False, ignore_drawables=False):
"""
Read an xml file and remove an element which is unused, then save the file back to the filesystem
:param ignore_dimens: True to ignore removing dimensions, False to remove them
:param ignore_drawables: True to ignore removing drawables, False to remove them
"""
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):
if ignore_dimens is False and ignore_drawables is False or (
element[0] != 'dimen' and element[0] != 'drawable'):
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, ignore_drawables=False, ignore_dimens=False,
strings_only=False):
"""
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)
if issue.remove_file and strings_only is False:
remove_resource_file(issue, filepath, ignore_layouts, ignore_drawables)
else:
remove_resource_value(issue, filepath)
remove_resource_value(issue, filepath, ignore_dimens, ignore_drawables)


def main():
lint_result_path, app_dir, ignore_layouts = run_lint_command()
lint_result_path, app_dir, ignore_layouts, ignore_drawables, ignore_dimens, strings_only = run_lint_command()
issues = parse_lint_result(lint_result_path)
remove_unused_resources(issues, app_dir, ignore_layouts)
remove_unused_resources(issues, app_dir, ignore_layouts, ignore_drawables, ignore_dimens, strings_only)


if __name__ == '__main__':
Expand Down
50 changes: 45 additions & 5 deletions test/test_clean_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def test_removes_given_resources_if_safe(self):

issue = clean_app.Issue(temp_path, True)

clean_app.remove_unused_resources([issue], os.path.dirname(temp_path), False)
clean_app.remove_unused_resources([issue], os.path.dirname(temp_path), False, False, False, False)
with self.assertRaises(IOError):
open(temp_path)

Expand All @@ -53,7 +53,7 @@ def _removes_an_unused_value_from_a_file(self, message, expected_elements_count=

issue = clean_app.Issue(temp_path, False)
issue.add_element(message)
clean_app.remove_unused_resources([issue], os.path.dirname(temp_path), True)
clean_app.remove_unused_resources([issue], os.path.dirname(temp_path), True, False, False, True)

root = ET.parse(temp_path).getroot()
self.assertEqual(expected_elements_count, len(root.findall('string')))
Expand Down Expand Up @@ -83,7 +83,7 @@ def test_ignores_layouts(self):

issue = clean_app.Issue(temp_path, False)
issue.add_element('The resource R.string.missing appears to be unused')
clean_app.remove_unused_resources([issue], os.path.dirname(temp_path), False)
clean_app.remove_unused_resources([issue], os.path.dirname(temp_path), False, False, False, False)

root = ET.parse(temp_path).getroot()
self.assertEqual(1, len(root.findall('layout')))
Expand All @@ -92,7 +92,7 @@ def test_remove_resource_file_skip_missing_files(self):
issue = MagicMock()
issue.elements = [['dummy']]
with patch('os.remove') as patch_remove:
clean_app.remove_resource_file(issue, 'dummy', False)
clean_app.remove_resource_file(issue, 'dummy', False, False)
self.assertFalse(patch_remove.called)

def test_remove_value_only_if_the_file_still_exists(self):
Expand All @@ -103,8 +103,48 @@ def test_remove_value_only_if_the_file_still_exists(self):
issue = clean_app.Issue(temp_path, False)
issue.add_element('The resource `R.drawable.drawable_missing` appears to be unused')

clean_app.remove_unused_resources([issue], os.path.dirname(temp_path), False)
clean_app.remove_unused_resources([issue], os.path.dirname(temp_path), False, False, False, False)


def test_ignores_drawables(self):
temp, temp_path = tempfile.mkstemp()
os.write(temp, """
<resources>
<string name="string1">string1</string>
<drawable name="missing">missing</drawable>
<drawable name="drawable2">drawable2</drawable>
<layout name="layout">layout</layout>
</resources>
""".encode('UTF-8'))
os.close(temp)

issue = clean_app.Issue(temp_path, False)
issue.add_element('The resource R.drawable.missing appears to be unused')
issue.add_element('The resource R.layout.layout appears to be unused')
clean_app.remove_unused_resources([issue], os.path.dirname(temp_path), False, True)

root = ET.parse(temp_path).getroot()
self.assertEqual(2, len(root.findall('drawable')))

def test_ignores_dimens(self):
temp, temp_path = tempfile.mkstemp()
os.write(temp, """
<resources>
<dimen name="large">large</dimen>
<dimen name="missing">missing</dimen>
<drawable name="drawable2">drawable2</drawable>
<layout name="layout">layout</layout>
</resources>
""".encode('UTF-8'))
os.close(temp)

issue = clean_app.Issue(temp_path, False)
issue.add_element('The resource R.dimen.missing appears to be unused')
issue.add_element('The resource R.layout.layout appears to be unused')
clean_app.remove_unused_resources([issue], os.path.dirname(temp_path), False, False, True)

root = ET.parse(temp_path).getroot()
self.assertEqual(2, len(root.findall('dimen')))

if __name__ == '__main__':
unittest.main()