chromium/ios/tools/cmvc.py

#!/usr/bin/env python3
# Copyright 2024 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
r"""
Generates code/header skeletons for coordinator, coordinator configuration,
coordinator delegate, mediator, view controller, presentation delegate,
consumer, mutator and BUILD file. It can be called piecemeal with different
parameters to add a new component to existing files. Note that it will create
all related files, even if they are are not asked for. For example adding
a consumer will automatically update/create the view controller and mediator.
BUILD.gn file is always added or updated, regardless of parts selected.
This script should be run from chromium root src folder.
Example:

cmvc --out ios/chrome/browser/ui/choose_from_drive --cm ChooseFromDrive c m

Will create the `out` folder if needed then generate or update:
choose_from_drive_coordinator.h
choose_from_drive_coordinator.mm
choose_from_drive_coordinator_delegate.h
choose_from_drive_mediator.h
choose_from_drive_mediator.mm

cmvc --out ios/chrome/browser/ui/choose_from_drive --cm ChooseFromDrive \
  --vc FilePicker cons

Will add:

file_picker_consumer.h
file_picker_view_controller.h
file_picker_view_controller.mm

And update:

choose_from_drive_mediator.h
choose_from_drive_mediator.mm

Note: As usual it is suggested to backup, or be ready to do a git revert, on
the destination folder before running this script.
Note: Adding a configuration to an existing coordinator is not handled.
It would need updating the designated initializer with adding the configuration.
The configuration file skeleton will still be created, however.

"""

import argparse
import os
import re
import subprocess
import sys

# Character to set colors in terminal.
TERMINAL_ERROR_COLOR = "\033[1m\033[91m"
TERMINAL_INFO_COLOR = "\033[22m\033[92m"
TERMINAL_RESET_COLOR = "\033[0m"

# Static obj-c match regular expressions.
SNAKE_CASE_RE = re.compile(r"(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])")
IMPORT_RE = re.compile(r"^#import\s")
FORWARDS_RE = re.compile(r"^(@class|@protocol|class|enum)\s+([A-Za-z0-9_]+);$")
OBJC_TYPE_START_RE = re.compile(r"^@(?:interface|protocol)\s+")
OBJC_TYPE_RE = re.compile(
    r"^@(interface|protocol)\s+([A-Za-z0-9_]+)[\s\n]+"
    r"(:[\s\n]*([A-Za-z0-9_]+))?[\s\n]*"
    r"(<[\s\n]*(,?[\s\n]*[A-Za-z0-9_]+[\s\n]*)+>)?[\s\n]*$")
END_RE = re.compile(r"^@end$")
PROPERTY_START_RE = re.compile(r"^@property\s*\(")
PROPERTY_RE = re.compile(
    r"^@property\s*(\([^\)]+\))[\s\n]*([a-zA-Z<>_,]+)[\s\n]*([a-zA-Z0-9_]+);$")
METHOD_START_RE = re.compile(r"^[-+]\s\(")
METHOD_H_END_RE = re.compile(r";$")
METHOD_HEAD_MM_RE = re.compile(r"^([-+]\s*[^{]+)")
IMPLEMENTATION_HEAD_RE = re.compile(
    r"^@implementation\s+([A-Za-z0-9_]+)\s*([{])?$")
IMPLEMENTATION_BLOCK_END_RE = re.compile(r"^}$")
PRAGMA_MARK_RE = re.compile(r"^#pragma\smark\s(-\s+)?(.+)$")
IVARS_RE = re.compile(r"^\s+(.+)\s+(.+);$")

GN_SOURCE_SET_START_RE = re.compile(r'^source_set\(\"([^\"]+)\"\)\s+{$')
GN_SOURCE_SET_END_RE = re.compile(r'^}$')
GN_BLOCK_ONE_LINE_RE = re.compile(
    r'^  (sources|deps|frameworks)\s*=\s*\[\s*\"([^\"]+)\"\s*\]$')
GN_BLOCK_START_RE = re.compile(r'^  (sources|deps|frameworks)\s*=\s*\[$')
GN_ITEM_RE = re.compile(r'^    \"([^\"]+)\",$')
GN_BLOCK_END_RE = re.compile(r'  \]$')

# Set to True for verbose output
DEBUG = False


def adapted_color_for_output(color_start, color_end):
    """Returns a the `color_start`, `color_end` tuple if the output is a
    terminal, or empty strings otherwise"""
    if not sys.stdout.isatty():
        return "", ""
    return color_start, color_end


def print_error(error_message, error_info=""):
    """Print the `error_message` with additional `error_info`"""
    color_start, color_end = adapted_color_for_output(TERMINAL_ERROR_COLOR,
                                                      TERMINAL_RESET_COLOR)

    error_message = color_start + "ERROR: " + error_message + color_end
    if len(error_info) > 0:
        error_message = error_message + "\n" + error_info
    print(error_message)


def print_info(info_message):
    """Print the `warning_message` with additional `warning_info`"""
    color_start, color_end = adapted_color_for_output(TERMINAL_INFO_COLOR,
                                                      TERMINAL_RESET_COLOR)

    info_message = color_start + "INFO: " + info_message + color_end
    print(info_message)


def print_debug(debug_message):
    """`print_info` `debug_message` if `DEBUG` is True."""
    if DEBUG:
        print_info(debug_message)


def to_variable_name(class_name):
    return "".join(c.lower() if i == 0 else c
                   for i, c in enumerate(class_name))


