Skip to content
Snippets Groups Projects
code_clean.py 51 KiB
Newer Older
  • Learn to ignore specific revisions
  • #!/usr/bin/env python3
    
    # SPDX-License-Identifier: GPL-2.0-or-later
    
    
    """
    Example:
      ./source/tools/utils/code_clean.py /src/cmake_debug --match ".*/editmesh_.*" --fix=use_const_vars
    
    Note: currently this is limited to paths in "source/" and "intern/",
    we could change this if it's needed.
    """
    
    
    import argparse
    
    import re
    import subprocess
    import sys
    import os
    
    from typing import (
        Any,
        Dict,
        Generator,
        List,
        Optional,
        Sequence,
    
        Tuple,
        Type,
    )
    
    # List of (source_file, all_arguments)
    ProcessedCommands = List[Tuple[str, str]]
    
    
    USE_MULTIPROCESS = True
    
    VERBOSE = False
    
    # Print the output of the compiler (_very_ noisy, only useful for troubleshooting compiler issues).
    VERBOSE_COMPILER = False
    
    
    Campbell Barton's avatar
    Campbell Barton committed
    # Print the result of each attempted edit:
    
    #
    # - Causes code not to compile.
    # - Compiles but changes the resulting behavior.
    # - Succeeds.
    VERBOSE_EDIT_ACTION = False
    
    
    
    BASE_DIR = os.path.abspath(os.path.dirname(__file__))
    
    SOURCE_DIR = os.path.normpath(os.path.join(BASE_DIR, "..", "..", ".."))
    
    
    # -----------------------------------------------------------------------------
    # Generic Constants
    
    
    # Sorted numeric types.
    # Intentionally missing are "unsigned".
    BUILT_IN_NUMERIC_TYPES = (
        "bool",
        "char",
        "char32_t",
        "double",
        "float",
        "int",
        "int16_t",
        "int32_t",
        "int64_t",
        "int8_t",
        "intptr_t",
        "long",
        "off_t",
        "ptrdiff_t",
        "short",
        "size_t",
        "ssize_t",
        "uchar",
        "uint",
        "uint16_t",
        "uint32_t",
        "uint64_t",
        "uint8_t",
        "uintptr_t",
        "ulong",
        "ushort",
    )
    
    
    IDENTIFIER_CHARS = set(string.ascii_letters + "_" + string.digits)
    
    
    
    # -----------------------------------------------------------------------------
    # General Utilities
    
    # Note that we could use a hash, however there is no advantage, compare it's contents.
    
    def file_as_bytes(filename: str) -> bytes:
    
        with open(filename, 'rb') as fh:
            return fh.read()
    
    
    
    def line_from_span(text: str, start: int, end: int) -> str:
    
        while start > 0 and text[start - 1] != '\n':
            start -= 1
        while end < len(text) and text[end] != '\n':
            end += 1
        return text[start:end]
    
    
    
    def files_recursive_with_ext(path: str, ext: Tuple[str, ...]) -> Generator[str, None, None]:
    
        for dirpath, dirnames, filenames in os.walk(path):
            # skip '.git' and other dot-files.
            dirnames[:] = [d for d in dirnames if not d.startswith(".")]
            for filename in filenames:
                if filename.endswith(ext):
                    yield os.path.join(dirpath, filename)
    
    
    
            data: str,
            pos_beg: int,
            pos_limit: int,
            beg_bracket: str,
            end_bracket: str,
    ) -> int:
        """
        Return the matching bracket or -1.
    
        .. note:: This is not sophisticated, brackets in strings will confuse the function.
        """
        level = 1
    
        # The next bracket.
        pos = pos_beg + 1
    
        # Clamp the limit.
        limit = min(pos_beg + pos_limit, len(data))
    
        while pos < limit:
            c = data[pos]
            if c == beg_bracket:
                level += 1
            elif c == end_bracket:
                level -= 1
                if level == 0:
                    return pos
            pos += 1
        return -1
    
    
    
    def text_matching_bracket_backward(
            data: str,
            pos_end: int,
            pos_limit: int,
            beg_bracket: str,
            end_bracket: str,
    ) -> int:
        """
        Return the matching bracket or -1.
    
        .. note:: This is not sophisticated, brackets in strings will confuse the function.
        """
        level = 1
    
        # The next bracket.
        pos = pos_end - 1
    
        # Clamp the limit.
        limit = max(0, pos_limit)
    
        while pos >= limit:
            c = data[pos]
            if c == end_bracket:
                level += 1
            elif c == beg_bracket:
                level -= 1
                if level == 0:
                    return pos
            pos -= 1
        return -1
    
    
    
    # -----------------------------------------------------------------------------
    # Execution Wrappers
    
    
    def run(args: Sequence[str], *, cwd: Optional[str], quiet: bool) -> int:
    
        if VERBOSE_COMPILER and not quiet:
    
            out = sys.stdout.fileno()
    
        else:
            out = subprocess.DEVNULL
    
    
        p = subprocess.Popen(args, stdout=out, stderr=out, cwd=cwd)
    
        p.wait()
        return p.returncode
    
    
    # -----------------------------------------------------------------------------
    # Build System Access
    
    
    def cmake_cache_var(cmake_dir: str, var: str) -> Optional[str]:
    
        with open(os.path.join(cmake_dir, "CMakeCache.txt"), encoding='utf-8') as cache_file:
            lines = [
                l_strip for l in cache_file
                if (l_strip := l.strip())
                if not l_strip.startswith(("//", "#"))
            ]
    
    
        for l in lines:
            if l.split(":")[0] == var:
                return l.split("=", 1)[-1]
        return None
    
    
    
    def cmake_cache_var_is_true(cmake_var: Optional[str]) -> bool:
        if cmake_var is None:
            return False
    
        cmake_var = cmake_var.upper()
        if cmake_var in {"ON", "YES", "TRUE", "Y"}:
            return True
        if cmake_var.isdigit() and cmake_var != "0":
            return True
    
        return False
    
    
    
    RE_CFILE_SEARCH = re.compile(r"\s\-c\s([\S]+)")
    
    
    
    def process_commands(cmake_dir: str, data: Sequence[str]) -> Optional[ProcessedCommands]:
    
        compiler_c = cmake_cache_var(cmake_dir, "CMAKE_C_COMPILER")
        compiler_cxx = cmake_cache_var(cmake_dir, "CMAKE_CXX_COMPILER")
    
        if compiler_c is None:
    
            sys.stderr.write("Can't find C compiler in %r\n" % cmake_dir)
    
            return None
        if compiler_cxx is None:
    
            sys.stderr.write("Can't find C++ compiler in %r\n" % cmake_dir)
    
            return None
    
    
        # Check for unsupported configurations.
        for arg in ("WITH_UNITY_BUILD", "WITH_COMPILER_CCACHE"):
            if cmake_cache_var_is_true(cmake_cache_var(cmake_dir, arg)):
                sys.stderr.write("The option '%s' must be disabled for proper functionality\n" % arg)
                return None
    
    
        file_args = []
    
        for l in data:
            if (
                    (compiler_c in l) or
                    (compiler_cxx in l)
            ):
                # Extract:
                #   -c SOME_FILE
                c_file_search = re.search(RE_CFILE_SEARCH, l)
                if c_file_search is not None:
                    c_file = c_file_search.group(1)
                    file_args.append((c_file, l))
                else:
                    # could print, NO C FILE FOUND?
                    pass
    
        file_args.sort()
    
        return file_args
    
    
    
    def find_build_args_ninja(build_dir: str) -> Optional[ProcessedCommands]:
    
        import time
    
        cmake_dir = build_dir
        make_exe = "ninja"
        process = subprocess.Popen(
            [make_exe, "-t", "commands"],
            stdout=subprocess.PIPE,
            cwd=build_dir,
        )
        while process.poll():
            time.sleep(1)
    
        assert process.stdout is not None
    
    
        out = process.stdout.read()
        process.stdout.close()
        # print("done!", len(out), "bytes")
        data = out.decode("utf-8", errors="ignore").split("\n")
        return process_commands(cmake_dir, data)
    
    
    
    def find_build_args_make(build_dir: str) -> Optional[ProcessedCommands]:
    
        import time
    
        make_exe = "make"
        process = subprocess.Popen(
            [make_exe, "--always-make", "--dry-run", "--keep-going", "VERBOSE=1"],
            stdout=subprocess.PIPE,
            cwd=build_dir,
        )
        while process.poll():
            time.sleep(1)
    
        assert process.stdout is not None
    
    
        out = process.stdout.read()
        process.stdout.close()
    
        # print("done!", len(out), "bytes")
        data = out.decode("utf-8", errors="ignore").split("\n")
    
        return process_commands(build_dir, data)
    
    
    
    # -----------------------------------------------------------------------------
    # Create Edit Lists
    
    # Create an edit list from a file, in the format:
    #
    #    [((start_index, end_index), text_to_replace), ...]
    #
    # Note that edits should not overlap, in the _very_ rare case overlapping edits are needed,
    # this could be run multiple times on the same code-base.
    #
    # Although this seems like it's not a common use-case.
    
    
    from collections import namedtuple
    Edit = namedtuple(
        "Edit", (
            # Keep first, for sorting.
            "span",
    
            "content",
            "content_fail",
    
            # Optional.
            "extra_build_args",
        ),
    
        defaults=(
            # `extra_build_args`.
            None,
        )
    )
    del namedtuple
    
    
        def __new__(cls, *args: Tuple[Any], **kwargs: Dict[str, Any]) -> Any:
    
            raise RuntimeError("%s should not be instantiated" % cls)
    
        @staticmethod
    
        def edit_list_from_file(_source: str, _data: str, _shared_edit_data: Any) -> List[Edit]:
            raise RuntimeError("This function must be overridden by it's subclass!")
            return []
    
        @staticmethod
        def setup() -> Any:
    
        def teardown(_shared_edit_data: Any) -> None:
    
            pass
    
    
    class edit_generators:
        # fake module.
    
        class sizeof_fixed_array(EditGenerator):
    
            """
            Use fixed size array syntax with `sizeof`:
    
            Replace:
              sizeof(float) * 4 * 4
            With:
              sizeof(float[4][4])
            """
    
            def edit_list_from_file(_source: str, data: str, _shared_edit_data: Any) -> List[Edit]:
    
                edits = []
    
                for match in re.finditer(r"sizeof\(([a-zA-Z_]+)\) \* (\d+) \* (\d+)", data):
    
                    edits.append(Edit(
                        span=match.span(),
                        content='sizeof(%s[%s][%s])' % (match.group(1), match.group(2), match.group(3)),
                        content_fail='__ALWAYS_FAIL__',
    
                    ))
    
                for match in re.finditer(r"sizeof\(([a-zA-Z_]+)\) \* (\d+)", data):
    
                    edits.append(Edit(
                        span=match.span(),
                        content='sizeof(%s[%s])' % (match.group(1), match.group(2)),
                        content_fail='__ALWAYS_FAIL__',
    
                    ))
    
                for match in re.finditer(r"\b(\d+) \* sizeof\(([a-zA-Z_]+)\)", data):
    
                    edits.append(Edit(
                        span=match.span(),
                        content='sizeof(%s[%s])' % (match.group(2), match.group(1)),
                        content_fail='__ALWAYS_FAIL__',
    
                    ))
                return edits
    
        class use_const(EditGenerator):
    
            """
            Use const variables:
    
            Replace:
              float abc[3] = {0, 1, 2};
            With:
              const float abc[3] = {0, 1, 2};
    
            Replace:
              float abc[3]
            With:
              const float abc[3]
    
    
            As well as casts.
    
            Replace:
              (float *)
            With:
              (const float *)
    
            Replace:
              (float (*))
            With:
              (const float (*))
    
            def edit_list_from_file(_source: str, data: str, _shared_edit_data: Any) -> List[Edit]:
    
                # `float abc[3] = {0, 1, 2};` -> `const float abc[3] = {0, 1, 2};`
    
                for match in re.finditer(r"(\(|, |  )([a-zA-Z_0-9]+ [a-zA-Z_0-9]+\[)\b([^\n]+ = )", data):
    
                    edits.append(Edit(
                        span=match.span(),
                        content='%s const %s%s' % (match.group(1), match.group(2), match.group(3)),
                        content_fail='__ALWAYS_FAIL__',
    
                # `float abc[3]` -> `const float abc[3]`
    
                for match in re.finditer(r"(\(|, )([a-zA-Z_0-9]+ [a-zA-Z_0-9]+\[)", data):
    
                    edits.append(Edit(
                        span=match.span(),
                        content='%s const %s' % (match.group(1), match.group(2)),
                        content_fail='__ALWAYS_FAIL__',
    
                    ))
    
                # `(float *)`      -> `(const float *)`
                # `(float (*))`    -> `(const float (*))`
                # `(float (*)[4])` -> `(const float (*)[4])`
                for match in re.finditer(
                        r"(\()"
                        r"([a-zA-Z_0-9]+\s*)"
                        r"(\*+\)|\(\*+\))"
                        r"(|\[[a-zA-Z_0-9]+\])",
                        data,
                ):
                    edits.append(Edit(
                        span=match.span(),
                        content='%sconst %s%s%s' % (match.group(1), match.group(2), match.group(3), match.group(4)),
                        content_fail='__ALWAYS_FAIL__',
    
                    ))
    
                return edits
    
        class use_zero_before_float_suffix(EditGenerator):
    
            """
            Use zero before the float suffix.
    
            Replace:
              1.f
            With:
              1.0f
    
            Replace:
              1.0F
            With:
              1.0f
            """
    
            def edit_list_from_file(_source: str, data: str, _shared_edit_data: Any) -> List[Edit]:
    
                for match in re.finditer(r"\b(\d+)\.([fF])\b", data):
    
                    edits.append(Edit(
                        span=match.span(),
                        content='%s.0%s' % (match.group(1), match.group(2)),
                        content_fail='__ALWAYS_FAIL__',
    
                for match in re.finditer(r"\b(\d+\.\d+)F\b", data):
    
                    edits.append(Edit(
                        span=match.span(),
                        content='%sf' % (match.group(1),),
                        content_fail='__ALWAYS_FAIL__',
    
        class use_brief_types(EditGenerator):
            """
    
            Use less verbose unsigned types.
    
    
            Replace:
              unsigned int
            With:
              uint
            """
            @staticmethod
            def edit_list_from_file(_source: str, data: str, _shared_edit_data: Any) -> List[Edit]:
                edits = []
    
                # `unsigned char` -> `uchar`.
                for match in re.finditer(r"(unsigned)\s+([a-z]+)", data):
                    edits.append(Edit(
                        span=match.span(),
                        content='u%s' % match.group(2),
                        content_fail='__ALWAYS_FAIL__',
                    ))
    
                # There may be some remaining uses of `unsigned` without any integer type afterwards.
                # `unsigned` -> `uint`.
                for match in re.finditer(r"\bunsigned\b", data):
    
                    edits.append(Edit(
                        span=match.span(),
                        content='uint',
                        content_fail='__ALWAYS_FAIL__',
                    ))
    
                return edits
    
        class use_nullptr(EditGenerator):
            """
            Use ``nullptr`` instead of ``NULL`` for C++ code.
    
            Replace:
              NULL
            With:
              nullptr
            """
            @staticmethod
            def edit_list_from_file(source: str, data: str, _shared_edit_data: Any) -> List[Edit]:
                edits = []
    
                # The user might exclude C++, if they forget, it is better not to operate on C.
                if not source.lower().endswith((".h", ".c")):
                    return edits
    
                # `NULL` -> `nullptr`.
                for match in re.finditer(r"\bNULL\b", data):
                    edits.append(Edit(
                        span=match.span(),
                        content='nullptr',
                        content_fail='__ALWAYS_FAIL__',
                    ))
    
                # There may be some remaining uses of `unsigned` without any integer type afterwards.
                # `unsigned` -> `uint`.
                for match in re.finditer(r"\bunsigned\b", data):
    
                    edits.append(Edit(
                        span=match.span(),
                        content='uint',
                        content_fail='__ALWAYS_FAIL__',
    
                    ))
    
                return edits
    
        class unused_arg_as_comment(EditGenerator):
            """
            Replace `UNUSED(argument)` in C++ code.
    
            Replace:
              void function(int UNUSED(arg)) {...}
            With:
              void function(int /*arg*/) {...}
            """
            @staticmethod
            def edit_list_from_file(source: str, data: str, _shared_edit_data: Any) -> List[Edit]:
    
    
                # The user might exclude C++, if they forget, it is better not to operate on C.
                if not source.lower().endswith((".h", ".c")):
                    return edits
    
                # `UNUSED(arg)` -> `/*arg*/`.
                for match in re.finditer(
                        r"\b(UNUSED)"
                        # # Opening parenthesis.
                        r"\("
                        # Capture the identifier as group 1.
                        r"([" + "".join(list(IDENTIFIER_CHARS)) + "]+)"
                        # # Capture any non-identifier characters as group 2.
                        # (e.g. `[3]`) which need to be added outside the comment.
                        r"([^\)]*)"
                        # Closing parenthesis of `UNUSED(..)`.
                        r"\)",
                        data,
                ):
                    edits.append(Edit(
                        span=match.span(),
                        content='/*%s*/%s' % (match.group(2), match.group(3)),
                        content_fail='__ALWAYS_FAIL__(%s%s)' % (match.group(2), match.group(3)),
    
        class use_elem_macro(EditGenerator):
    
            """
            Use the `ELEM` macro for more abbreviated expressions.
    
            Replace:
              (a == b || a == c)
              (a != b && a != c)
            With:
              (ELEM(a, b, c))
              (!ELEM(a, b, c))
            """
    
            def edit_list_from_file(_source: str, data: str, _shared_edit_data: Any) -> List[Edit]:
    
                for use_brackets in (True, False):
    
                    test_equal = (
                        r'([^\|\(\)]+)'  # group 1 (no (|))
                        r'\s+==\s+'
                        r'([^\|\(\)]+)'  # group 2 (no (|))
                    )
    
                    test_not_equal = (
                        r'([^\|\(\)]+)'  # group 1 (no (|))
                        r'\s+!=\s+'
                        r'([^\|\(\)]+)'  # group 2 (no (|))
                    )
    
                    if use_brackets:
                        test_equal = r'\(' + test_equal + r'\)'
                        test_not_equal = r'\(' + test_not_equal + r'\)'
    
                    for is_equal in (True, False):
                        for n in reversed(range(2, 64)):
                            if is_equal:
                                re_str = r'\(' + r'\s+\|\|\s+'.join([test_equal] * n) + r'\)'
                            else:
                                re_str = r'\(' + r'\s+\&\&\s+'.join([test_not_equal] * n) + r'\)'
    
                            for match in re.finditer(re_str, data):
                                var = match.group(1)
                                var_rest = []
                                groups = match.groups()
                                groups_paired = [(groups[i * 2], groups[i * 2 + 1]) for i in range(len(groups) // 2)]
                                found = True
                                for a, b in groups_paired:
                                    # Unlikely but possible the checks are swapped.
                                    if b == var and a != var:
                                        a, b = b, a
    
                                    if a != var:
                                        found = False
                                        break
                                    var_rest.append(b)
    
                                if found:
                                    edits.append(Edit(
                                        span=match.span(),
                                        content='(%sELEM(%s, %s))' % (
                                            ('' if is_equal else '!'),
                                            var,
                                            ', '.join(var_rest),
                                        ),
                                        # Use same expression otherwise this can change values
                                        # inside assert when it shouldn't.
                                        content_fail='(%s__ALWAYS_FAIL__(%s, %s))' % (
                                            ('' if is_equal else '!'),
                                            var,
                                            ', '.join(var_rest),
                                        ),
                                    ))
    
    
                return edits
    
        class use_str_elem_macro(EditGenerator):
    
            """
            Use `STR_ELEM` macro:
    
            Replace:
              (STREQ(a, b) || STREQ(a, c))
            With:
              (STR_ELEM(a, b, c))
            """
    
            def edit_list_from_file(_source: str, data: str, _shared_edit_data: Any) -> List[Edit]:
    
                for use_brackets in (True, False):
    
                    test_equal = (
                        r'STREQ'
                        r'\('
                        r'([^\|\(\),]+)'  # group 1 (no (|,))
                        r',\s+'
                        r'([^\|\(\),]+)'  # group 2 (no (|,))
                        r'\)'
                    )
    
                    test_not_equal = (
                        '!'  # Only difference.
                        r'STREQ'
                        r'\('
                        r'([^\|\(\),]+)'  # group 1 (no (|,))
                        r',\s+'
                        r'([^\|\(\),]+)'  # group 2 (no (|,))
                        r'\)'
                    )
    
                    if use_brackets:
                        test_equal = r'\(' + test_equal + r'\)'
                        test_not_equal = r'\(' + test_not_equal + r'\)'
    
                    for is_equal in (True, False):
                        for n in reversed(range(2, 64)):
                            if is_equal:
                                re_str = r'\(' + r'\s+\|\|\s+'.join([test_equal] * n) + r'\)'
                            else:
                                re_str = r'\(' + r'\s+\&\&\s+'.join([test_not_equal] * n) + r'\)'
    
                            for match in re.finditer(re_str, data):
                                var = match.group(1)
                                var_rest = []
                                groups = match.groups()
                                groups_paired = [(groups[i * 2], groups[i * 2 + 1]) for i in range(len(groups) // 2)]
                                found = True
                                for a, b in groups_paired:
                                    # Unlikely but possible the checks are swapped.
                                    if b == var and a != var:
                                        a, b = b, a
    
                                    if a != var:
                                        found = False
                                        break
                                    var_rest.append(b)
    
                                if found:
                                    edits.append(Edit(
                                        span=match.span(),
                                        content='(%sSTR_ELEM(%s, %s))' % (
                                            ('' if is_equal else '!'),
                                            var,
                                            ', '.join(var_rest),
                                        ),
                                        # Use same expression otherwise this can change values
                                        # inside assert when it shouldn't.
                                        content_fail='(%s__ALWAYS_FAIL__(%s, %s))' % (
                                            ('' if is_equal else '!'),
                                            var,
                                            ', '.join(var_rest),
                                        ),
                                    ))
    
    
                return edits
    
        class use_const_vars(EditGenerator):
    
            """
            Use `const` where possible:
    
            Replace:
              float abc[3] = {0, 1, 2};
            With:
              const float abc[3] = {0, 1, 2};
            """
    
            def edit_list_from_file(_source: str, data: str, _shared_edit_data: Any) -> List[Edit]:
    
                edits = []
    
                # for match in re.finditer(r"(  [a-zA-Z0-9_]+ [a-zA-Z0-9_]+ = [A-Z][A-Z_0-9_]*;)", data):
    
                #     edits.append(Edit(
                #         span=match.span(),
                #         content='const %s' % (match.group(1).lstrip()),
                #         content_fail='__ALWAYS_FAIL__',
    
                #     ))
    
                for match in re.finditer(r"(  [a-zA-Z0-9_]+ [a-zA-Z0-9_]+ = .*;)", data):
    
                    edits.append(Edit(
                        span=match.span(),
                        content='const %s' % (match.group(1).lstrip()),
                        content_fail='__ALWAYS_FAIL__',
    
                    ))
    
                return edits
    
        class remove_return_parens(EditGenerator):
    
    Campbell Barton's avatar
    Campbell Barton committed
            Remove redundant parenthesis around return arguments:
    
            def edit_list_from_file(_source: str, data: str, _shared_edit_data: Any) -> List[Edit]:
    
                edits = []
    
                # Remove `return (NULL);`
                for match in re.finditer(r"return \(([a-zA-Z_0-9]+)\);", data):
    
                    edits.append(Edit(
                        span=match.span(),
                        content='return %s;' % (match.group(1)),
                        content_fail='return __ALWAYS_FAIL__;',
    
                    ))
                return edits
    
        class use_streq_macro(EditGenerator):
    
            """
            Use `STREQ` macro:
    
            Replace:
              strcmp(a, b) == 0
            With:
              STREQ(a, b)
    
            Replace:
              strcmp(a, b) != 0
            With:
              !STREQ(a, b)
            """
    
            def edit_list_from_file(_source: str, data: str, _shared_edit_data: Any) -> List[Edit]:
    
                # `strcmp(a, b) == 0` -> `STREQ(a, b)`
    
                for match in re.finditer(r"\bstrcmp\((.*)\) == 0", data):
    
                    edits.append(Edit(
                        span=match.span(),
                        content='STREQ(%s)' % (match.group(1)),
                        content_fail='__ALWAYS_FAIL__',
    
                    ))
                for match in re.finditer(r"!strcmp\((.*)\)", data):
    
                    edits.append(Edit(
                        span=match.span(),
                        content='STREQ(%s)' % (match.group(1)),
                        content_fail='__ALWAYS_FAIL__',
    
                # `strcmp(a, b) != 0` -> `!STREQ(a, b)`
    
                for match in re.finditer(r"\bstrcmp\((.*)\) != 0", data):
    
                    edits.append(Edit(
                        span=match.span(),
                        content='!STREQ(%s)' % (match.group(1)),
                        content_fail='__ALWAYS_FAIL__',
    
                    ))
                for match in re.finditer(r"\bstrcmp\((.*)\)", data):
    
                    edits.append(Edit(
                        span=match.span(),
                        content='!STREQ(%s)' % (match.group(1)),
                        content_fail='__ALWAYS_FAIL__',
    
                    ))
    
                return edits
    
        class use_array_size_macro(EditGenerator):
    
            """
            Use macro for an error checked array size:
    
            Replace:
              sizeof(foo) / sizeof(*foo)
            With:
              ARRAY_SIZE(foo)
            """
    
            def edit_list_from_file(_source: str, data: str, _shared_edit_data: Any) -> List[Edit]:
    
                edits = []
                # Note that this replacement is only valid in some cases,
                # so only apply with validation that binary output matches.
                for match in re.finditer(r"\bsizeof\((.*)\) / sizeof\([^\)]+\)", data):
    
                    edits.append(Edit(
                        span=match.span(),
                        content='ARRAY_SIZE(%s)' % match.group(1),
                        content_fail='__ALWAYS_FAIL__',
    
        class parenthesis_cleanup(EditGenerator):
            """
            Use macro for an error checked array size:
    
            Replace:
              ((a + b))
            With:
              (a + b)
    
    
            Replace:
              (func(a + b))
            With:
              func(a + b)
    
            Note that the `CFLAGS` should be set so missing parentheses that contain assignments - error instead of warn:
            With GCC: `-Werror=parentheses`
    
            """
            @staticmethod
            def edit_list_from_file(_source: str, data: str, _shared_edit_data: Any) -> List[Edit]:
                edits = []
    
    
                # Give up after searching for a bracket this many characters and finding none.
                bracket_seek_limit = 4000
    
                # Don't match double brackets because this will not match multiple overlapping matches
                # Where 3 brackets should be checked as two separate pairs.
                for match in re.finditer(r"(\()", data):
                    outer_beg = match.span()[0]
                    inner_beg = outer_beg + 1
                    if data[inner_beg] != "(":
                        continue
    
                    inner_end = text_matching_bracket_forward(data, inner_beg, inner_beg + bracket_seek_limit, "(", ")")
    
                    if inner_end == -1:
                        continue
                    outer_beg = inner_beg - 1
    
                    outer_end = text_matching_bracket_forward(data, outer_beg, inner_end + 1, "(", ")")
    
                    if outer_end != inner_end + 1:
                        continue
    
                    text = data[inner_beg:inner_end + 1]
                    edits.append(Edit(
                        span=(outer_beg, outer_end + 1),
                        content=text,
                        content_fail='(__ALWAYS_FAIL__)',
                    ))
    
    
                # Handle `(func(a + b))` -> `func(a + b)`
                for match in re.finditer(r"(\))", data):
                    inner_end = match.span()[0]
                    outer_end = inner_end + 1
                    if data[outer_end] != ")":
                        continue
    
                    inner_beg = text_matching_bracket_backward(data, inner_end, inner_end - bracket_seek_limit, "(", ")")
                    if inner_beg == -1:
                        continue
                    outer_beg = text_matching_bracket_backward(data, outer_end, outer_end - bracket_seek_limit, "(", ")")
                    if outer_beg == -1:
                        continue
    
                    # The text between the first two opening brackets:
                    # `(function_name(a + b))` -> `function_name`.
                    text = data[outer_beg + 1:inner_beg]
    
                    # Handled in the first loop looking for forward brackets.
                    if text == "":
                        continue
    
                    # Don't convert `prefix(func(a + b))` -> `prefixfunc(a + b)`
                    if data[outer_beg - 1] in IDENTIFIER_CHARS:
                        continue
    
                    # Don't convert `static_cast<float>(foo(bar))` -> `static_cast<float>foo(bar)`
                    # While this will always fail to compile it slows down tests.
                    if data[outer_beg - 1] == ">":
                        continue
    
                    # Exact rule here is arbitrary, in general though spaces mean there are operations
                    # that can use the brackets.
                    if " " in text:
                        continue
    
                    # Search back an arbitrary number of chars 8 should be enough
                    # but manual formatting can add additional white-space, so increase
                    # the size to account for that.
                    prefix = data[max(outer_beg - 20, 0):outer_beg].strip()
                    if prefix:
                        # Avoid `if (SOME_MACRO(..)) {..}` -> `if SOME_MACRO(..) {..}`
                        # While correct it relies on parenthesis within the macro which isn't ideal.
                        if prefix.split()[-1] in {"if", "while", "switch"}:
                            continue
                        # Avoid `*(--foo)` -> `*--foo`.
                        # While correct it reads badly.
                        if data[outer_beg - 1] == "*":
                            continue
    
                    text_no_parens = data[outer_beg + 1: outer_end]
    
                    edits.append(Edit(
                        span=(outer_beg, outer_end + 1),
                        content=text_no_parens,
                        content_fail='__ALWAYS_FAIL__',
                    ))
    
    
        class header_clean(EditGenerator):
            """
            Clean headers, ensuring that the headers removed are not used directly or indirectly.
    
            Note that the `CFLAGS` should be set so missing prototypes error instead of warn:
            With GCC: `-Werror=missing-prototypes`
            """
    
            @staticmethod
    
            def _header_guard_from_filename(f: str) -> str:
    
                return '__%s__' % os.path.basename(f).replace('.', '_').upper()
    
            @classmethod