summaryrefslogtreecommitdiffstats
path: root/debian/uncrustify-trinity/uncrustify-trinity-0.72.0/scripts/update_emscripten_bindings.py
blob: d44a58a62fa0de39f3525b57571a0ea8675a07eb (plain)
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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
#!/bin/python
from __future__ import print_function  # python >= 2.6, chained 'with' >= 2.7

from os.path import dirname, abspath
from os import fdopen as os_fdopen, remove as os_remove, name as os_name
from shutil import copy2
from subprocess import Popen, PIPE
from sys import exit as sys_exit, stderr
from tempfile import mkstemp
from contextlib import contextmanager
from threading import Timer
import re


ROOT_DIR = dirname(dirname(abspath(__file__)))

# ==============================================================================

FILE_BINDINGS = "%s/src/uncrustify_emscripten.cpp" % ROOT_DIR
FILE_TS = "%s/emscripten/libUncrustify.d.ts" % ROOT_DIR

REGION_START = "region enum bindings"
REGION_END = "endregion enum bindings"

''' Enums which values need to be updated in the binding code '''
ENUMS_INFO = [
    {
        'name': 'option_type_e',
        'substitute_name': 'OptionType',
        'filepath': '%s/src/option.h' % ROOT_DIR,
        'extra_arg': [],
        'filter_values': [],
        'suffix_chars': 0,
    },
    {
        'name': 'iarf_e',
        'substitute_name': 'IARF',
        'filepath': '%s/src/option.h' % ROOT_DIR,
        'extra_arg': [],
        'filter_values': ['NOT_DEFINED'],
        'suffix_chars': 0,
    },
    {
        'name': 'line_end_e',
        'substitute_name': 'LineEnd',
        'filepath': '%s/src/option.h' % ROOT_DIR,
        'extra_arg': [],
        'filter_values': [],
        'suffix_chars': 0,
    },
    {
        'name': 'token_pos_e',
        'substitute_name': 'TokenPos',
        'filepath': '%s/src/option.h' % ROOT_DIR,
        'extra_arg': [],
        'filter_values': [],
        'suffix_chars': 0,
    },
    {
        'name': 'log_sev_t',
        'substitute_name': 'LogType',
        'filepath': '%s/src/log_levels.h' % ROOT_DIR,
        'extra_arg': [],
        'filter_values': [],
        'suffix_chars': 1,
    },
    {
        'name': 'c_token_t',
        'substitute_name': 'TokenType',
        'filepath': '%s/src/token_enum.h' % ROOT_DIR,
        'extra_arg': [],
        'filter_values': ['CT_TOKEN_COUNT_'],
        'suffix_chars': 3,
    },
    {
        'name': 'lang_flag_e',
        'substitute_name': 'Language',
        'filepath': '%s/src/uncrustify_types.h' % ROOT_DIR,
        'extra_arg': ["-extra-arg=-std=c++1z", "-extra-arg=-DEMSCRIPTEN"],
        'filter_values': [
            'LANG_ALLC',
            'LANG_ALL',
            'FLAG_HDR',
            'FLAG_DIG',
            'FLAG_PP',
        ],
        'suffix_chars': 5,
    },
]

# ==============================================================================

NULL_DEV = "/dev/null" if os_name != "nt" else "nul"


@contextmanager
def make_raw_temp_file(*args, **kwargs):
    fd, tmp_file_name = mkstemp(*args, **kwargs)
    try:
        yield (fd, tmp_file_name)
    finally:
        os_remove(tmp_file_name)


@contextmanager
def open_fd(*args, **kwargs):
    fp = os_fdopen(*args, **kwargs)
    try:
        yield fp
    finally:
        fp.close()


def term_proc(proc, timeout):
    """
    helper function terminate a process if a timer times out

    :param proc: the process object that is going to be terminated
    :param timeout: value that will be set to indicate termination
    """
    timeout["value"] = True
    proc.terminate()


def proc_output(args, timeout_sec=10):
    """
    grabs output from called program
    :param args: string array containing program name and program arguments
    :param timeout_sec: max sec the program can run without being terminated
    :return: utf8 decoded program output in a string
    """
    proc = Popen(args, stdout=PIPE)

    timeout = {"value": False}
    if timeout_sec is not None:
        timeout = {"value": False}
        timer = Timer(timeout_sec, term_proc, [proc, timeout])
        timer.start()

    output_b, error_txt_b = proc.communicate()

    if timeout_sec is not None:
        timer.cancel()

    output = output_b.decode("UTF-8")

    if timeout["value"]:
        print("proc timeout: %s" % ' '.join(args), file=stderr)

    return output if not timeout["value"] else None