def main(args):
    parser = argparse.ArgumentParser(
        description=__doc__, formatter_class=argparse.RawTextHelpFormatter)
    parser.add_argument(
        'parts',
        nargs='+',
        help='list parts to generate: all [c]oordinator [m]ediator '
        '[mu]tator [pres]entation_delegate [conf]iguration '
        'coordinator_[d]elegate [cons]umer [v]iew_controller')
    parser.add_argument("--cm", help="Coordinator/Mediator prefix", default="")
    parser.add_argument("--vc", help="ViewController prefix", default="")
    parser.add_argument("--out", help="The destination dir", default=".")
    args = parser.parse_args()

    coordinator = ("all" in args.parts or "c" in args.parts
                   or "coordinator" in args.parts)
    mediator = ("all" in args.parts or "m" in args.parts
                or "mediator" in args.parts)
    mutator = ("all" in args.parts or "mu" in args.parts
               or "mutator" in args.parts)
    presentation_delegate = ("all" in args.parts or "pres" in args.parts
                             or "presentation_delegate" in args.parts)
    configuration = ("all" in args.parts or "conf" in args.parts
                     or "configuration" in args.parts)
    coordinator_delegate = ("all" in args.parts or "d" in args.parts
                            or "coordinator_delegate" in args.parts)
    consumer = ("all" in args.parts or "cons" in args.parts
                or "consumer" in args.parts)
    view_controller = ("all" in args.parts or "v" in args.parts
                       or "view_controller" in args.parts)

    out_path = args.out
    coordinator_prefix = args.cm
    vc_prefix = args.vc

    if not os.path.exists(out_path):
        print_info(f"creating folder: {out_path}")
        os.makedirs(out_path)

    # Object class names.
    coordinator_name = f"{coordinator_prefix}Coordinator"
    coordinator_delegate_name = f"{coordinator_prefix}CoordinatorDelegate"
    configuration_name = f"{coordinator_prefix}Configuration"
    mediator_name = f"{coordinator_prefix}Mediator"

    vc_name = f"{vc_prefix}ViewController"
    vc_presentation_delegate_name = f"{vc_prefix}PresentationDelegate"
    vc_consumer_name = f"{vc_prefix}Consumer"
    vc_mutator_name = f"{vc_prefix}Mutator"

    # File names.
    coordinator_snake_name = SNAKE_CASE_RE.sub("_", coordinator_prefix).lower()
    vc_snake_name = SNAKE_CASE_RE.sub("_", vc_prefix).lower()

    coordinator_h = os.path.join(out_path,
                                 f"{coordinator_snake_name}_coordinator.h")
    coordinator_mm = os.path.join(out_path,
                                  f"{coordinator_snake_name}_coordinator.mm")
    coordinator_delegate_h = os.path.join(
        out_path, f"{coordinator_snake_name}_coordinator_delegate.h")
    configuration_h = os.path.join(
        out_path, f"{coordinator_snake_name}_configuration.h")
    configuration_mm = os.path.join(
        out_path, f"{coordinator_snake_name}_configuration.mm")
    mediator_h = os.path.join(out_path, f"{coordinator_snake_name}_mediator.h")
    mediator_mm = os.path.join(out_path,
                               f"{coordinator_snake_name}_mediator.mm")

    vc_h = os.path.join(out_path, f"{vc_snake_name}_view_controller.h")
    vc_mm = os.path.join(out_path, f"{vc_snake_name}_view_controller.mm")
    vc_presentation_delegate_h = os.path.join(
        out_path, f"{vc_snake_name}_presentation_delegate.h")
    vc_consumer_h = os.path.join(out_path, f"{vc_snake_name}_consumer.h")
    vc_mutator_h = os.path.join(out_path, f"{vc_snake_name}_mutator.h")

    # BUILD.gn data
    cm_source_set_files = []
    vc_source_set_files = []
    cm_source_set_deps = ["//base"]
    vc_source_set_deps = ["//ui/base"]
    build_gn = os.path.join(out_path, "BUILD.gn")

    # Coordinator h and mm files
    if (coordinator or presentation_delegate or configuration
            or coordinator_delegate):
        cm_source_set_deps.append("//ios/chrome/browser/shared/coordinator/"
                                  "chrome_coordinator")
        cm_source_set_files.append(f"{coordinator_snake_name}_coordinator.h")
        cm_source_set_files.append(f"{coordinator_snake_name}_coordinator.mm")

        wanted_imports = {
            '#import "ios/chrome/browser/shared/coordinator/'
            'chrome_coordinator/chrome_coordinator.h"',
        }
        if presentation_delegate:
            wanted_imports.add(f'#import "{vc_presentation_delegate_h}"')

        wanted_forwards = set()
        if configuration:
            wanted_forwards.add(f"@class {configuration_name};")
        if coordinator_delegate:
            wanted_forwards.add(f"@protocol {coordinator_delegate_name};")

        wanted_obj_type_protocols = set()
        if presentation_delegate:
            wanted_obj_type_protocols.add(vc_presentation_delegate_name)

        wanted_properties = {}
        if coordinator_delegate:
            wanted_properties[f"id<{coordinator_delegate_name}>"] = (
                "(nonatomic, weak)",
                "delegate",
            )

        if configuration:
            wanted_methods = [
                "- (instancetype)initWithBaseViewController:"
                "(UIViewController*)viewController"
                " browser:(Browser*)browser"
                f"  configuration:({configuration_name}*)configuration"
                " NS_DESIGNATED_INITIALIZER;\n",
                "- (instancetype)initWithBaseViewController:"
                "(UIViewController*)viewController"
                " browser:(Browser*)browser NS_UNAVAILABLE;\n",
            ]
        else:
            wanted_methods = [
                "- (instancetype)initWithBaseViewController:"
                "(UIViewController*)viewController"
                " browser:(Browser*)browser NS_DESIGNATED_INITIALIZER;"
            ]

        update_h_file(
            coordinator_h,
            "interface",
            coordinator_name,
            "ChromeCoordinator",
            wanted_imports,
            wanted_forwards,
            wanted_obj_type_protocols,
            wanted_properties,
            wanted_methods,
        )

        wanted_imports = {
            f'#import "{coordinator_h}"',
        }
        if mediator:
            wanted_imports.add(f'#import "{mediator_h}"')
        if view_controller:
            wanted_imports.add(f'#import "{vc_h}"')

        wanted_ivars = {}
        if mediator:
            wanted_ivars["_mediator"] = f"{mediator_name}*"
        if view_controller:
            wanted_ivars["_viewController"] = f"{vc_name}*"
        if configuration:
            wanted_ivars["_configuration"] = f"__strong {configuration_name}*"

        wanted_methods = {}
        if configuration:
            wanted_methods["-"] = {
                f"- (instancetype)initWithBaseViewController:"
                f"(UIViewController*)viewController browser:(Browser*)browser "
                f"configuration:({configuration_name}*)configuration":
                "self = [super initWithBaseViewController:viewController "
                "browser:browser];\n"
                "if (self) {\n"
                "    _configuration = configuration;\n"
                "}\n"
                " return self;\n"
            }
        else:
            wanted_methods["-"] = {
                f"- (instancetype)initWithBaseViewController:"
                f"(UIViewController*)viewController browser:(Browser*)browser":
                "self = [super initWithBaseViewController:viewController "
                "browser:browser];\n"
                "if (self) {\n"
                "}\n"
                " return self;\n"
            }
        start_parts = []
        if mediator:
            start_parts.append(f"_mediator = [[{mediator_name} alloc] "
                               "initWithSomething:@\"something\"];")
        if view_controller:
            start_parts.append(f"_viewController = [[{vc_name} alloc] init];")
        if view_controller and mutator and mediator:
            start_parts.append(f"_viewController.mutator = _mediator;")
        if view_controller and presentation_delegate:
            start_parts.append(
                f"_mediator.{to_variable_name(vc_consumer_name)}"
                f" = _viewController;")
        stop_parts = []
        if mediator:
            stop_parts.append(f"[_mediator disconnect];")
            stop_parts.append(f"_mediator = nil;")
        if view_controller:
            stop_parts.append(f"[_viewController "
                              "willMoveToParentViewController:nil];")
            stop_parts.append(f"[_viewController "
                              "removeFromParentViewController];")
            stop_parts.append(f"_viewController = nil;")

        wanted_methods["ChromeCoordinator"] = {
            "- (void)start": "\n".join(start_parts),
            "- (void)stop": "\n".join(stop_parts),
        }

        if presentation_delegate:
            wanted_methods[f"{vc_presentation_delegate_name}"] = {
                f"- (void){to_variable_name(vc_name)}:({vc_name}*)"
                f"viewController dismissedAnimated:(BOOL)animated":
                "// TODO(crbug"
                ".com/_BUG_): do dismiss animated or remove."
            }

        update_mm_file(
            coordinator_mm,
            coordinator_name,
            wanted_imports,
            wanted_ivars,
            wanted_methods,
        )

    # Presentation delegate h file
    if presentation_delegate or view_controller:
        vc_source_set_files.append(f"{vc_snake_name}_presentation_delegate.h")

        wanted_imports = {"#import <Foundation/Foundation.h>"}
        wanted_forwards = {f"@class {vc_name};"}
        wanted_obj_type_protocols = set()
        wanted_properties = {}
        wanted_methods = [
            f"// Called when the user dismisses the VC.\n"
            f"- (void){to_variable_name(vc_name)}:({vc_name}*)viewController"
            f" dismissedAnimated:(BOOL)animated;\n"
        ]

        update_h_file(
            vc_presentation_delegate_h,
            "protocol",
            vc_presentation_delegate_name,
            None,
            wanted_imports,
            wanted_forwards,
            wanted_obj_type_protocols,
            wanted_properties,
            wanted_methods,
        )

    # Coordinator configuration h and mm files
    if configuration:
        cm_source_set_files.append(f"{coordinator_snake_name}_configuration.h")
        cm_source_set_files.append(
            f"{coordinator_snake_name}_configuration.mm")

        wanted_imports = set()
        wanted_imports.add("#import <Foundation/Foundation.h>")
        wanted_forwards = set()
        wanted_obj_type_protocols = set()
        wanted_properties = {}
        wanted_methods = []

        update_h_file(
            configuration_h,
            "interface",
            configuration_name,
            "NSObject",
            wanted_imports,
            wanted_forwards,
            wanted_obj_type_protocols,
            wanted_properties,
            wanted_methods,
        )

        wanted_imports = {
            f'#import "{configuration_h}"',
        }
        wanted_ivars = {}
        wanted_methods = {}

        update_mm_file(
            configuration_mm,
            configuration_name,
            wanted_imports,
            wanted_ivars,
            wanted_methods,
        )

    # Coordinator delegate h file
    if coordinator_delegate or coordinator:
        cm_source_set_files.append(f"{coordinator_snake_name}"
                                   "_coordinator_delegate.h")

        wanted_imports = set()
        wanted_imports.add("#import <Foundation/Foundation.h>")
        wanted_forwards = set()
        wanted_forwards.add(f"@class {coordinator_name};")
        wanted_obj_type_protocols = set()
        wanted_obj_type_protocols.add("NSObject")
        wanted_properties = {}
        wanted_methods = []

        update_h_file(
            coordinator_delegate_h,
            "protocol",
            coordinator_delegate_name,
            None,
            wanted_imports,
            wanted_forwards,
            wanted_obj_type_protocols,
            wanted_properties,
            wanted_methods,
        )

    # Mediator h and mm files
    if mediator or mutator or consumer:
        cm_source_set_files.append(f"{coordinator_snake_name}_mediator.h")
        cm_source_set_files.append(f"{coordinator_snake_name}_mediator.mm")

        wanted_imports = set()
        wanted_imports.add("#import <Foundation/Foundation.h>")
        if mutator:
            wanted_imports.add(f'#import "{vc_mutator_h}"')

        wanted_forwards = set()
        if consumer:
            wanted_forwards.add(f"@protocol {vc_consumer_name};")

        wanted_obj_type_protocols = set()
        if mutator:
            wanted_obj_type_protocols.add(vc_mutator_name)

        wanted_properties = {}
        if consumer:
            wanted_properties[f"id<{vc_consumer_name}>"] = (
                "(nonatomic, weak)",
                to_variable_name(vc_consumer_name),
            )

        wanted_methods = [
            "- (instancetype)initWithSomething:(NSString*)something"
            " NS_DESIGNATED_INITIALIZER;\n",
            "- (instancetype)init NS_UNAVAILABLE;\n\n",
            "- (void)disconnect;\n",
        ]

        update_h_file(
            mediator_h,
            "interface",
            mediator_name,
            "NSObject",
            wanted_imports,
            wanted_forwards,
            wanted_obj_type_protocols,
            wanted_properties,
            wanted_methods,
        )

        wanted_imports = {
            f'#import "{mediator_h}"',
        }
        if consumer:
            wanted_imports.add(f'#import "{vc_consumer_h}"')

        wanted_ivars = {"_something": "NSString*"}

        wanted_methods = {}

        wanted_methods["-"] = {
            f"- (instancetype)initWithSomething:(NSString*)something":
            "self = [super init];\n"
            "if (self) {\n"
            "  _something = something;\n"
            "}\n"
            "return self;\n",
        }

        if consumer:
            wanted_methods["Properties getters/setters"] = {
                f"- (void)set{vc_consumer_name}:(id<{vc_consumer_name}>)"
                "consumer":
                f"_{to_variable_name(vc_consumer_name)} = consumer;\n"
                f"[_{to_variable_name(vc_consumer_name)} "
                f"setAnotherSomething:_something];\n",
            }

        wanted_methods["Public"] = {
            "- (void)disconnect": "_something = nil;\n",
        }

        update_mm_file(
            mediator_mm,
            mediator_name,
            wanted_imports,
            wanted_ivars,
            wanted_methods,
        )

    # Consumer h file
    if consumer:
        vc_source_set_files.append(f"{vc_snake_name}_consumer.h")

        wanted_imports = set()
        wanted_imports.add("#import <Foundation/Foundation.h>")
        wanted_forwards = set()
        wanted_obj_type_protocols = set()
        wanted_properties = {}
        wanted_methods = [
            "// Called when the the data model has a new something.\n"
            "- (void)setAnotherSomething:(NSString*)something;\n"
        ]

        update_h_file(
            vc_consumer_h,
            "protocol",
            vc_consumer_name,
            None,
            wanted_imports,
            wanted_forwards,
            wanted_obj_type_protocols,
            wanted_properties,
            wanted_methods,
        )

    # Mutator h file
    if mutator:
        vc_source_set_files.append(f"{vc_snake_name}_mutator.h")

        wanted_imports = set()
        wanted_imports.add("#import <Foundation/Foundation.h>")
        wanted_forwards = set()
        wanted_obj_type_protocols = set()
        wanted_properties = {}
        wanted_methods = []

        update_h_file(
            vc_mutator_h,
            "protocol",
            vc_mutator_name,
            None,
            wanted_imports,
            wanted_forwards,
            wanted_obj_type_protocols,
            wanted_properties,
            wanted_methods,
        )

    # View controller h and mm files
    if view_controller or consumer or mutator or presentation_delegate:
        vc_source_set_files.append(f"{vc_snake_name}_view_controller.h")
        vc_source_set_files.append(f"{vc_snake_name}_view_controller.mm")

        wanted_imports = {
            "#import <UIKit/UIKit.h>",
        }
        if consumer:
            wanted_imports.add(f'#import "{vc_consumer_h}"')
        if mutator:
            wanted_imports.add(f'#import "{vc_mutator_h}"')

        wanted_forwards = set()
        if coordinator_delegate:
            wanted_forwards.add(f"@protocol {vc_presentation_delegate_name};")

        wanted_obj_type_protocols = set()
        if consumer:
            wanted_obj_type_protocols.add(vc_consumer_name)

        wanted_properties = {}
        if mutator:
            wanted_properties[f"id<{vc_mutator_name}>"] = (
                "(nonatomic, weak)",
                "mutator",
            )
        if presentation_delegate:
            wanted_properties[f"id<{vc_presentation_delegate_name}>"] = (
                "(nonatomic, weak)",
                "presentationDelegate",
            )

        wanted_methods = [
            "- (instancetype)init NS_DESIGNATED_INITIALIZER;\n",
            "- (instancetype)initWithCoder:(NSCoder*)coder NS_UNAVAILABLE;\n",
            "- (instancetype)initWithNibName:(NSString*)nibNameOrNil",
            " bundle:(NSBundle*)nibBundleOrNil NS_UNAVAILABLE;\n",
        ]

        update_h_file(
            vc_h,
            "interface",
            vc_name,
            "UIViewController",
            wanted_imports,
            wanted_forwards,
            wanted_obj_type_protocols,
            wanted_properties,
            wanted_methods,
        )

        wanted_imports = {
            f'#import "{vc_h}"',
        }
        if consumer:
            wanted_imports.add(f'#import "{vc_consumer_h}"')
        if presentation_delegate:
            wanted_imports.add(f'#import "{vc_presentation_delegate_h}"')

        wanted_ivars = {}
        wanted_methods = {}

        wanted_methods["-"] = {
            "- (instancetype)init":
            "self = [super initWithNibName:nil bundle:nil];\n"
            "if (self) {\n"
            "}\n"
            "return self;\n",
        }

        wanted_methods["UIViewController"] = {
            "- (void)viewDidLoad":
            "[super viewDidLoad];\n"
            "// TODO(crbug"
            ".com/_BUG_): setup;\n",
        }

        if consumer:
            wanted_methods[f"{vc_consumer_name}"] = {
                "- (void)setAnotherSomething:(NSString*)something": "",
            }

        update_mm_file(
            vc_mm,
            vc_name,
            wanted_imports,
            wanted_ivars,
            wanted_methods,
        )

    # Create or update BUILD.gn file
    if len(vc_source_set_files):
        cm_source_set_deps.append(f":{vc_snake_name}")

    wanted_source_sets = {}
    if len(cm_source_set_files):
        wanted_source_sets[coordinator_snake_name] = {
            "sources": cm_source_set_files,
            "deps": cm_source_set_deps,
            "frameworks": ["UIKit.framework"],
        }
    if len(cm_source_set_files):
        wanted_source_sets[vc_snake_name] = {
            "sources": vc_source_set_files,
            "deps": vc_source_set_deps,
            "frameworks": ["UIKit.framework"],
        }

    if len(wanted_source_sets):
        update_build_file(build_gn, wanted_source_sets)


