summaryrefslogtreecommitdiffstats
path: root/tools/android/find_unused_resources.py
blob: 1e8fa48abc3ce7b29a8ccac66c07f2b324ad4ff7 (plain)
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
#!/usr/bin/python
# Copyright (c) 2013 The Chromium Authors. All rights reserved.
# 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."""

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 = [r'@(())(%s)/(\w+)' % type_regex,
              r'\b((\w+\.)*)R\.(%s)\.(\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 >> sys.stderr, (
        'Warning: No resources found. Did you provide the correct R.txt paths?')
  if not used_resources:
    print >> sys.stderr, (
        'Warning: No resources referenced from Java or resource files. Did you '
        'provide the correct source paths?')
  if undefined_resources:
    print >> sys.stderr, (
        '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))

  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()