chromium/tools/android/find_unused_resources.py

#!/usr/bin/env python
# Copyright 2013 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Lists unused Java strings and other resources."""

from __future__ import print_function

import optparse
import re
import subprocess
import sys


def GetLibraryResources(r_txt_paths):
  """Returns the resources packaged in a list of libraries.

  Args:
    r_txt_paths: paths to each library's generated R.txt file which lists the
        resources it contains.

  Returns:
    The resources in the libraries as a list of tuples (type, name). Example:
    [('drawable', 'arrow'), ('layout', 'month_picker'), ...]
  """
  resources = []
  for r_txt_path in r_txt_paths:
    with open(r_txt_path, 'r') as f:
      for line in f:
        line = line.strip()
        if not line:
          continue
        data_type, res_type, name, _ = line.split(None, 3)
        assert data_type in ('int', 'int[]')
        # Hide attrs, which are redundant with styleables and always appear
        # unused, and hide ids, which are innocuous even if unused.
        if res_type in ('attr', 'id'):
          continue
        resources.append((res_type, name))
  return resources


def GetUsedResources(source_paths, resource_types):
  """Returns the types and names of resources used in Java or resource files.

  Args:
    source_paths: a list of files or folders collectively containing all the
        Java files, resource files, and the AndroidManifest.xml.
    resource_types: a list of resource types to look for.  Example:
        ['string', 'drawable']

  Returns:
    The resources referenced by the Java and resource files as a list of tuples
    (type, name).  Example:
    [('drawable', 'app_icon'), ('layout', 'month_picker'), ...]
  """
  type_regex = '|'.join(map(re.escape, resource_types))
  patterns = [
      # @+drawable/id and @drawable/id
      r'@((\+?))(%s)/(\w+)' % type_regex,
      # package.R.style.id
      r'\b((\w+\.)*)R\.(%s)\.(\w+)' % type_regex,
      # <style name="child" parent="id">
      r'<(())(%s).*parent="(\w+)">' % type_regex,
  ]
  resources = []
  for pattern in patterns:
    p = subprocess.Popen(
        ['grep', '-REIhoe', pattern] + source_paths,
        stdout=subprocess.PIPE)
    grep_out, grep_err = p.communicate()
    # Check stderr instead of return code, since return code is 1 when no
    # matches are found.
    assert not grep_err, 'grep failed'
    matches = re.finditer(pattern, grep_out)
    for match in matches:
      package = match.group(1)
      if package == 'android.':
        continue
      type_ = match.group(3)
      name = match.group(4)
      resources.append((type_, name))
  return resources


def FormatResources(resources):
  """Formats a list of resources for printing.

  Args:
    resources: a list of resources, given as (type, name) tuples.
  """
  return '\n'.join(['%-12s %s' % (t, n) for t, n in sorted(resources)])


def ParseArgs(args):
  parser = optparse.OptionParser()
  parser.add_option('-v', help='Show verbose output', action='store_true')
  parser.add_option('-s', '--source-path', help='Specify a source folder path '
                    '(e.g. ui/android/java)', action='append', default=[])
  parser.add_option('-r', '--r-txt-path', help='Specify a "first-party" R.txt '
                    'file (e.g. out/Debug/content_shell_apk/R.txt)',
                    action='append', default=[])
  parser.add_option('-t', '--third-party-r-txt-path', help='Specify an R.txt '
                    'file for a third party library', action='append',
                    default=[])
  options, args = parser.parse_args(args=args)
  if args:
    parser.error('positional arguments not allowed')
  if not options.source_path:
    parser.error('at least one source folder path must be specified with -s')
  if not options.r_txt_path:
    parser.error('at least one R.txt path must be specified with -r')
  return (options.v, options.source_path, options.r_txt_path,
          options.third_party_r_txt_path)


def main(args=None):
  verbose, source_paths, r_txt_paths, third_party_r_txt_paths = ParseArgs(args)
  defined_resources = (set(GetLibraryResources(r_txt_paths)) -
                       set(GetLibraryResources(third_party_r_txt_paths)))
  resource_types = list(set([r[0] for r in defined_resources]))
  used_resources = set(GetUsedResources(source_paths, resource_types))
  unused_resources = defined_resources - used_resources
  undefined_resources = used_resources - defined_resources

  # aapt dump fails silently. Notify the user if things look wrong.
  if not defined_resources:
    print(
        'Warning: No resources found. Did you provide the correct R.txt paths?',
        file=sys.stderr)
  if not used_resources:
    print(
        'Warning: No resources referenced from Java or resource files. Did you '
        'provide the correct source paths?',
        file=sys.stderr)
  if undefined_resources:
    print(
        'Warning: found %d "undefined" resources that are referenced by Java '
        'files or by other resources, but are not defined anywhere. Run with '
        '-v to see them.' % len(undefined_resources),
        file=sys.stderr)

  if verbose:
    print('%d undefined resources:' % len(undefined_resources))
    print(FormatResources(undefined_resources), '\n')
    print('%d resources defined:' % len(defined_resources))
    print(FormatResources(defined_resources), '\n')
    print('%d used resources:' % len(used_resources))
    print(FormatResources(used_resources), '\n')
    print('%d unused resources:' % len(unused_resources))
  print(FormatResources(unused_resources))


if __name__ == '__main__':
  main()