diff --git a/README.md b/README.md index a8d110e..9bef630 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/android_clean_app.py b/android_clean_app.py index 7d0f5fd..fca4020 100644 --- a/android_clean_app.py +++ b/android_clean_app.py @@ -17,7 +17,6 @@ class Issue: - """ Stores a single issue reported by Android Lint """ @@ -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)) @@ -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): @@ -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__': diff --git a/test/test_clean_app.py b/test/test_clean_app.py index 8e1cfc4..9fef94a 100644 --- a/test/test_clean_app.py +++ b/test/test_clean_app.py @@ -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) @@ -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'))) @@ -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'))) @@ -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): @@ -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, """ + + string1 + missing + drawable2 + layout + + """.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, """ + + large + missing + drawable2 + layout + + """.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()