def get_enum_lines(enum_info):
    """
    extracts enum values from a file via clang-check

    :param enum_info: dict with:
                        'name' (name of the enum),
                        'filepath' (file containing the enum definition),
                        'extra_arg' (extra arguments passed to clang-check)
    :return: list containing enum values
    """
    cut_len = len(enum_info['name'])

    proc_args = ["clang-check", enum_info['filepath'], "-ast-dump",
                 '-ast-dump-filter=%s' % enum_info['name']]
    proc_args += enum_info['extra_arg']

    output = proc_output(proc_args)
    if output is None or len(output) == 0:
        print("ScriptError: %s - empty clang-check return" % get_enum_lines.__name__,
              file=stderr)
        return ()

    reg_obj = re.compile("EnumConstantDecl.+col:\d+ (referenced )?(\w+)")

    lines = [m.group(2) for l in output.splitlines()
             for m in [re.search(reg_obj, l)] if m]
    lines = [line for line in lines if line not in enum_info['filter_values']]

    if len(lines) == 0:
        print("ScriptError: %s - no enum_info names found" % get_enum_lines.__name__,
              file=stderr)
        return ()
    return lines


def write_ts(opened_file_obj, enum_info):
    """
    writes enum values in a specific typescript d.ts file format

    :param opened_file_obj: opened file file object (with write permissions)
    :param enum_info: dict with:
                        'name' (name of the enum),
                        'substitute_name' (substitute name for the enum),
                        'filepath' (file containing the enum definition),
                        'extra_arg' (extra arguments passed to clang-check)
    :return: False on failure else True
    """
    lines = get_enum_lines(enum_info)
    if len(lines) == 0:
        return False

    opened_file_obj.write(
        '    export interface %sValue extends EmscriptenEnumTypeObject {}\n'
        '    export interface %s extends EmscriptenEnumType\n'
        '    {\n'
        % (enum_info['substitute_name'], enum_info['substitute_name'])
    )
    for line in lines:
        opened_file_obj.write(
            '        %s : %sValue;\n'
            % (line[enum_info['suffix_chars']:], enum_info['substitute_name'])
        )
    opened_file_obj.write(
        '    }\n\n'
    )
    return True


def write_bindings(opened_file_obj, enum_info):
    """
    writes enum values in a specific emscripten embind enum bindings format

    :param opened_file_obj: opened file file object (with write permissions)
    :param enum_info: dict with:
                        'name' (name of the enum),
                        'filepath' (file containing the enum definition),
                        'extra_arg' (extra arguments passed to clang-check)
    :return: False on failure else True
    """
    lines = get_enum_lines(enum_info)
    if len(lines) == 0:
        return False

    opened_file_obj.write(
        '   enum_<%s>("%s")' % (enum_info['name'], enum_info['substitute_name'])
    )
    for line in lines:
        opened_file_obj.write(
            '\n      .value("%s", %s::%s)'
            % (line[enum_info['suffix_chars']:], enum_info['name'], line)
        )
    opened_file_obj.write(
        ';\n\n'
    )
    return True


def update_file(file_path, writer_func, enums_info):
    """
    reads in a file and replaces old enum value in a region, which is defined by
    region start and end string, with updated ones

    :param file_path: file in which the replacement will be made
    :param writer_func: name of the function that will be called to write new
                        content
    :param enums_info:list of dicts each containing:
                    'name' (name of the enum),
                    'substitute_name' (substitute name for the enum),
                    'filepath' (file containing the enum definition),
                    'extra_arg' (extra arguments passed to clang-check)
    :return: False on failure else True
    """
    in_target_region = False

    reg_obj_start = re.compile(".*%s$" % REGION_START)
    reg_obj_end = re.compile(".*%s$" % REGION_END)
    reg_obj = reg_obj_start

    with make_raw_temp_file(suffix='.unc') as (fd, tmp_file_path):
        with open(file_path, 'r') as fr, open_fd(fd, 'w') as fw:
            for line in fr:
                match = None if reg_obj is None else re.search(reg_obj, line)

                if match is None and not in_target_region:
                    fw.write(line)                # write out of region code

                elif match is not None and not in_target_region:
                    fw.write(line)                # hit the start region

                    in_target_region = True
                    reg_obj = reg_obj_end

                    for enum in enums_info:
                        succes_flag = writer_func(fw, enum)
                        if not succes_flag:       # abort, keep input file clean
                            return False

                elif match is None and in_target_region:
                    pass                          # ignore old binding code

                elif match and in_target_region:  # hit the endregion
                    fw.write(line)

                    in_target_region = False
                    reg_obj = None

        copy2(tmp_file_path, file_path)           # overwrite input file
        return True


def main():
    flag = update_file(FILE_BINDINGS, write_bindings, ENUMS_INFO)
    if not flag:
        return 1

    flag = update_file(FILE_TS, write_ts, ENUMS_INFO)
    if not flag:
        return 1

    return 0


if __name__ == "__main__":
    sys_exit(main())