#!/usr/bin/env python # Copyright 2014 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. """A tool to scan source files for unneeded grit includes. Example: cd /work/chrome/src tools/resources/list_unused_grit_header.py ui/strings/ui_strings.grd chrome ui """ import os import sys import xml.etree.ElementTree from find_unused_resources import GetBaseResourceId IF_ELSE_TAGS = ('if', 'else') def Usage(prog_name): print prog_name, 'GRD_FILE PATHS_TO_SCAN' def FilterResourceIds(resource_id): """If the resource starts with IDR_, find its base resource id.""" if resource_id.startswith('IDR_'): return GetBaseResourceId(resource_id) return resource_id def GetResourcesForNode(node, parent_file, resource_tag): """Recursively iterate through a node and extract resource names. Args: node: The node to iterate through. parent_file: The file that contains node. resource_tag: The resource tag to extract names from. Returns: A list of resource names. """ resources = [] for child in node.getchildren(): if child.tag == resource_tag: resources.append(child.attrib['name']) elif child.tag in IF_ELSE_TAGS: resources.extend(GetResourcesForNode(child, parent_file, resource_tag)) elif child.tag == 'part': parent_dir = os.path.dirname(parent_file) part_file = os.path.join(parent_dir, child.attrib['file']) part_tree = xml.etree.ElementTree.parse(part_file) part_root = part_tree.getroot() assert part_root.tag == 'grit-part' resources.extend(GetResourcesForNode(part_root, part_file, resource_tag)) else: raise Exception('unknown tag:', child.tag) # Handle the special case for resources of type "FOO_{LEFT,RIGHT,TOP}". if resource_tag == 'structure': resources = [FilterResourceIds(resource_id) for resource_id in resources] return resources def FindNodeWithTag(node, tag): """Look through a node's children for a child node with a given tag. Args: root: The node to examine. tag: The tag on a child node to look for. Returns: A child node with the given tag, or None. """ result = None for n in node.getchildren(): if n.tag == tag: assert not result result = n return result def GetResourcesForGrdFile(tree, grd_file): """Find all the message and include resources from a given grit file. Args: tree: The XML tree. grd_file: The file that contains the XML tree. Returns: A list of resource names. """ root = tree.getroot() assert root.tag == 'grit' release_node = FindNodeWithTag(root, 'release') assert release_node != None resources = set() for node_type in ('message', 'include', 'structure'): resources_node = FindNodeWithTag(release_node, node_type + 's') if resources_node != None: resources = resources.union( set(GetResourcesForNode(resources_node, grd_file, node_type))) return resources def GetOutputFileForNode(node): """Find the output file starting from a given node. Args: node: The root node to scan from. Returns: A grit header file name. """ output_file = None for child in node.getchildren(): if child.tag == 'output': if child.attrib['type'] == 'rc_header': assert output_file is None output_file = child.attrib['filename'] elif child.tag in IF_ELSE_TAGS: child_output_file = GetOutputFileForNode(child) if not child_output_file: continue assert output_file is None output_file = child_output_file else: raise Exception('unknown tag:', child.tag) return output_file def GetOutputHeaderFile(tree): """Find the output file for a given tree. Args: tree: The tree to scan. Returns: A grit header file name. """ root = tree.getroot() assert root.tag == 'grit' output_node = FindNodeWithTag(root, 'outputs') assert output_node != None return GetOutputFileForNode(output_node) def ShouldScanFile(filename): """Return if the filename has one of the extensions below.""" extensions = ['.cc', '.cpp', '.h', '.mm'] file_extension = os.path.splitext(filename)[1] return file_extension in extensions def NeedsGritInclude(grit_header, resources, filename): """Return whether a file needs a given grit header or not. Args: grit_header: The grit header file name. resources: The list of resource names in grit_header. filename: The file to scan. Returns: True if the file should include the grit header. """ # A list of special keywords that implies the file needs grit headers. # To be more thorough, one would need to run a pre-processor. SPECIAL_KEYWORDS = ( '#include "ui_localizer_table.h"', # ui_localizer.mm 'DEFINE_RESOURCE_ID', # chrome/browser/android/resource_mapper.cc ) with open(filename, 'rb') as f: grit_header_line = grit_header + '"\n' has_grit_header = False while True: line = f.readline() if not line: break if line.endswith(grit_header_line): has_grit_header = True break if not has_grit_header: return True rest_of_the_file = f.read() return (any(resource in rest_of_the_file for resource in resources) or any(keyword in rest_of_the_file for keyword in SPECIAL_KEYWORDS)) def main(argv): if len(argv) < 3: Usage(argv[0]) return 1 grd_file = argv[1] paths_to_scan = argv[2:] for f in paths_to_scan: if not os.path.exists(f): print 'Error: %s does not exist' % f return 1 tree = xml.etree.ElementTree.parse(grd_file) grit_header = GetOutputHeaderFile(tree) if not grit_header: print 'Error: %s does not generate any output headers.' % grit_header return 1 resources = GetResourcesForGrdFile(tree, grd_file) files_with_unneeded_grit_includes = [] for path_to_scan in paths_to_scan: if os.path.isdir(path_to_scan): for root, dirs, files in os.walk(path_to_scan): if '.git' in dirs: dirs.remove('.git') full_paths = [os.path.join(root, f) for f in files if ShouldScanFile(f)] files_with_unneeded_grit_includes.extend( [f for f in full_paths if not NeedsGritInclude(grit_header, resources, f)]) elif os.path.isfile(path_to_scan): if not NeedsGritInclude(grit_header, resources, path_to_scan): files_with_unneeded_grit_includes.append(path_to_scan) else: print 'Warning: Skipping %s' % path_to_scan if files_with_unneeded_grit_includes: print '\n'.join(files_with_unneeded_grit_includes) return 2 return 0 if __name__ == '__main__': sys.exit(main(sys.argv))