find_unused_resources.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. #!/usr/bin/env python
  2. # Copyright (c) 2013 The Chromium Authors. All rights reserved.
  3. # Use of this source code is governed by a BSD-style license that can be
  4. # found in the LICENSE file.
  5. """Lists unused Java strings and other resources."""
  6. from __future__ import print_function
  7. import optparse
  8. import re
  9. import subprocess
  10. import sys
  11. def GetLibraryResources(r_txt_paths):
  12. """Returns the resources packaged in a list of libraries.
  13. Args:
  14. r_txt_paths: paths to each library's generated R.txt file which lists the
  15. resources it contains.
  16. Returns:
  17. The resources in the libraries as a list of tuples (type, name). Example:
  18. [('drawable', 'arrow'), ('layout', 'month_picker'), ...]
  19. """
  20. resources = []
  21. for r_txt_path in r_txt_paths:
  22. with open(r_txt_path, 'r') as f:
  23. for line in f:
  24. line = line.strip()
  25. if not line:
  26. continue
  27. data_type, res_type, name, _ = line.split(None, 3)
  28. assert data_type in ('int', 'int[]')
  29. # Hide attrs, which are redundant with styleables and always appear
  30. # unused, and hide ids, which are innocuous even if unused.
  31. if res_type in ('attr', 'id'):
  32. continue
  33. resources.append((res_type, name))
  34. return resources
  35. def GetUsedResources(source_paths, resource_types):
  36. """Returns the types and names of resources used in Java or resource files.
  37. Args:
  38. source_paths: a list of files or folders collectively containing all the
  39. Java files, resource files, and the AndroidManifest.xml.
  40. resource_types: a list of resource types to look for. Example:
  41. ['string', 'drawable']
  42. Returns:
  43. The resources referenced by the Java and resource files as a list of tuples
  44. (type, name). Example:
  45. [('drawable', 'app_icon'), ('layout', 'month_picker'), ...]
  46. """
  47. type_regex = '|'.join(map(re.escape, resource_types))
  48. patterns = [
  49. # @+drawable/id and @drawable/id
  50. r'@((\+?))(%s)/(\w+)' % type_regex,
  51. # package.R.style.id
  52. r'\b((\w+\.)*)R\.(%s)\.(\w+)' % type_regex,
  53. # <style name="child" parent="id">
  54. r'<(())(%s).*parent="(\w+)">' % type_regex,
  55. ]
  56. resources = []
  57. for pattern in patterns:
  58. p = subprocess.Popen(
  59. ['grep', '-REIhoe', pattern] + source_paths,
  60. stdout=subprocess.PIPE)
  61. grep_out, grep_err = p.communicate()
  62. # Check stderr instead of return code, since return code is 1 when no
  63. # matches are found.
  64. assert not grep_err, 'grep failed'
  65. matches = re.finditer(pattern, grep_out)
  66. for match in matches:
  67. package = match.group(1)
  68. if package == 'android.':
  69. continue
  70. type_ = match.group(3)
  71. name = match.group(4)
  72. resources.append((type_, name))
  73. return resources
  74. def FormatResources(resources):
  75. """Formats a list of resources for printing.
  76. Args:
  77. resources: a list of resources, given as (type, name) tuples.
  78. """
  79. return '\n'.join(['%-12s %s' % (t, n) for t, n in sorted(resources)])
  80. def ParseArgs(args):
  81. parser = optparse.OptionParser()
  82. parser.add_option('-v', help='Show verbose output', action='store_true')
  83. parser.add_option('-s', '--source-path', help='Specify a source folder path '
  84. '(e.g. ui/android/java)', action='append', default=[])
  85. parser.add_option('-r', '--r-txt-path', help='Specify a "first-party" R.txt '
  86. 'file (e.g. out/Debug/content_shell_apk/R.txt)',
  87. action='append', default=[])
  88. parser.add_option('-t', '--third-party-r-txt-path', help='Specify an R.txt '
  89. 'file for a third party library', action='append',
  90. default=[])
  91. options, args = parser.parse_args(args=args)
  92. if args:
  93. parser.error('positional arguments not allowed')
  94. if not options.source_path:
  95. parser.error('at least one source folder path must be specified with -s')
  96. if not options.r_txt_path:
  97. parser.error('at least one R.txt path must be specified with -r')
  98. return (options.v, options.source_path, options.r_txt_path,
  99. options.third_party_r_txt_path)
  100. def main(args=None):
  101. verbose, source_paths, r_txt_paths, third_party_r_txt_paths = ParseArgs(args)
  102. defined_resources = (set(GetLibraryResources(r_txt_paths)) -
  103. set(GetLibraryResources(third_party_r_txt_paths)))
  104. resource_types = list(set([r[0] for r in defined_resources]))
  105. used_resources = set(GetUsedResources(source_paths, resource_types))
  106. unused_resources = defined_resources - used_resources
  107. undefined_resources = used_resources - defined_resources
  108. # aapt dump fails silently. Notify the user if things look wrong.
  109. if not defined_resources:
  110. print(
  111. 'Warning: No resources found. Did you provide the correct R.txt paths?',
  112. file=sys.stderr)
  113. if not used_resources:
  114. print(
  115. 'Warning: No resources referenced from Java or resource files. Did you '
  116. 'provide the correct source paths?',
  117. file=sys.stderr)
  118. if undefined_resources:
  119. print(
  120. 'Warning: found %d "undefined" resources that are referenced by Java '
  121. 'files or by other resources, but are not defined anywhere. Run with '
  122. '-v to see them.' % len(undefined_resources),
  123. file=sys.stderr)
  124. if verbose:
  125. print('%d undefined resources:' % len(undefined_resources))
  126. print(FormatResources(undefined_resources), '\n')
  127. print('%d resources defined:' % len(defined_resources))
  128. print(FormatResources(defined_resources), '\n')
  129. print('%d used resources:' % len(used_resources))
  130. print(FormatResources(used_resources), '\n')
  131. print('%d unused resources:' % len(unused_resources))
  132. print(FormatResources(unused_resources))
  133. if __name__ == '__main__':
  134. main()