Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(276)

Unified Diff: tools/clang/pass_to_move/add_header.py

Issue 1505823003: Add script to mass add a new #include. Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Some fixes Created 4 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « cheddar_monkey.py ('k') | tools/clang/pass_to_move/add_header_test.py » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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())
« no previous file with comments | « cheddar_monkey.py ('k') | tools/clang/pass_to_move/add_header_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698