| Index: tools/clang/pass_to_move/add_header.py
|
| diff --git a/tools/clang/pass_to_move/add_header.py b/tools/clang/pass_to_move/add_header.py
|
| new file mode 100755
|
| index 0000000000000000000000000000000000000000..08f75c5ebb4508570e951f9669452e25c99ee17e
|
| --- /dev/null
|
| +++ b/tools/clang/pass_to_move/add_header.py
|
| @@ -0,0 +1,318 @@
|
| +#!/usr/bin/env python
|
| +# Copyright 2015 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.
|
| +
|
| +import argparse
|
| +import collections
|
| +import difflib
|
| +import os.path
|
| +import re
|
| +import sys
|
| +
|
| +_HEADER_TYPE_C_SYSTEM = 0
|
| +_HEADER_TYPE_CXX_SYSTEM = 1
|
| +_HEADER_TYPE_USER = 2
|
| +_HEADER_TYPE_INVALID = -1
|
| +
|
| +
|
| +def ClassifyHeader(decorated_name):
|
| + if IsCSystemHeader(decorated_name):
|
| + return _HEADER_TYPE_C_SYSTEM
|
| + elif IsCXXSystemHeader(decorated_name):
|
| + return _HEADER_TYPE_CXX_SYSTEM
|
| + elif IsUserHeader(decorated_name):
|
| + return _HEADER_TYPE_USER
|
| + else:
|
| + return _HEADER_TYPE_INVALID
|
| +
|
| +
|
| +def UndecoratedName(decorated_name):
|
| + return decorated_name[1:-1]
|
| +
|
| +
|
| +def IsSystemHeader(decorated_name):
|
| + return decorated_name[0] == '<' and decorated_name[-1] == '>'
|
| +
|
| +
|
| +def IsCSystemHeader(decorated_name):
|
| + return IsSystemHeader(decorated_name) and UndecoratedName(
|
| + decorated_name).endswith('.h')
|
| +
|
| +
|
| +def IsCXXSystemHeader(decorated_name):
|
| + return IsSystemHeader(decorated_name) and not UndecoratedName(
|
| + decorated_name).endswith('.h')
|
| +
|
| +
|
| +def IsUserHeader(decorated_name):
|
| + return decorated_name[0] == '"' and decorated_name[-1] == '"'
|
| +
|
| +
|
| +_EMPTY_LINE_RE = re.compile(r'\s*$')
|
| +_COMMENT_RE = re.compile(r'\s*//(.*)$')
|
| +_INCLUDE_RE = re.compile(
|
| + r'\s*#(import|include)\s+([<"].+?[">])\s*?(?://(.*))?$')
|
| +
|
| +
|
| +def FindIncludes(lines):
|
| + """Finds the block of #includes, assuming Google+Chrome C++ style source.
|
| +
|
| + Returns:
|
| + begin, end: The begin and end indices of the #include block, respectively.
|
| + If no #include block is found, the returned indices will be negative.
|
| + """
|
| + begin = end = -1
|
| + for idx, line in enumerate(lines):
|
| + # TODO(dcheng): #define and #undef should probably also be allowed.
|
| + if _EMPTY_LINE_RE.match(line) or _COMMENT_RE.match(line):
|
| + continue
|
| + m = _INCLUDE_RE.match(line)
|
| + if not m:
|
| + if begin < 0:
|
| + # No match, but no #includes have been seen yet. Keep scanning for the
|
| + # first #include.
|
| + continue
|
| + break
|
| +
|
| + if begin < 0:
|
| + begin = idx
|
| + end = idx + 1
|
| + return begin, end
|
| +
|
| +
|
| +class Include(object):
|
| + """Represents an #include and any interesting things associated with it."""
|
| +
|
| + def __init__(self, decorated_name, directive, preamble, inline_comment):
|
| + self.decorated_name = decorated_name
|
| + self.directive = directive
|
| + self.preamble = preamble
|
| + self.inline_comment = inline_comment
|
| + self.header_type = ClassifyHeader(decorated_name)
|
| + assert self.header_type != _HEADER_TYPE_INVALID
|
| + self.is_primary_header = False
|
| +
|
| + def __repr__(self):
|
| + return str((self.decorated_name, self.directive, self.preamble,
|
| + self.inline_comment, self.header_type, self.is_primary_header))
|
| +
|
| + def ShouldInsertNewline(self, previous_include):
|
| + return (self.is_primary_header != previous_include.is_primary_header or
|
| + self.header_type != previous_include.header_type)
|
| +
|
| + def ToSource(self):
|
| + source = []
|
| + source.extend(self.preamble)
|
| + include_line = '#%s %s' % (self.directive, self.decorated_name)
|
| + if self.inline_comment:
|
| + include_line = include_line + ' //' + self.inline_comment
|
| + source.append(include_line)
|
| + return [line.rstrip() for line in source]
|
| +
|
| +
|
| +def ParseIncludes(lines):
|
| + """Parses lines into a list of Include objects. Returns None on failure.
|
| +
|
| + Args:
|
| + lines: A list of strings representing C++ source code.
|
| + """
|
| + includes = []
|
| + preamble = []
|
| + for line in lines:
|
| + if _EMPTY_LINE_RE.match(line):
|
| + if preamble:
|
| + # preamble contents are flushed when an #include directive is matched.
|
| + # If preamble is non-empty, that means there is a preamble separated
|
| + # from its #include directive by at least one newline. Just give up,
|
| + # since the sorter has no idea how to preserve structure in this case.
|
| + return
|
| + continue
|
| + m = _INCLUDE_RE.match(line)
|
| + if not m:
|
| + preamble.append(line)
|
| + continue
|
| + includes.append(Include(m.group(2), m.group(1), preamble, m.group(3)))
|
| + preamble = []
|
| + if preamble:
|
| + return
|
| + return includes
|
| +
|
| +
|
| +def _DecomposePath(filename):
|
| + """Decomposes a filename into a list of directories and the basename."""
|
| + dirs = []
|
| + dirname, basename = os.path.split(filename)
|
| + while dirname:
|
| + dirname, last = os.path.split(dirname)
|
| + dirs.append(last)
|
| + dirs.reverse()
|
| + # Remove the extension from the basename.
|
| + basename = os.path.splitext(basename)[0]
|
| + return dirs, basename
|
| +
|
| +
|
| +def MarkPrimaryInclude(includes, filename):
|
| + """Finds the primary header in includes and marks it as such.
|
| +
|
| + Per the style guide, if moo.cc's main purpose is to implement or test the
|
| + functionality in moo.h, moo.h should be ordered first in the includes.
|
| +
|
| + Args:
|
| + includes: A list of Include objects.
|
| + filename: The filename to use as the basis for finding the primary header.
|
| + """
|
| + # Header files never have a primary include.
|
| + if filename.endswith('.h'):
|
| + return
|
| +
|
| + basis = _DecomposePath(filename)
|
| + PLATFORM_SUFFIX = \
|
| + r'(?:_(?:android|aura|chromeos|ios|linux|mac|ozone|posix|win|x11))?'
|
| + TEST_SUFFIX = \
|
| + r'(?:_(?:browser|interactive_ui|ui|unit)?test)?'
|
| +
|
| + # The list of includes is searched in reverse order of length. Even though
|
| + # matching is fuzzy, moo_posix.h should take precedence over moo.h when
|
| + # considering moo_posix.cc.
|
| + includes.sort(key=lambda i: -len(i.decorated_name))
|
| + for include in includes:
|
| + if include.header_type != _HEADER_TYPE_USER:
|
| + continue
|
| + to_test = _DecomposePath(UndecoratedName(include.decorated_name))
|
| +
|
| + # If the basename to test is longer than the basis, just skip it and
|
| + # continue. moo.c should never match against moo_posix.h.
|
| + if len(to_test[1]) > len(basis[1]):
|
| + continue
|
| +
|
| + # The basename in the two paths being compared need to fuzzily match.
|
| + # This allows for situations where moo_posix.cc implements the interfaces
|
| + # defined in moo.h.
|
| + escaped_basename = re.escape(to_test[1])
|
| + if not (re.match(escaped_basename + PLATFORM_SUFFIX + TEST_SUFFIX + '$',
|
| + basis[1]) or
|
| + re.match(escaped_basename + TEST_SUFFIX + PLATFORM_SUFFIX + '$',
|
| + basis[1])):
|
| + continue
|
| +
|
| + # The topmost directory name must match, and the rest of the directory path
|
| + # should be 'substantially similar'.
|
| + s = difflib.SequenceMatcher(None, to_test[0], basis[0])
|
| + first_matched = False
|
| + total_matched = 0
|
| + for match in s.get_matching_blocks():
|
| + if total_matched == 0 and match.a == 0 and match.b == 0:
|
| + first_matched = True
|
| + total_matched += match.size
|
| +
|
| + if not first_matched:
|
| + continue
|
| +
|
| + # 'Substantially similar' is defined to be:
|
| + # - no more than two differences
|
| + # - at least one match besides the topmost directory
|
| + total_differences = abs(total_matched - len(to_test[0])) + abs(
|
| + total_matched - len(basis[0]))
|
| + # Note: total_differences != 0 is mainly intended to allow more succint
|
| + # tests (otherwise tests with just a basename would always trip the
|
| + # total_matched < 2 check).
|
| + if total_differences != 0 and (total_differences > 2 or total_matched < 2):
|
| + continue
|
| +
|
| + include.is_primary_header = True
|
| + return
|
| +
|
| +
|
| +def SerializeIncludes(includes):
|
| + """Turns includes back into the corresponding C++ source code.
|
| +
|
| + This function assumes that the list of input Include objects is already sorted
|
| + according to Google style.
|
| +
|
| + Args:
|
| + includes: a list of Include objects.
|
| +
|
| + Returns:
|
| + A list of strings representing C++ source code.
|
| + """
|
| + source = []
|
| +
|
| + # Assume there's always at least one include.
|
| + previous_include = None
|
| + for include in includes:
|
| + if previous_include and include.ShouldInsertNewline(previous_include):
|
| + source.append('')
|
| + source.extend(include.ToSource())
|
| + previous_include = include
|
| + return source
|
| +
|
| +
|
| +def InsertHeaderIntoSource(filename, source, decorated_name):
|
| + """Inserts the specified header into some source text, if needed.
|
| +
|
| + Args:
|
| + filename: The name of the source file.
|
| + source: A string containing the contents of the source file.
|
| + decorated_name: The decorated name of the header to insert.
|
| +
|
| + Returns:
|
| + None on failure or the modified source text on success.
|
| + """
|
| + lines = source.splitlines()
|
| + begin, end = FindIncludes(lines)
|
| +
|
| + # No #includes in this file. Just give up.
|
| + # TODO(dcheng): Be more clever and insert it after the file-level comment or
|
| + # include guard as appropriate.
|
| + if begin < 0:
|
| + return
|
| +
|
| + includes = ParseIncludes(lines[begin:end])
|
| + if not includes:
|
| + return
|
| + if decorated_name in [i.decorated_name for i in includes]:
|
| + # Nothing to do.
|
| + return source
|
| + MarkPrimaryInclude(includes, filename)
|
| + includes.append(Include(decorated_name, 'include', [], None))
|
| +
|
| + def SortKey(include):
|
| + return (not include.is_primary_header, include.header_type,
|
| + include.decorated_name)
|
| +
|
| + includes.sort(key=SortKey)
|
| + lines[begin:end] = SerializeIncludes(includes)
|
| + lines.append('') # To avoid eating the newline at the end of the file.
|
| + return '\n'.join(lines)
|
| +
|
| +
|
| +def main():
|
| + parser = argparse.ArgumentParser(
|
| + description='Mass insert a new header into a bunch of files.')
|
| + parser.add_argument(
|
| + '--header',
|
| + help='The decorated filename of the header to insert (e.g. "a" or <a>)',
|
| + required=True)
|
| + parser.add_argument('files', nargs='+')
|
| + args = parser.parse_args()
|
| + if ClassifyHeader(args.header) == _HEADER_TYPE_INVALID:
|
| + print '--header argument must be a decorated filename, e.g.'
|
| + print ' --header "<utility>"'
|
| + print 'or'
|
| + print ' --header \'"moo.h"\''
|
| + return 1
|
| + print 'Inserting #include %s...' % args.header
|
| + for filename in args.files:
|
| + with file(filename, 'r') as f:
|
| + new_source = InsertHeaderIntoSource(
|
| + os.path.normpath(filename), f.read(), args.header)
|
| + if not new_source:
|
| + print 'Failed to process file: %s' % filename
|
| + continue
|
| + with file(filename, 'w') as f:
|
| + f.write(new_source)
|
| +
|
| +
|
| +if __name__ == '__main__':
|
| + sys.exit(main())
|
|
|