def update_build_file(filename, wanted_source_sets):
    """Updates or creates a build file of given`filename` with the content
    request in dictionary `wanted_source_sets`. The dictionary contains keys to
    lists for array elements of BUILD.gn (sources, deps, frameworks)"""
    print_info(filename)

    if not os.path.isfile(filename):
        subprocess.check_call(["tools/boilerplate.py", filename])

    surface_first_line = -1

    found_source_sets = []
    source_set = None

    with open(filename, "r") as body:
        all_lines = body.readlines()

        currently_in = [filename, "license"]

        for line_index, line in enumerate(all_lines):
            if currently_in[-1] == "license":
                if line[0] == "#":
                    continue
                currently_in.pop()
                surface_first_line = line_index
                # fallthrough

            if currently_in[-1] == "source_set":
                if GN_SOURCE_SET_END_RE.search(line):
                    source_set["last_line"] = line_index + 1
                    currently_in.pop()
                    continue
                match = GN_BLOCK_ONE_LINE_RE.search(line)
                if match:
                    block = match.groups()[0]
                    item = match.groups()[1]
                    source_set[f"{block}_first_line"] = line_index
                    source_set[f"{block}_last_line"] = line_index + 1
                    source_set[block].append(item)
                    source_set["block_order"].append(block)
                    continue
                match = GN_BLOCK_START_RE.search(line)
                if match:
                    block = match.groups()[0]
                    source_set[f"{block}_first_line"] = line_index
                    source_set["block_order"].append(block)
                    currently_in.append(block)
                continue

            if currently_in[-1] == "sources" or currently_in[
                    -1] == "deps" or currently_in[-1] == "frameworks":
                block = currently_in[-1]
                if GN_BLOCK_END_RE.search(line):
                    source_set[f"{block}_last_line"] = line_index + 1
                    currently_in.pop()
                    continue
                match = GN_ITEM_RE.search(line)
                if match:
                    source_set[block].append(match.groups()[0])
                    continue

            match = GN_SOURCE_SET_START_RE.search(line)
            if match:
                source_set = {
                    "name": match.groups()[0],
                    "first_line": line_index,
                    "last_line": -1,
                    "sources": [],
                    "sources_first_line": len(all_lines),
                    "sources_last_line": -1,
                    "deps": [],
                    "deps_first_line": len(all_lines),
                    "deps_last_line": -1,
                    "frameworks": [],
                    "frameworks_first_line": len(all_lines),
                    "frameworks_last_line": -1,
                    "block_order": []
                }
                found_source_sets.append(source_set)
                currently_in.append("source_set")
                continue

        # Remove items already in any existing source set
        for block in ['sources']:
            existing_items = set()
            for source_set in found_source_sets:
                for source in source_set[block]:
                    existing_items.add(source)

            for source_set, content in wanted_source_sets.items():
                new_items = []
                for source in content[block]:
                    if source not in existing_items:
                        new_items.append(source)
                content[block] = new_items

        # Remove items in matching source set
        for block in ['deps', 'frameworks']:
            for source_set in found_source_sets:
                if source_set["name"] in wanted_source_sets:
                    wanted_source_set = wanted_source_sets[source_set["name"]]
                    existing_items = set(source_set[block])
                    new_items = []
                    for source in wanted_source_set[block]:
                        if source not in existing_items:
                            new_items.append(source)
                    wanted_source_set[block] = new_items

        print_debug(f"surface: {surface_first_line}")
        print_debug(f"found_source_sets: {found_source_sets}")
        print_debug(f"wanted_source_sets: {wanted_source_sets}")

        # rewrite
        content = []
        changed = False
        cursor = 0

        # Copy until surface start.
        while cursor < len(all_lines) and cursor < surface_first_line:
            content += [all_lines[cursor]]
            cursor += 1

        # Update existing source sets
        for source_set in found_source_sets:
            name = source_set["name"]
            # Copy until first line inside source_set start.
            while (cursor < len(all_lines) and
                   cursor < source_set["first_line"] + 1):
                content += [all_lines[cursor]]
                cursor += 1
            if name in wanted_source_sets:
                wanted_source_set = wanted_source_sets[name]
                # Update existing source set, following block_order
                for block in source_set["block_order"]:
                    if len(wanted_source_set[block]) == 0:
                        continue
                    print_info(f"  updating {name}:{block}")
                    first_line = source_set[f"{block}_first_line"]
                    last_line = source_set[f"{block}_last_line"]
                    # Copy until block start.
                    while cursor < len(all_lines) and cursor < first_line:
                        content += [all_lines[cursor]]
                        cursor += 1
                    # rewrite
                    content += [f"  {block} = [\n"]
                    items = set(source_set[block]).union(
                        wanted_source_set[block])
                    for item in items:
                        content += [f'    "{item}",\n']
                    content += [f"  ]\n"]
                    # Skip lines until end of block
                    cursor = last_line
                    changed = True
                # Add other blocks that are in wanted_source_set.
                for block, items in wanted_source_set.items():
                    if block not in source_set["block_order"]:
                        print_info(f"  adding {name}:{block}")
                        content += [f"  {block} = [\n"]
                        for item in items:
                            content += [f'    "{item}",\n']
                        content += [f"  ]\n"]
                        changed = True
                del wanted_source_sets[name]

        # copy remaining lines
        while cursor < len(all_lines):
            content += [all_lines[cursor]]
            cursor += 1
        content += ["\n"]

        # Add remaining source sets
        for name, source_set in wanted_source_sets.items():
            if len(source_set["sources"]) == 0:
                continue
            print_info(f"  adding {name}")
            content += [f'source_set("{name}") {{\n']
            for block, items in source_set.items():
                content += [f"  {block} = [\n"]
                for item in items:
                    content += [f'    "{item}",\n']
                content += [f"  ]\n"]
            content += ["}\n\n"]
            changed = True

        if not changed:
            print_info(" nothing to do")
            return

        with open(filename, "w") as output:
            output.write("".join(content))

        subprocess.check_call(["gn", "format", filename])


