diff --git a/docs/clang_tool_refactoring.md b/docs/clang_tool_refactoring.md index d4e9f94748c834..62ec4418d5811e 100644 --- a/docs/clang_tool_refactoring.md +++ b/docs/clang_tool_refactoring.md @@ -54,8 +54,10 @@ Other useful references when writing the tool: ### Edit serialization format ``` ==== BEGIN EDITS ==== -r:::path/to/file1:::offset1:::length1:::replacement text -r:::path/to/file2:::offset2:::length2:::replacement text +r:::path/to/file/to/edit:::offset1:::length1:::replacement text +r:::path/to/file/to/edit:::offset2:::length2:::replacement text +r:::path/to/file2/to/edit:::offset3:::length3:::replacement text +include-user-header:::path/to/file2/to/edit:::-1:::-1:::header/file/to/include.h ... @@ -64,8 +66,8 @@ r:::path/to/file2:::offset2:::length2:::replacement text The header and footer are required. Each line between the header and footer represents one edit. Fields are separated by `:::`, and the first field must -be `r` (for replacement). In the future, this may be extended to handle header -insertion/removal. A deletion is an edit with no replacement text. +be `r` (for replacement) or `include-user-header`. +A deletion is an edit with no replacement text. The edits are applied by [`apply_edits.py`](#Running), which understands certain conventions: diff --git a/tools/clang/rewrite_raw_ptr_fields/RewriteRawPtrFields.cpp b/tools/clang/rewrite_raw_ptr_fields/RewriteRawPtrFields.cpp index d5f83ab48499d1..502f8fcfd35493 100644 --- a/tools/clang/rewrite_raw_ptr_fields/RewriteRawPtrFields.cpp +++ b/tools/clang/rewrite_raw_ptr_fields/RewriteRawPtrFields.cpp @@ -41,15 +41,48 @@ #include "llvm/Support/TargetSelect.h" using namespace clang::ast_matchers; -using clang::tooling::CommonOptionsParser; -using clang::tooling::Replacement; namespace { +// Include path that needs to be added to all the files where CheckedPtr<...> +// replaces a raw pointer. +const char kIncludePath[] = "base/memory/checked_ptr.h"; + +// Output format is documented in //docs/clang_tool_refactoring.md +class ReplacementsPrinter { + public: + ReplacementsPrinter() { llvm::outs() << "==== BEGIN EDITS ====\n"; } + + ~ReplacementsPrinter() { llvm::outs() << "==== END EDITS ====\n"; } + + void PrintReplacement(const clang::SourceManager& source_manager, + const clang::SourceRange& replacement_range, + std::string replacement_text) { + clang::tooling::Replacement replacement( + source_manager, clang::CharSourceRange::getCharRange(replacement_range), + replacement_text); + llvm::StringRef file_path = replacement.getFilePath(); + std::replace(replacement_text.begin(), replacement_text.end(), '\n', '\0'); + llvm::outs() << "r:::" << file_path << ":::" << replacement.getOffset() + << ":::" << replacement.getLength() + << ":::" << replacement_text << "\n"; + + bool was_inserted = false; + std::tie(std::ignore, was_inserted) = + files_with_already_added_includes_.insert(file_path.str()); + if (was_inserted) + llvm::outs() << "include-user-header:::" << file_path + << ":::-1:::-1:::" << kIncludePath << "\n"; + } + + private: + std::set files_with_already_added_includes_; +}; + class FieldDeclRewriter : public MatchFinder::MatchCallback { public: - explicit FieldDeclRewriter(std::vector* replacements) - : replacements_(replacements) {} + explicit FieldDeclRewriter(ReplacementsPrinter* replacements_printer) + : replacements_printer_(replacements_printer) {} void run(const MatchFinder::MatchResult& result) override { const clang::SourceManager& source_manager = *result.SourceManager; @@ -88,10 +121,9 @@ class FieldDeclRewriter : public MatchFinder::MatchCallback { if (field_decl->isMutable()) replacement_text.insert(0, "mutable "); - // Generate and add a replacement. - replacements_->emplace_back( - source_manager, clang::CharSourceRange::getCharRange(replacement_range), - replacement_text); + // Generate and print a replacement. + replacements_printer_->PrintReplacement(source_manager, replacement_range, + replacement_text); } private: @@ -120,7 +152,7 @@ class FieldDeclRewriter : public MatchFinder::MatchCallback { return result; } - std::vector* const replacements_; + ReplacementsPrinter* const replacements_printer_; }; } // namespace @@ -132,12 +164,12 @@ int main(int argc, const char* argv[]) { llvm::InitializeNativeTargetAsmParser(); llvm::cl::OptionCategory category( "rewrite_raw_ptr_fields: changes |T* field_| to |CheckedPtr field_|."); - CommonOptionsParser options(argc, argv, category); + clang::tooling::CommonOptionsParser options(argc, argv, category); clang::tooling::ClangTool tool(options.getCompilations(), options.getSourcePathList()); MatchFinder match_finder; - std::vector replacements; + ReplacementsPrinter replacements_printer; // Field declarations ========= // Given @@ -146,7 +178,7 @@ int main(int argc, const char* argv[]) { // }; // matches |int* y|. auto field_decl_matcher = fieldDecl(hasType(pointerType())).bind("fieldDecl"); - FieldDeclRewriter field_decl_rewriter(&replacements); + FieldDeclRewriter field_decl_rewriter(&replacements_printer); match_finder.addMatcher(field_decl_matcher, &field_decl_rewriter); // Prepare and run the tool. @@ -156,15 +188,5 @@ int main(int argc, const char* argv[]) { if (result != 0) return result; - // Serialization format is documented in tools/clang/scripts/run_tool.py - llvm::outs() << "==== BEGIN EDITS ====\n"; - for (const auto& r : replacements) { - std::string replacement_text = r.getReplacementText().str(); - std::replace(replacement_text.begin(), replacement_text.end(), '\n', '\0'); - llvm::outs() << "r:::" << r.getFilePath() << ":::" << r.getOffset() - << ":::" << r.getLength() << ":::" << replacement_text << "\n"; - } - llvm::outs() << "==== END EDITS ====\n"; - return 0; } diff --git a/tools/clang/rewrite_raw_ptr_fields/tests/attributes-expected.cc b/tools/clang/rewrite_raw_ptr_fields/tests/attributes-expected.cc index ed095d1ad3587a..310b05a255f7c2 100644 --- a/tools/clang/rewrite_raw_ptr_fields/tests/attributes-expected.cc +++ b/tools/clang/rewrite_raw_ptr_fields/tests/attributes-expected.cc @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include "base/memory/checked_ptr.h" + class SomeClass; // Based on Chromium's //base/thread_annotations.h diff --git a/tools/clang/rewrite_raw_ptr_fields/tests/basics-expected.cc b/tools/clang/rewrite_raw_ptr_fields/tests/basics-expected.cc index e1df2a052faa2d..4b06d4efbfd942 100644 --- a/tools/clang/rewrite_raw_ptr_fields/tests/basics-expected.cc +++ b/tools/clang/rewrite_raw_ptr_fields/tests/basics-expected.cc @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include "base/memory/checked_ptr.h" + class SomeClass; class MyClass { diff --git a/tools/clang/rewrite_raw_ptr_fields/tests/initializers-expected.cc b/tools/clang/rewrite_raw_ptr_fields/tests/initializers-expected.cc index 1432c2a380dead..848925921628dd 100644 --- a/tools/clang/rewrite_raw_ptr_fields/tests/initializers-expected.cc +++ b/tools/clang/rewrite_raw_ptr_fields/tests/initializers-expected.cc @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include "base/memory/checked_ptr.h" + class SomeClass; SomeClass* GetPointer(); diff --git a/tools/clang/rewrite_raw_ptr_fields/tests/qualifiers-expected.cc b/tools/clang/rewrite_raw_ptr_fields/tests/qualifiers-expected.cc index 657ca6cb0b2862..117f5db770fed2 100644 --- a/tools/clang/rewrite_raw_ptr_fields/tests/qualifiers-expected.cc +++ b/tools/clang/rewrite_raw_ptr_fields/tests/qualifiers-expected.cc @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include "base/memory/checked_ptr.h" + class SomeClass; class MyClass { diff --git a/tools/clang/rewrite_raw_ptr_fields/tests/typedefs-expected.cc b/tools/clang/rewrite_raw_ptr_fields/tests/typedefs-expected.cc index c8257367aad1d0..318db1c2640ddd 100644 --- a/tools/clang/rewrite_raw_ptr_fields/tests/typedefs-expected.cc +++ b/tools/clang/rewrite_raw_ptr_fields/tests/typedefs-expected.cc @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include "base/memory/checked_ptr.h" + class SomeClass; // Expected rewrite: typedef CheckedPtr SomeClassPtrTypedef. diff --git a/tools/clang/rewrite_raw_ptr_fields/tests/various-types-expected.cc b/tools/clang/rewrite_raw_ptr_fields/tests/various-types-expected.cc index 16320f88c4ad37..8d75beeb317482 100644 --- a/tools/clang/rewrite_raw_ptr_fields/tests/various-types-expected.cc +++ b/tools/clang/rewrite_raw_ptr_fields/tests/various-types-expected.cc @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include "base/memory/checked_ptr.h" + class SomeClass; struct MyStruct { diff --git a/tools/clang/scripts/apply_edits.py b/tools/clang/scripts/apply_edits.py index 7669bcc9a911a7..f8a35f8b9eaf58 100755 --- a/tools/clang/scripts/apply_edits.py +++ b/tools/clang/scripts/apply_edits.py @@ -22,6 +22,7 @@ import multiprocessing import os import os.path +import re import subprocess import sys @@ -98,38 +99,164 @@ def _ResolvePath(path): return edits -def _ApplyEditsToSingleFile(filename, edits): +_PLATFORM_SUFFIX = \ + r'(?:_(?:android|aura|chromeos|ios|linux|mac|ozone|posix|win|x11))?' +_TEST_SUFFIX = \ + r'(?:_(?:browser|interactive_ui|ui|unit)?test)?' +_suffix_regex = re.compile(_PLATFORM_SUFFIX + _TEST_SUFFIX) + + +def _FindPrimaryHeaderBasename(filepath): + """ Translates bar/foo.cc -> foo + bar/foo_posix.cc -> foo + bar/foo_unittest.cc -> foo + bar/foo.h -> None + """ + dirname, filename = os.path.split(filepath) + basename, extension = os.path.splitext(filename) + if extension == '.h': + return None + basename = _suffix_regex.sub('', basename) + return basename + + +_INCLUDE_INSERTION_POINT_REGEX_TEMPLATE = r''' + ^(?! # Match the start of the first line that is + # not one of the following: + + \s+ # 1. Line starting with whitespace + # (this includes blank lines and continuations of + # C comments that start with whitespace/indentation) + + | // # 2a. A C++ comment + | /\* # 2b. A C comment + | \* # 2c. A continuation of a C comment (see also rule 1. above) + + # 3. Include guards (Chromium-style) + | \#ifndef \s+ [A-Z0-9_]+_H ( | _ | __ ) \b \s* $ + | \#define \s+ [A-Z0-9_]+_H ( | _ | __ ) \b \s* $ + + # 3b. Include guards (anything that repeats): + # - the same has to repeat in both the #ifndef and the #define + # - #define has to be "simple" - either: + # - either: #define GUARD + # - or : #define GUARD 1 + | \#ifndef \s+ (?P [A-Za-z0-9_]* ) \s* $ ( \n | \r )* ^ + \#define \s+ (?P=guard) \s* ( | 1 \s* ) $ + | \#define \s+ (?P=guard) \s* ( | 1 \s* ) $ # Skipping previous line. + + # 4. A C/C++ system include + | \#include \s* < .* > + + # 5. A primary header include + # (%%s should be the basename returned by _FindPrimaryHeaderBasename). + # + # TODO(lukasza): Do not allow any directory below - require the top-level + # directory to be the same and at least one itermediate dirname to be the + # same. + | \#include \s* " + [^"]* \b # Allowing any directory + %s[^"/]*\.h " # Matching both basename.h and basename_posix.h + ) +''' + + +def _InsertNonSystemIncludeHeader(filepath, header_line_to_add, contents): + """ Mutates |contents| (contents of |filepath|) to #include + the |header_to_add + """ + # Don't add the header if it is already present. + replacement_text = header_line_to_add + if replacement_text in contents: + return + replacement_text += '\n' + + # Find the right insertion point. + # + # Note that we depend on a follow-up |git cl format| for the right order of + # headers. Therefore we just need to find the right header group (e.g. skip + # system headers and the primary header). + primary_header_basename = _FindPrimaryHeaderBasename(filepath) + if primary_header_basename is None: + primary_header_basename = ':this:should:never:match:' + regex_text = _INCLUDE_INSERTION_POINT_REGEX_TEMPLATE % primary_header_basename + match = re.search(regex_text, contents, re.MULTILINE | re.VERBOSE) + assert (match is not None) + insertion_point = match.start() + + # Extra empty line is required if the addition is not adjacent to other + # includes. + if not contents[insertion_point:].startswith('#include'): + replacement_text += '\n' + + # Make the edit. + contents[insertion_point:insertion_point] = replacement_text + + +def _ApplyReplacement(filepath, contents, edit, last_edit): + if (last_edit is not None and edit.edit_type == last_edit.edit_type + and edit.offset == last_edit.offset and edit.length == last_edit.length): + raise ValueError(('Conflicting replacement text: ' + + '%s at offset %d, length %d: "%s" != "%s"\n') % + (filepath, edit.offset, edit.length, edit.replacement, + last_edit.replacement)) + + contents[edit.offset:edit.offset + edit.length] = edit.replacement + if not edit.replacement: + _ExtendDeletionIfElementIsInList(contents, edit.offset) + + +def _ApplyIncludeHeader(filepath, contents, edit, last_edit): + header_line_to_add = '#include "%s"' % edit.replacement + _InsertNonSystemIncludeHeader(filepath, header_line_to_add, contents) + + +def _ApplySingleEdit(filepath, contents, edit, last_edit): + if edit.edit_type == 'r': + _ApplyReplacement(filepath, contents, edit, last_edit) + elif edit.edit_type == 'include-user-header': + _ApplyIncludeHeader(filepath, contents, edit, last_edit) + else: + raise ValueError('Unrecognized edit directive "%s": %s\n' % + (edit.edit_type, filepath)) + + +def _ApplyEditsToSingleFileContents(filepath, contents, edits): # Sort the edits and iterate through them in reverse order. Sorting allows # duplicate edits to be quickly skipped, while reversing means that # subsequent edits don't need to have their offsets updated with each edit # applied. + # + # Note that after sorting in reverse, the 'i' directives will come after 'r' + # directives. + edits.sort(reverse=True) + edit_count = 0 error_count = 0 - edits.sort() last_edit = None - with open(filename, 'rb+') as f: - contents = bytearray(f.read()) - for edit in reversed(edits): - if edit == last_edit: - continue - if (last_edit is not None and edit.edit_type == last_edit.edit_type and - edit.offset == last_edit.offset and edit.length == last_edit.length): - sys.stderr.write( - 'Conflicting edit: %s at offset %d, length %d: "%s" != "%s"\n' % - (filename, edit.offset, edit.length, edit.replacement, - last_edit.replacement)) - error_count += 1 - continue - + for edit in edits: + if edit == last_edit: + continue + try: + _ApplySingleEdit(filepath, contents, edit, last_edit) last_edit = edit - contents[edit.offset:edit.offset + edit.length] = edit.replacement - if not edit.replacement: - _ExtendDeletionIfElementIsInList(contents, edit.offset) edit_count += 1 + except ValueError as err: + sys.stderr.write(str(err) + '\n') + error_count += 1 + + return (edit_count, error_count) + + +def _ApplyEditsToSingleFile(filepath, edits): + with open(filepath, 'rb+') as f: + contents = bytearray(f.read()) + edit_and_error_counts = _ApplyEditsToSingleFileContents( + filepath, contents, edits) f.seek(0) f.truncate() f.write(contents) - return (edit_count, error_count) + return edit_and_error_counts def _ApplyEdits(edits): diff --git a/tools/clang/scripts/apply_edits_test.py b/tools/clang/scripts/apply_edits_test.py new file mode 100755 index 00000000000000..4632bc19b0ef2f --- /dev/null +++ b/tools/clang/scripts/apply_edits_test.py @@ -0,0 +1,549 @@ +#!/usr/bin/env python +# Copyright 2020 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 unittest + +import apply_edits + + +def _FindPHB(filepath): + return apply_edits._FindPrimaryHeaderBasename(filepath) + + +class FindPrimaryHeaderBasenameTest(unittest.TestCase): + def testNoOpOnHeader(self): + self.assertIsNone(_FindPHB('bar.h')) + self.assertIsNone(_FindPHB('foo/bar.h')) + + def testStripDirectories(self): + self.assertEqual('bar', _FindPHB('foo/bar.cc')) + + def testStripPlatformSuffix(self): + self.assertEqual('bar', _FindPHB('bar_posix.cc')) + self.assertEqual('bar', _FindPHB('bar_unittest.cc')) + + def testStripTestSuffix(self): + self.assertEqual('bar', _FindPHB('bar_browsertest.cc')) + self.assertEqual('bar', _FindPHB('bar_unittest.cc')) + + def testStripPlatformAndTestSuffix(self): + self.assertEqual('bar', _FindPHB('bar_uitest_aura.cc')) + self.assertEqual('bar', _FindPHB('bar_linux_unittest.cc')) + + def testNoSuffixStrippingWithoutUnderscore(self): + self.assertEqual('barunittest', _FindPHB('barunittest.cc')) + + +def _ApplyEdit(old_contents_string, + edit, + contents_filepath="some_file.cc", + last_edit=None): + if last_edit is not None: + assert (last_edit > edit) # Test or prod caller should ensure. + ba = bytearray() + ba.extend(old_contents_string.encode('ASCII')) + apply_edits._ApplySingleEdit(contents_filepath, ba, edit, last_edit) + return ba.decode('ASCII') + + +def _InsertHeader(old_contents, + contents_filepath='foo/impl.cc', + new_header_path='new/header.h'): + edit = apply_edits.Edit('include-user-header', -1, -1, new_header_path) + return _ApplyEdit(old_contents, edit, contents_filepath=contents_filepath) + + +class InsertIncludeHeaderTest(unittest.TestCase): + def testSkippingCppComments(self): + old_contents = ''' +// Copyright info here. + +#include "old/header.h" + ''' + expected_new_contents = ''' +// Copyright info here. + +#include "new/header.h" +#include "old/header.h" + ''' + new_header_line = '#include "new/header.h' + self.assertEqual(expected_new_contents, _InsertHeader(old_contents)) + + def testSkippingOldStyleComments(self): + old_contents = ''' +/* Copyright + * info here. + */ + +#include "old/header.h" + ''' + expected_new_contents = ''' +/* Copyright + * info here. + */ + +#include "new/header.h" +#include "old/header.h" + ''' + self.assertEqual(expected_new_contents, _InsertHeader(old_contents)) + + def testSkippingOldStyleComments_NoWhitespaceAtLineStart(self): + old_contents = ''' +/* Copyright +* info here. +*/ + +#include "old/header.h" + ''' + expected_new_contents = ''' +/* Copyright +* info here. +*/ + +#include "new/header.h" +#include "old/header.h" + ''' + self.assertEqual(expected_new_contents, _InsertHeader(old_contents)) + + def testSkippingSystemHeaders(self): + old_contents = ''' +#include +#include // blah + +#include "old/header.h" + ''' + expected_new_contents = ''' +#include +#include // blah + +#include "new/header.h" +#include "old/header.h" + ''' + self.assertEqual(expected_new_contents, _InsertHeader(old_contents)) + + def testSkippingPrimaryHeader(self): + old_contents = ''' +// Copyright info here. + +#include "foo/impl.h" + +#include "old/header.h" + ''' + expected_new_contents = ''' +// Copyright info here. + +#include "foo/impl.h" + +#include "new/header.h" +#include "old/header.h" + ''' + self.assertEqual(expected_new_contents, _InsertHeader(old_contents)) + + def testSimilarNonPrimaryHeader_WithPrimaryHeader(self): + old_contents = ''' +// Copyright info here. + +#include "primary/impl.h" // This is the primary header. + +#include "unrelated/impl.h" // This is *not* the primary header. +#include "zzz/foo.h" + ''' + expected_new_contents = ''' +// Copyright info here. + +#include "primary/impl.h" // This is the primary header. + +#include "unrelated/impl.h" // This is *not* the primary header. +#include "new/header.h" +#include "zzz/foo.h" + ''' + self.assertEqual(expected_new_contents, _InsertHeader(old_contents)) + + def testSimilarNonPrimaryHeader_NoPrimaryHeader(self): + old_contents = ''' +// Copyright info here. + +#include "unrelated/impl.h" // This is *not* the primary header. +#include "zzz/foo.h" + ''' + expected_new_contents = ''' +// Copyright info here. + +#include "unrelated/impl.h" // This is *not* the primary header. +#include "new/header.h" +#include "zzz/foo.h" + ''' + self.assertEqual(expected_new_contents, _InsertHeader(old_contents)) + + def testSkippingIncludeGuards(self): + old_contents = ''' +#ifndef FOO_IMPL_H_ +#define FOO_IMPL_H_ + +#include "old/header.h" + +#endif FOO_IMPL_H_ + ''' + expected_new_contents = ''' +#ifndef FOO_IMPL_H_ +#define FOO_IMPL_H_ + +#include "new/header.h" +#include "old/header.h" + +#endif FOO_IMPL_H_ + ''' + self.assertEqual(expected_new_contents, + _InsertHeader(old_contents, 'foo/impl.h', 'new/header.h')) + + def testSkippingIncludeGuards2(self): + # This test is based on base/third_party/valgrind/memcheck.h + old_contents = ''' +#ifndef __MEMCHECK_H +#define __MEMCHECK_H + +#include "old/header.h" + +#endif + ''' + expected_new_contents = ''' +#ifndef __MEMCHECK_H +#define __MEMCHECK_H + +#include "new/header.h" +#include "old/header.h" + +#endif + ''' + self.assertEqual(expected_new_contents, _InsertHeader(old_contents)) + + def testSkippingIncludeGuards3(self): + # This test is based on base/third_party/xdg_mime/xdgmime.h + old_contents = ''' +#ifndef __XDG_MIME_H__ +#define __XDG_MIME_H__ + +#include "old/header.h" + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +typedef void (*XdgMimeCallback) (void *user_data); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ +#endif /* __XDG_MIME_H__ */ + ''' + expected_new_contents = ''' +#ifndef __XDG_MIME_H__ +#define __XDG_MIME_H__ + +#include "new/header.h" +#include "old/header.h" + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +typedef void (*XdgMimeCallback) (void *user_data); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ +#endif /* __XDG_MIME_H__ */ + ''' + self.assertEqual(expected_new_contents, _InsertHeader(old_contents)) + + def testSkippingIncludeGuards4(self): + # This test is based on ash/first_run/desktop_cleaner.h and/or + # components/subresource_filter/core/common/scoped_timers.h and/or + # device/gamepad/abstract_haptic_gamepad.h + old_contents = ''' +#ifndef ASH_FIRST_RUN_DESKTOP_CLEANER_ +#define ASH_FIRST_RUN_DESKTOP_CLEANER_ + +#include "old/header.h" + +namespace ash { +} // namespace ash + +#endif // ASH_FIRST_RUN_DESKTOP_CLEANER_ + ''' + expected_new_contents = ''' +#ifndef ASH_FIRST_RUN_DESKTOP_CLEANER_ +#define ASH_FIRST_RUN_DESKTOP_CLEANER_ + +#include "new/header.h" +#include "old/header.h" + +namespace ash { +} // namespace ash + +#endif // ASH_FIRST_RUN_DESKTOP_CLEANER_ + ''' + self.assertEqual(expected_new_contents, _InsertHeader(old_contents)) + + def testSkippingIncludeGuards5(self): + # This test is based on third_party/weston/include/GLES2/gl2.h (the |extern + # "C"| part has been removed to make the test trickier to handle right - + # otherwise it is easy to see that the header has to be included before the + # |extern "C"| part). + # + # The tricky parts below include: + # 1. upper + lower case characters allowed in the guard name + # 2. Having to recognize that GL_APIENTRYP is *not* a guard + old_contents = ''' +#ifndef __gles2_gl2_h_ +#define __gles2_gl2_h_ 1 + +#include + +#ifndef GL_APIENTRYP +#define GL_APIENTRYP GL_APIENTRY* +#endif + +#endif + ''' + expected_new_contents = ''' +#ifndef __gles2_gl2_h_ +#define __gles2_gl2_h_ 1 + +#include + +#include "new/header.h" + +#ifndef GL_APIENTRYP +#define GL_APIENTRYP GL_APIENTRY* +#endif + +#endif + ''' + self.assertEqual(expected_new_contents, _InsertHeader(old_contents)) + + def testSkippingIncludeGuards6(self): + # This test is based on ios/third_party/blink/src/html_token.h + old_contents = ''' +#ifndef HTMLToken_h +#define HTMLToken_h + +#include +#include + +#include "base/macros.h" + +// ... + +#endif + ''' + expected_new_contents = ''' +#ifndef HTMLToken_h +#define HTMLToken_h + +#include +#include + +#include "new/header.h" +#include "base/macros.h" + +// ... + +#endif + ''' + self.assertEqual(expected_new_contents, _InsertHeader(old_contents)) + + def testNoOpIfAlreadyPresent(self): + # This tests that the new header won't be inserted (and duplicated) + # if it is already included. + old_contents = ''' +// Copyright info here. + +#include "old/header.h" +#include "new/header.h" +#include "new/header2.h" + ''' + expected_new_contents = ''' +// Copyright info here. + +#include "old/header.h" +#include "new/header.h" +#include "new/header2.h" + ''' + self.assertEqual(expected_new_contents, _InsertHeader(old_contents)) + + def testNoOpIfAlreadyPresent_WithTrailingComment(self): + # This tests that the new header won't be inserted (and duplicated) + # if it is already included. + old_contents = ''' +// Copyright info here. + +#include "old/header.h" +#include "new/header.h" // blah +#include "new/header2.h" + ''' + expected_new_contents = ''' +// Copyright info here. + +#include "old/header.h" +#include "new/header.h" // blah +#include "new/header2.h" + ''' + self.assertEqual(expected_new_contents, _InsertHeader(old_contents)) + + def testNoOldHeaders(self): + # This tests that an extra new line is inserted after the new header + # when there are no old headers immediately below. + old_contents = ''' +#include + +struct S {}; + ''' + expected_new_contents = ''' +#include + +#include "new/header.h" + +struct S {}; + ''' + self.assertEqual(expected_new_contents, _InsertHeader(old_contents)) + + def testPlatformIfDefs(self): + # This test is based on + # //base/third_party/double_conversion/double-conversion/utils.h + # We need to insert the new header in a non-conditional part. + old_contents = ''' +#ifndef DOUBLE_CONVERSION_UTILS_H_ +#define DOUBLE_CONVERSION_UTILS_H_ + +#include +#include + +#ifndef DOUBLE_CONVERSION_UNREACHABLE +#ifdef _MSC_VER +void DOUBLE_CONVERSION_NO_RETURN abort_noreturn(); +inline void abort_noreturn() { abort(); } +#define DOUBLE_CONVERSION_UNREACHABLE() (abort_noreturn()) +#else +#define DOUBLE_CONVERSION_UNREACHABLE() (abort()) +#endif +#endif + +namespace double_conversion { + ''' + expected_new_contents = ''' +#ifndef DOUBLE_CONVERSION_UTILS_H_ +#define DOUBLE_CONVERSION_UTILS_H_ + +#include +#include + +#include "new/header.h" + +#ifndef DOUBLE_CONVERSION_UNREACHABLE +#ifdef _MSC_VER +void DOUBLE_CONVERSION_NO_RETURN abort_noreturn(); +inline void abort_noreturn() { abort(); } +#define DOUBLE_CONVERSION_UNREACHABLE() (abort_noreturn()) +#else +#define DOUBLE_CONVERSION_UNREACHABLE() (abort()) +#endif +#endif + +namespace double_conversion { + ''' + self.assertEqual(expected_new_contents, _InsertHeader(old_contents)) + + def testNoOldIncludesAndIfDefs(self): + # Artificial test: no old #includes + some #ifdefs. The main focus of the + # test is ensuring that the new header will be inserted into the + # unconditional part of the file. + old_contents = ''' +#ifndef NDEBUG +#include "base/logging.h" +#endif + +void foo(); + ''' + expected_new_contents = ''' +#include "new/header.h" + +#ifndef NDEBUG +#include "base/logging.h" +#endif + +void foo(); + ''' + self.assertEqual(expected_new_contents, _InsertHeader(old_contents)) + + def testNoOldIncludesAndIfDefs2(self): + # Artificial test: no old #includes + some #ifdefs. The main focus of the + # test is ensuring that the new header will be inserted into the + # unconditional part of the file. + old_contents = ''' +#if defined(OS_WIN) +#include "foo_win.h" +#endif + +void foo(); + ''' + expected_new_contents = ''' +#include "new/header.h" + +#if defined(OS_WIN) +#include "foo_win.h" +#endif + +void foo(); + ''' + self.assertEqual(expected_new_contents, _InsertHeader(old_contents)) + + +def _CreateReplacement(content_string, old_substring, new_substring): + """ Test helper for creating an Edit object with the right offset, etc. """ + offset = content_string.find(old_substring) + return apply_edits.Edit('r', offset, len(old_substring), new_substring) + + +class ApplyReplacementTest(unittest.TestCase): + def testBasics(self): + old_text = "123 456 789" + r = _CreateReplacement(old_text, "456", "foo") + new_text = _ApplyEdit(old_text, r) + self.assertEqual("123 foo 789", new_text) + + def testMiddleListElementRemoval(self): + old_text = "(123, 456, 789) // foobar" + r = _CreateReplacement(old_text, "456", "") + new_text = _ApplyEdit(old_text, r) + self.assertEqual("(123, 789) // foobar", new_text) + + def testFinalElementRemoval(self): + old_text = "(123, 456, 789) // foobar" + r = _CreateReplacement(old_text, "789", "") + new_text = _ApplyEdit(old_text, r) + self.assertEqual("(123, 456) // foobar", new_text) + + def testConflictingReplacement(self): + old_text = "123 456 789" + last = _CreateReplacement(old_text, "456", "foo") + edit = _CreateReplacement(old_text, "456", "bar") + expected_msg_regex = 'Conflicting replacement text' + expected_msg_regex += '.*some_file.cc at offset 4, length 3' + expected_msg_regex += '.*"bar" != "foo"' + with self.assertRaisesRegexp(ValueError, expected_msg_regex): + _ApplyEdit(old_text, edit, last_edit=last) + + def testUnrecognizedEditDirective(self): + old_text = "123 456 789" + edit = apply_edits.Edit('unknown_directive', 123, 456, "foo") + expected_msg_regex = 'Unrecognized edit directive "unknown_directive"' + expected_msg_regex += '.*some_file.cc' + with self.assertRaisesRegexp(ValueError, expected_msg_regex): + _ApplyEdit(old_text, edit) + + +if __name__ == '__main__': + unittest.main()