1#!/usr/bin/env python
2# Copyright 2014 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"""A tool to scan source files for unneeded grit includes.
7
8Example:
9  cd /work/chrome/src
10  tools/resources/list_unused_grit_header.py ui/strings/ui_strings.grd chrome ui
11"""
12
13import os
14import sys
15import xml.etree.ElementTree
16
17from find_unused_resources import GetBaseResourceId
18
19IF_ELSE_TAGS = ('if', 'else')
20
21
22def Usage(prog_name):
23  print prog_name, 'GRD_FILE PATHS_TO_SCAN'
24
25
26def FilterResourceIds(resource_id):
27  """If the resource starts with IDR_, find its base resource id."""
28  if resource_id.startswith('IDR_'):
29    return GetBaseResourceId(resource_id)
30  return resource_id
31
32
33def GetResourcesForNode(node, parent_file, resource_tag):
34  """Recursively iterate through a node and extract resource names.
35
36  Args:
37    node: The node to iterate through.
38    parent_file: The file that contains node.
39    resource_tag: The resource tag to extract names from.
40
41  Returns:
42    A list of resource names.
43  """
44  resources = []
45  for child in node.getchildren():
46    if child.tag == resource_tag:
47      resources.append(child.attrib['name'])
48    elif child.tag in IF_ELSE_TAGS:
49      resources.extend(GetResourcesForNode(child, parent_file, resource_tag))
50    elif child.tag == 'part':
51      parent_dir = os.path.dirname(parent_file)
52      part_file = os.path.join(parent_dir, child.attrib['file'])
53      part_tree = xml.etree.ElementTree.parse(part_file)
54      part_root = part_tree.getroot()
55      assert part_root.tag == 'grit-part'
56      resources.extend(GetResourcesForNode(part_root, part_file, resource_tag))
57    else:
58      raise Exception('unknown tag:', child.tag)
59
60  # Handle the special case for resources of type "FOO_{LEFT,RIGHT,TOP}".
61  if resource_tag == 'structure':
62    resources = [FilterResourceIds(resource_id) for resource_id in resources]
63  return resources
64
65
66def FindNodeWithTag(node, tag):
67  """Look through a node's children for a child node with a given tag.
68
69  Args:
70    root: The node to examine.
71    tag: The tag on a child node to look for.
72
73  Returns:
74    A child node with the given tag, or None.
75  """
76  result = None
77  for n in node.getchildren():
78    if n.tag == tag:
79      assert not result
80      result = n
81  return result
82
83
84def GetResourcesForGrdFile(tree, grd_file):
85  """Find all the message and include resources from a given grit file.
86
87  Args:
88    tree: The XML tree.
89    grd_file: The file that contains the XML tree.
90
91  Returns:
92    A list of resource names.
93  """
94  root = tree.getroot()
95  assert root.tag == 'grit'
96  release_node = FindNodeWithTag(root, 'release')
97  assert release_node != None
98
99  resources = set()
100  for node_type in ('message', 'include', 'structure'):
101    resources_node = FindNodeWithTag(release_node, node_type + 's')
102    if resources_node != None:
103      resources = resources.union(
104          set(GetResourcesForNode(resources_node, grd_file, node_type)))
105  return resources
106
107
108def GetOutputFileForNode(node):
109  """Find the output file starting from a given node.
110
111  Args:
112    node: The root node to scan from.
113
114  Returns:
115    A grit header file name.
116  """
117  output_file = None
118  for child in node.getchildren():
119    if child.tag == 'output':
120      if child.attrib['type'] == 'rc_header':
121        assert output_file is None
122        output_file = child.attrib['filename']
123    elif child.tag in IF_ELSE_TAGS:
124      child_output_file = GetOutputFileForNode(child)
125      if not child_output_file:
126        continue
127      assert output_file is None
128      output_file = child_output_file
129    else:
130      raise Exception('unknown tag:', child.tag)
131  return output_file
132
133
134def GetOutputHeaderFile(tree):
135  """Find the output file for a given tree.
136
137  Args:
138    tree: The tree to scan.
139
140  Returns:
141    A grit header file name.
142  """
143  root = tree.getroot()
144  assert root.tag == 'grit'
145  output_node = FindNodeWithTag(root, 'outputs')
146  assert output_node != None
147  return GetOutputFileForNode(output_node)
148
149
150def ShouldScanFile(filename):
151  """Return if the filename has one of the extensions below."""
152  extensions = ['.cc', '.cpp', '.h', '.mm']
153  file_extension = os.path.splitext(filename)[1]
154  return file_extension in extensions
155
156
157def NeedsGritInclude(grit_header, resources, filename):
158  """Return whether a file needs a given grit header or not.
159
160  Args:
161    grit_header: The grit header file name.
162    resources: The list of resource names in grit_header.
163    filename: The file to scan.
164
165  Returns:
166    True if the file should include the grit header.
167  """
168  # A list of special keywords that implies the file needs grit headers.
169  # To be more thorough, one would need to run a pre-processor.
170  SPECIAL_KEYWORDS = (
171      '#include "ui_localizer_table.h"',  # ui_localizer.mm
172      'DEFINE_RESOURCE_ID',  # chrome/browser/android/resource_mapper.cc
173      )
174  with open(filename, 'rb') as f:
175    grit_header_line = grit_header + '"\n'
176    has_grit_header = False
177    while True:
178      line = f.readline()
179      if not line:
180        break
181      if line.endswith(grit_header_line):
182        has_grit_header = True
183        break
184
185    if not has_grit_header:
186      return True
187    rest_of_the_file = f.read()
188    return (any(resource in rest_of_the_file for resource in resources) or
189            any(keyword in rest_of_the_file for keyword in SPECIAL_KEYWORDS))
190
191
192def main(argv):
193  if len(argv) < 3:
194    Usage(argv[0])
195    return 1
196  grd_file = argv[1]
197  paths_to_scan = argv[2:]
198  for f in paths_to_scan:
199    if not os.path.exists(f):
200      print 'Error: %s does not exist' % f
201      return 1
202
203  tree = xml.etree.ElementTree.parse(grd_file)
204  grit_header = GetOutputHeaderFile(tree)
205  if not grit_header:
206    print 'Error: %s does not generate any output headers.' % grit_header
207    return 1
208  resources = GetResourcesForGrdFile(tree, grd_file)
209
210  files_with_unneeded_grit_includes = []
211  for path_to_scan in paths_to_scan:
212    if os.path.isdir(path_to_scan):
213      for root, dirs, files in os.walk(path_to_scan):
214        if '.git' in dirs:
215          dirs.remove('.git')
216        full_paths = [os.path.join(root, f) for f in files if ShouldScanFile(f)]
217        files_with_unneeded_grit_includes.extend(
218            [f for f in full_paths
219             if not NeedsGritInclude(grit_header, resources, f)])
220    elif os.path.isfile(path_to_scan):
221      if not NeedsGritInclude(grit_header, resources, path_to_scan):
222        files_with_unneeded_grit_includes.append(path_to_scan)
223    else:
224      print 'Warning: Skipping %s' % path_to_scan
225
226  if files_with_unneeded_grit_includes:
227    print '\n'.join(files_with_unneeded_grit_includes)
228    return 2
229  return 0
230
231
232if __name__ == '__main__':
233  sys.exit(main(sys.argv))
234