def update_mm_file(
    filename,
    type_name,
    wanted_imports,
    wanted_ivars,
    wanted_methods,
):
    """Creates or updates a objc mm file with `filename`.
    Inserts or adds (in a sorted fashion) all #import pre-processor lines in
    the sets of `wanted_imports`, all ivars and methods in `wanted_ivars` in
    `wanted_methods` to the implementation of class with `type_name`
    Note: Methods are not sorted, but are added at the end of the specified
    section. Sections in the code are determined by `#pragma mark` tags."""

    print_info(filename)

    if not os.path.isfile(filename):
        subprocess.check_call(["tools/boilerplate.py", filename])

    with open(filename, "r") as body:
        all_lines = body.readlines()

        currently_in = [filename, "license"]
        surface_first_line = -1
        import_first_line = len(all_lines)
        import_last_line = -1
        implementation_head_first_line = len(all_lines)
        ivars_first_line = len(all_lines)
        ivars_last_line = -1
        implementation_head_last_line = -1
        implementation_last_line = -1

        found_sections = {}
        found_methods = {}
        found_imports = set()
        found_ivars = {}

        signature = ""

        section = "-"

        skip_lines = 0
        for line_index, line in enumerate(all_lines):
            if skip_lines > 0:
                skip_lines = skip_lines - 1
                continue

            if currently_in[-1] == "license":
                if line.startswith("//"):
                    continue
                else:
                    surface_first_line = line_index
                    currently_in.pop()
                    # fall through

            if currently_in[-1] == "objc_block_ignore":
                if END_RE.search(line):
                    currently_in.pop()
                    continue

            if currently_in[-1] == "ivars":
                match = IVARS_RE.search(line)
                if match:
                    found_ivars[match.groups()[1]] = match.groups()[0]
                    ivars_first_line = min(line_index, ivars_first_line)
                if IMPLEMENTATION_BLOCK_END_RE.search(line):
                    ivars_last_line = line_index
                    implementation_head_last_line = line_index + 1
                    found_sections[section] = {'first_line': line_index + 1}
                    currently_in.pop()
                continue

            if currently_in[-1] == "implementation":
                match = PRAGMA_MARK_RE.search(line)
                if match:
                    found_sections[section]['last_line'] = line_index
                    section = match.groups()[1]
                    found_sections[section] = {'first_line': line_index + 1}
                elif END_RE.search(line):
                    found_sections[section]['last_line'] = line_index
                    implementation_last_line = line_index
                    currently_in.pop()
                elif METHOD_START_RE.search(line):
                    current_line = line
                    index = 0
                    next_line = all_lines[line_index + 1]
                    multiline = current_line.strip()
                    while not current_line.strip()[-1] == "{":
                        separator = '' if multiline[-1] == ':' else ' '
                        multiline = multiline + separator + next_line.strip()
                        index = index + 1
                        current_line = next_line
                        next_line = all_lines[line_index + index + 1]
                    skip_lines = index
                    match = METHOD_HEAD_MM_RE.match(multiline)
                    if match:
                        signature = match.groups()[0].strip()
                        found_methods[signature] = {
                            "section": section,
                            "header_first_line": line_index,
                            "body_first_line": line_index + index + 1,
                        }
                        currently_in.append("method")
                continue

            if currently_in[-1] == "method":
                if IMPLEMENTATION_BLOCK_END_RE.search(line):
                    found_methods[signature]["body_last_line"] = line_index + 1
                    currently_in.pop()
                continue

            if IMPORT_RE.search(line):
                import_first_line = min(line_index, import_first_line)
                import_last_line = max(line_index + 1, import_last_line)
                found_imports.add(line.strip())
                continue

            match = IMPLEMENTATION_HEAD_RE.search(line)
            if match:
                if match.groups()[0] != type_name:
                    currently_in.append("objc_block_ignore")
                    continue
                implementation_head_first_line = line_index
                implementation_head_last_line = line_index + 1
                currently_in.append("implementation")
                if match.groups()[1] == "{":
                    currently_in.append("ivars")
                else:
                    found_sections[section] = {'first_line': line_index + 1}
                continue

        new_imports = wanted_imports.difference(found_imports)
        new_ivars = dict(wanted_ivars)
        for ivar in found_ivars:
            if ivar in new_ivars:
                del new_ivars[ivar]

        # For all methods wanted, remove them in any section where they exist.
        new_methods = {}
        if len(wanted_methods):
            for section, methods in wanted_methods.items():
                new_section = {}
                for signature, value in methods.items():
                    if signature not in found_methods:
                        new_section[signature] = value
                if len(new_section):
                    new_methods[section] = new_section

        print_debug(f"surface: {surface_first_line + 1}")
        print_debug(f"import: {import_first_line + 1} -"
                    f" {import_last_line + 1}")
        if len(new_imports):
            print_debug(f"  new_imports: {new_imports}")
        print_debug(
            f"implementation head: {implementation_head_first_line + 1}"
            f" - {implementation_head_last_line + 1}")
        print_debug(f"implementation: - {implementation_last_line + 1}")
        print_debug(f"ivars: {ivars_first_line + 1} -"
                    f" {ivars_last_line + 1}")
        if len(found_ivars):
            print_debug(f"  {found_ivars}")
        if len(new_ivars):
            print_debug(f"  new_ivars: {new_ivars}")
        print_debug("sections:")
        for section, value in found_sections.items():
            print_debug(f"  '{section}': {value['first_line'] + 1} - "
                        f"{value['last_line'] + 1}")
        print_debug("methods:")
        for signature, value in found_methods.items():
            print_debug(f"  '{signature}': {value}")
        if len(new_methods):
            print_debug(f"  new_methods: {new_methods}")

        # rewrite
        content = []
        changed = False
        cursor = 0

        # Copy until surface start.
        while cursor < len(all_lines) and cursor < surface_first_line:
            content += [all_lines[cursor]]
            cursor += 1

        # Imports.
        if len(new_imports) > 0:
            if import_first_line > import_last_line:
                print_info(" adding import section")
                if content[-1] != "\n":
                    content += ["\n"]
                sorted_imports = list(new_imports)
                sorted_imports.sort()
                for new_import in sorted_imports:
                    content += [f"{new_import}\n"]
                changed = True
            else:
                # Copy until imports start.
                while cursor < len(all_lines) and cursor < import_first_line:
                    content += all_lines[cursor]
                    cursor += 1

                # Insert missing imports if and where needed.
                print_info(" merging import section")
                while cursor < import_last_line:
                    line = all_lines[cursor].strip()
                    sorted_imports = list(new_imports)
                    sorted_imports.sort()
                    for new_import in sorted_imports:
                        if new_import < line:
                            content += [f"{new_import}\n"]
                            changed = True
                            new_imports.remove(new_import)
                    content += [all_lines[cursor]]
                    cursor += 1
                sorted_imports = list(new_imports)
                sorted_imports.sort()
                for new_import in sorted_imports:
                    content += [f"{new_import}\n"]
                    changed = True

        if implementation_head_first_line > implementation_head_last_line:
            # Copy until end.
            while cursor < len(all_lines):
                content += all_lines[cursor]
                cursor += 1

            print_info(f" adding implementation/ivars/methods sections")
            # Add Implementation header.
            content += ["\n"]
            content += [f"@implementation {type_name}"]
            # Add ivars.
            if len(new_ivars):
                content += [" {\n"]
                for ivar, type in new_ivars.items():
                    content += [f"  {type} {ivar};\n"]
                content += ["}"]
            content += ["\n\n"]

            if len(new_methods):
                content += ["\n"]
                for section, methods in new_methods.items():
                    if section != "-":
                        content += [f"#pragma mark - {section}\n\n"]
                    for signature, body in methods.items():
                        content += [f"{signature} {{\n{body}\n}}\n\n"]

            content += ["\n@end\n\n"]
            changed = True
        else:
            # Copy until implementation_head_first_line.
            while (cursor < len(all_lines)
                   and cursor < implementation_head_first_line):
                content += all_lines[cursor]
                cursor += 1

            # If ivars changed.
            if len(new_ivars):
                if ivars_first_line < ivars_last_line:
                    # Copy existing ivars
                    while cursor < len(all_lines) and cursor < ivars_last_line:
                        content += all_lines[cursor]
                        cursor += 1
                    # Add new ivars
                    for ivar, type in new_ivars.items():
                        content += [f"  {type} {ivar};\n"]
                    changed = True
                else:
                    # Copy until implementation_head_last_line.
                    while (cursor < len(all_lines)
                           and cursor < implementation_head_last_line):
                        content += all_lines[cursor]
                        cursor += 1
                    # Add new ivars.
                    content += ["{\n"]
                    for ivar, type in new_ivars.items():
                        content += [f"  {type} {ivar};\n"]
                    content += ["}\n"]
                    changed = True

            # If new methods / sections to add.
            if len(new_methods):
                for section, range in found_sections.items():
                    # Copy lines until end of section.
                    while (cursor < len(all_lines)
                           and cursor < range['last_line']):
                        content += all_lines[cursor]
                        cursor += 1
                    for method_section, methods in new_methods.items():
                        if section != method_section:
                            continue
                        for signature, body in methods.items():
                            content += [f"{signature} {{\n{body}\n}}\n\n"]
                        changed = True

                # Copy until implementation_last_line.
                while (cursor < len(all_lines)
                       and cursor < implementation_last_line):
                    content += all_lines[cursor]
                    cursor += 1
                # Add remaining sections, if any.
                for section, methods in new_methods.items():
                    if section in found_sections:
                        continue
                    if section != "-":
                        content += [f"#pragma mark - {section}\n\n"]
                    for signature, body in methods.items():
                        content += [f"{signature} {{\n{body}\n}}\n\n"]
                    changed = True

        if not changed:
            print_info(" nothing to do")
            return

        # copy remaining lines
        while cursor < len(all_lines):
            content += [all_lines[cursor]]
            cursor += 1

        with open(filename, "w") as output:
            output.write("".join(content))

        subprocess.check_call(["clang-format", "-i", filename])


