#!/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:]))