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