chromium/tools/resources/list_unused_grit_header.py

#!/usr/bin/env python
# Copyright 2014 The Chromium Authors
# 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
"""

from __future__ import print_function

import os
import sys
import xml.etree.ElementTree

from find_unused_resources import GetBaseResourceId

IF_ELSE_THEN_TAGS = ('if', 'else', 'then')


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_THEN_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_THEN_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
      'DECLARE_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.' % grd_file)
    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))