Home | History | Annotate | Download | only in resources
      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 
      8 Example:
      9   cd /work/chrome/src
     10   tools/resources/list_unused_grit_header.py ui/strings/ui_strings.grd chrome ui
     11 """
     12 
     13 import os
     14 import sys
     15 import xml.etree.ElementTree
     16 
     17 from find_unused_resources import GetBaseResourceId
     18 
     19 IF_ELSE_TAGS = ('if', 'else')
     20 
     21 
     22 def Usage(prog_name):
     23   print prog_name, 'GRD_FILE PATHS_TO_SCAN'
     24 
     25 
     26 def 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 
     33 def 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 
     66 def 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 
     84 def 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 
    108 def 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 
    134 def 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 
    150 def 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 
    157 def 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 
    192 def 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 
    232 if __name__ == '__main__':
    233   sys.exit(main(sys.argv))
    234