def update_h_file(
    filename,
    type,
    type_name,
    type_super_name,
    wanted_imports,
    wanted_forwards,
    wanted_obj_type_protocols,
    wanted_properties,
    wanted_methods,
):
    """Creates or updates a objc header file with `filename`.
    Inserts or adds (in a sorted fashion) all #import pre-processor lines in
    the sets of `wanted_imports`, all forward declarations `wanted_forwards` in
    a sorted fashion. Inserts or updates an obj block of `type`
    (protocol or interface) with `type_name` (and with superclass
    `type_super_name` if type is interface) and with the
    `wanted_obj_type_protocols`. Inserts or adds `wanted_properties` and
    `wanted_methods`.
    Note: Properties are not sorted, new ones are added at the end."""

    print_info(filename)

    if not os.path.isfile(filename):
        subprocess.check_call(["tools/boilerplate.py", filename])

    guard = filename.upper() + "_"
    for char in "/\\.+":
        guard = guard.replace(char, "_")

    guard_start_re = re.compile(r"^#define %s" % guard)
    guard_end_re = re.compile(r"^#endif  // %s" % guard)

    with open(filename, "r") as body:
        all_lines = body.readlines()

        currently_in = [filename]
        surface_first_line = len(all_lines)
        surface_last_line = -1
        import_first_line = len(all_lines)
        import_last_line = -1
        forward_first_line = len(all_lines)
        forward_last_line = -1
        obj_type_head_first_line = len(all_lines)
        obj_type_head_last_line = -1
        obj_type_first_line = len(all_lines)
        obj_type_last_line = -1
        properties_first_line = len(all_lines)
        properties_last_line = -1
        method_first_line = len(all_lines)
        method_last_line = -1

        found_imports = set()
        found_forwards = set()
        found_obj_type_protocols = set()

        skip_lines = 0
        for line_index, line in enumerate(all_lines):
            if skip_lines > 0:
                skip_lines = skip_lines - 1
                continue

            if currently_in[-1] == "objc_block_ignore":
                if END_RE.search(line):
                    currently_in.pop()
                    continue

            if currently_in[-1] == "interface":
                if END_RE.search(line):
                    obj_type_last_line = line_index
                    currently_in.pop()
                    continue
                if METHOD_START_RE.search(line):
                    method_first_line = min(line_index, method_first_line)
                    if METHOD_H_END_RE.search(line):
                        method_last_line = max(line_index + 1,
                                               method_last_line)
                    else:
                        currently_in.append("method")
                    continue
                if PROPERTY_START_RE.search(line):
                    current_line = line
                    index = 0
                    next_line = all_lines[line_index + 1]
                    multiline = current_line
                    while (len(next_line.strip()) > 0
                           and not current_line.strip()[-1] == ";"):
                        multiline = multiline + next_line
                        index = index + 1
                        current_line = next_line
                        next_line = all_lines[line_index + index + 1]
                    skip_lines = index
                    match = PROPERTY_RE.match(multiline)
                    if match:
                        wanted_properties.pop(match.groups()[1], None)
                        properties_first_line = min(line_index,
                                                    properties_first_line)
                        properties_last_line = max(line_index + 1,
                                                   properties_last_line)
                continue

            if currently_in[-1] == "method":
                if METHOD_H_END_RE.search(line):
                    method_last_line = max(line_index + 1, method_last_line)
                    currently_in.pop()
                continue

            if guard_start_re.search(line):
                surface_first_line = line_index + 1
                currently_in.append("guard")
                continue
            if guard_end_re.search(line):
                surface_last_line = line_index
                if currently_in.pop() != "guard":
                    print_error("found unexpected h file guard:",
                                f"{line_index}: {line}")
                continue

            if IMPORT_RE.search(line):
                import_first_line = min(line_index, import_first_line)
                import_last_line = max(line_index + 1, import_last_line)
                found_imports.add(line.strip())
                continue

            if FORWARDS_RE.search(line):
                forward_first_line = min(line_index, forward_first_line)
                forward_last_line = max(line_index + 1, forward_last_line)
                found_forwards.add(line.strip())
                continue

            if OBJC_TYPE_START_RE.search(line):
                current_line = line
                index = 0
                next_line = all_lines[line_index + 1]
                multiline = current_line
                while len(next_line.strip()) > 0 and (
                        next_line.strip()[0] in ":<,"
                        or current_line.strip()[-1] in ":<,"):
                    multiline = multiline + next_line
                    index = index + 1
                    current_line = next_line
                    next_line = all_lines[line_index + index + 1]
                skip_lines = index
                match = OBJC_TYPE_RE.match(multiline)
                if (match and match.groups()[0] == type
                        and match.groups()[1] == type_name):
                    obj_type_head_first_line = line_index
                    obj_type_head_last_line = line_index + index + 1
                    obj_type_first_line = obj_type_head_last_line
                    if match.groups()[4]:
                        found_obj_type_protocols = set(
                            map(
                                lambda s: s.strip(),
                                match.groups()[4].strip("<>").split(","),
                            ))
                    currently_in.append("interface")
                else:
                    currently_in.append("objc_block_ignore")
                continue

        if surface_first_line >= surface_last_line:
            print_error("h file surface not found:")

        print_debug(f"surface: {surface_first_line + 1} -"
                    f" {surface_last_line + 1}")
        print_debug(f"import: {import_first_line + 1} -"
                    f" {import_last_line + 1}")
        print_debug(f"forward: {forward_first_line + 1} -"
                    f" {forward_last_line + 1}")
        print_debug(f"{type} head: {obj_type_head_first_line + 1} -"
                    f" {obj_type_head_last_line + 1}")
        print_debug(f"{type} body: {obj_type_first_line + 1} -"
                    f" {obj_type_last_line + 1}")
        print_debug(f"properties: {properties_first_line + 1} -"
                    f" {properties_last_line + 1}")
        print_debug(f"methods: {method_first_line + 1} -"
                    f" {method_last_line + 1}")

        # TODO: assert that if sections exist they are in the order expected.

        # rewrite
        content = []
        changed = False
        cursor = 0

        new_imports = wanted_imports.difference(found_imports)
        new_forwards = wanted_forwards.difference(found_forwards)
        obj_type_protocols = wanted_obj_type_protocols.union(
            found_obj_type_protocols)

        if len(obj_type_protocols) != len(found_obj_type_protocols):
            changed = True

        # Copy until surface start.
        while cursor < len(all_lines) and cursor < surface_first_line:
            content += [all_lines[cursor]]
            cursor += 1

        # Imports.
        if len(new_imports) > 0:
            if import_first_line > import_last_line:
                print_info(" adding import section")
                if content[-1] != "\n":
                    content += ["\n"]
                sorted_imports = list(new_imports)
                sorted_imports.sort()
                for new_import in sorted_imports:
                    content += [f"{new_import}\n"]
                changed = True
            else:
                # Copy until imports start.
                while cursor < len(all_lines) and cursor < import_first_line:
                    content += all_lines[cursor]
                    cursor += 1

                # Insert missing imports if and where needed.
                print_info(" merging import section")
                while cursor < import_last_line:
                    line = all_lines[cursor].strip()
                    sorted_imports = list(new_imports)
                    sorted_imports.sort()
                    for new_import in sorted_imports:
                        if new_import < line:
                            content += [f"{new_import}\n"]
                            changed = True
                            new_imports.remove(new_import)
                    content += [all_lines[cursor]]
                    cursor += 1
                sorted_imports = list(new_imports)
                sorted_imports.sort()
                for new_import in sorted_imports:
                    content += [f"{new_import}\n"]
                    changed = True

        # Forward declarations.
        if len(new_forwards) > 0:

            def sort_by_forward_name(s):
                return FORWARDS_RE.search(s).groups()[0]

            if forward_first_line > forward_last_line:
                print_info(" adding forward declarations section")
                if content[-1] != "\n":
                    content += ["\n"]
                sorted_forwards = list(new_forwards)
                sorted_forwards.sort(key=sort_by_forward_name)
                for new_forward in sorted_forwards:
                    content += [f"{new_forward}\n"]
                changed = True
            else:
                # Copy until forwards start.
                while cursor < len(all_lines) and cursor < forward_first_line:
                    content += all_lines[cursor]
                    cursor += 1

                # Insert missing forwards if and where needed.
                print_info(" merging forward section")
                while cursor < forward_last_line:
                    line = all_lines[cursor].strip()
                    sorted_forwards = list(new_forwards)
                    sorted_forwards.sort(key=sort_by_forward_name)
                    for new_forward in sorted_forwards:
                        if new_forward < line:
                            content += [f"{new_forward}\n"]
                            changed = True
                            new_forwards.remove(new_forward)
                    content += [all_lines[cursor]]
                    cursor += 1
                sorted_forwards = list(new_forwards)
                sorted_forwards.sort(key=sort_by_forward_name)
                for new_forward in sorted_forwards:
                    content += [f"{new_forward}\n"]
                    changed = True

        type_super = f" : {type_super_name}" if type_super_name else ""

        if obj_type_head_first_line > obj_type_head_last_line:
            # Copy until surface end.
            while cursor < len(all_lines) and cursor < surface_last_line:
                content += all_lines[cursor]
                cursor += 1

            print_info(f" adding {type}/property/methods sections")
            content += ["\n"]
            protocols_string = ""
            if len(obj_type_protocols):
                lst = list(obj_type_protocols)
                lst.sort()
                protocols_string = f"<{', '.join(lst)}>"
            content += [
                f"@{type} {type_name}{type_super} {protocols_string}"
                "\n\n"
            ]
            # Add properties.
            if len(wanted_properties):
                content += ["\n"]
                for type in wanted_properties:
                    attributes, name = wanted_properties[type]
                    content += [f"@property{attributes} {type} {name};\n\n"]
            # Add methods.
            if len(wanted_methods):
                for method in wanted_methods:
                    content += [method, "\n"]
            content += ["@end\n\n"]
            changed = True
        else:
            # Copy until type head start.
            while cursor < len(
                    all_lines) and cursor < obj_type_head_first_line:
                content += all_lines[cursor]
                cursor += 1

            if changed:
                print_info(" rewriting {type} head")

            # Rewrite interface/protocol header.
            protocols_string = ""
            if len(obj_type_protocols):
                lst = list(obj_type_protocols)
                lst.sort()
                protocols_string = f"<{', '.join(lst)}>"
            content += [
                f"@{type} {type_name}{type_super} {protocols_string}"
                "\n\n"
            ]

            # Skip existing head data
            while cursor < len(all_lines) and cursor < obj_type_head_last_line:
                cursor += 1

            # Copy existing properties
            if properties_first_line < properties_last_line:
                while cursor < len(
                        all_lines) and cursor < properties_last_line:
                    content += all_lines[cursor]
                    cursor += 1

            # Add properties.
            if len(wanted_properties):
                print_info(" adding properties section")
                content += ["\n"]
                for type in wanted_properties:
                    attributes, name = wanted_properties[type]
                    content += [f"@property{attributes} {type} {name};\n"]
                    content += ["\n"]
                changed = True

            # Add methods section if none there already.
            if method_first_line > method_last_line:
                content += ["\n"]
                if len(wanted_methods):
                    print_info(" adding methods section")
                    for methods in wanted_methods:
                        content += [methods, "\n"]
                    changed = True

        if not changed:
            print_info(" nothing to do")
            return

        # copy remaining lines
        while cursor < len(all_lines):
            content += [all_lines[cursor]]
            cursor += 1

        with open(filename, "w") as output:
            output.write("".join(content))

        subprocess.check_call(["clang-format", "-i", filename])


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))