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