aboutsummaryrefslogtreecommitdiff
path: root/scripts/formatcode.py
blob: 49a8753da99ac1318fe8fc8fd3f7b287e0904f71 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import argparse
import fileinput
import os
import pathlib
import re
import subprocess

match_expressions = []
valid_extensions = []
root_dir = ''
use_batching = True

def is_header_missing(f):
    with open(f) as reader:
        lines = reader.read().lstrip().splitlines()
        if len(lines) > 0: return not lines[0].startswith("// ")
        return True

def add_headers(files, header):
    for line in fileinput.input(files, inplace=True):
        if fileinput.isfirstline():
            [ print(h) for h in header.splitlines() ]
        print(line, end="")

def scan_tree(root):
    files = []
    header_files = []
    with os.scandir(root) as dirs:
        for entry in dirs:
            if entry.is_dir():
                scan_tree(os.path.join(root, entry.name))
                continue
            full_path = os.path.join(root, entry.name)
            relative_root_path = os.path.relpath(full_path, start=root_dir)
            if is_matching_filename(relative_root_path):
                print("... formatting: {}".format(relative_root_path))
                files.append(full_path)
                if is_header_missing(full_path):
                    header_files.append(full_path)
    args = ""
    if files:
        if use_batching:
            os.system("clang-format -i " + " ".join(files))
        else:
            for file in files:
                os.system("clang-format -i " + file)
    if header_files:
        add_headers(header_files, "// Copyright Epic Games, Inc. All Rights Reserved.\n\n")

def scan_zen(root):
    with os.scandir(root) as dirs:
        for entry in dirs:
            if entry.is_dir() and entry.name.startswith("zen"):
                    scan_tree(os.path.join(root, entry.name))

def is_matching_filename(relative_root_path):
    global match_expressions
    global root_dir
    global valid_extensions

    if os.path.splitext(relative_root_path)[1].lower() not in valid_extensions:
        return False
    if not match_expressions:
        return True
    relative_root_path = relative_root_path.replace('\\', '/')
    for regex in match_expressions:
        if regex.fullmatch(relative_root_path):
            return True
    return False

def parse_match_expressions(wildcards, matches):
    global match_expressions
    global valid_extensions

    valid_extensions = ['.cpp', '.h']

    for wildcard in wildcards:
        regex = wildcard.replace('*', '%FORMAT_STAR%').replace('\\', '/')
        regex = re.escape(regex)
        regex = '.*' + regex.replace('%FORMAT_STAR%', '.*') + '.*'
        try:
            match_expressions.append(re.compile(regex, re.IGNORECASE))
        except Exception as ex:
            print(f'Could not parse input filename expression \'{wildcard}\': {str(ex)}')
            quit()
    for regex in matches:
        try:
            match_expressions.append(re.compile(regex, re.IGNORECASE))
        except Exception as ex:
            print(f'Could not parse input --match expression \'{regex}\': {str(ex)}')
            quit()

def validate_clang_format():
    vstring = subprocess.check_output("clang-format --version", shell=True).decode().rstrip()

    match = re.search(r'(\d+)\.(\d+)(\.(\d+))?$', vstring)
    if not match:
        raise ValueError("invalid version number '%s'" % vstring)

    (major, minor, patch) = match.group(1, 2, 4)

    if int(major) < 13:
        if int(minor) == 0:
            if int(patch) < 1:
                raise ValueError(f'invalid clang-format version -- we require at least v12.0.1')

def _main():
    global root_dir, use_batching

    parser = argparse.ArgumentParser()
    parser.add_argument('filenames', nargs='*', help="Match text for filenames. If fullpath contains text it is a match, " +\
        "* is a wildcard. Directory separators are matched by either / or \\. Case insensitive.")
    parser.add_argument('--match', action='append', default=[], help="Match regular expression for filenames. " +\
        "Relative path from the root zen directory must be a complete match. Directory separators are matched only by /. Case insensitive.")
    parser.add_argument('--batch', dest='use_batching', action='store_true', help="Enable batching calls to clang-format.")
    parser.add_argument('--no-batch', dest='use_batching', action='store_false', help="Disable batching calls to clang-format.")
    parser.set_defaults(use_batching=True)
    options = parser.parse_args()

    parse_match_expressions(options.filenames, options.match)
    root_dir = pathlib.Path(__file__).parent.parent.resolve()
    use_batching = options.use_batching

    validate_clang_format()

    while True:
        if (os.path.isfile(".clang-format")):
            scan_zen(".")
            quit()
        else:
            cwd = os.getcwd()
            if os.path.dirname(cwd) == cwd:
                quit()
            os.chdir("..")


if __name__ == '__main__':
    _main()