1bf215546Sopenharmony_ci#!/usr/bin/env python3
2bf215546Sopenharmony_ci"""
3bf215546Sopenharmony_ciCheck for and replace aliases with their new names from vk.xml
4bf215546Sopenharmony_ci"""
5bf215546Sopenharmony_ci
6bf215546Sopenharmony_ciimport argparse
7bf215546Sopenharmony_ciimport pathlib
8bf215546Sopenharmony_ciimport subprocess
9bf215546Sopenharmony_ciimport sys
10bf215546Sopenharmony_ciimport xml.etree.ElementTree as et
11bf215546Sopenharmony_ci
12bf215546Sopenharmony_ciTHIS_FILE = pathlib.Path(__file__)
13bf215546Sopenharmony_ciCWD = pathlib.Path.cwd()
14bf215546Sopenharmony_ci
15bf215546Sopenharmony_ciVK_XML = THIS_FILE.parent / 'vk.xml'
16bf215546Sopenharmony_ciEXCLUDE_PATHS = [
17bf215546Sopenharmony_ci    VK_XML.relative_to(CWD).as_posix(),
18bf215546Sopenharmony_ci
19bf215546Sopenharmony_ci    # These files come from other repos, there's no point checking and
20bf215546Sopenharmony_ci    # fixing them here as that would be overwritten in the next sync.
21bf215546Sopenharmony_ci    'src/amd/vulkan/radix_sort/',
22bf215546Sopenharmony_ci    'src/virtio/venus-protocol/',
23bf215546Sopenharmony_ci]
24bf215546Sopenharmony_ci
25bf215546Sopenharmony_ci
26bf215546Sopenharmony_cidef get_aliases(xml_file: pathlib.Path):
27bf215546Sopenharmony_ci    """
28bf215546Sopenharmony_ci    Get all the aliases defined in vk.xml
29bf215546Sopenharmony_ci    """
30bf215546Sopenharmony_ci    xml = et.parse(xml_file)
31bf215546Sopenharmony_ci
32bf215546Sopenharmony_ci    for node in ([]
33bf215546Sopenharmony_ci        + xml.findall('.//enum[@alias]')
34bf215546Sopenharmony_ci        + xml.findall('.//type[@alias]')
35bf215546Sopenharmony_ci        + xml.findall('.//command[@alias]')
36bf215546Sopenharmony_ci    ):
37bf215546Sopenharmony_ci        yield node.attrib['name'], node.attrib['alias']
38bf215546Sopenharmony_ci
39bf215546Sopenharmony_ci
40bf215546Sopenharmony_cidef remove_prefix(string: str, prefix: str):
41bf215546Sopenharmony_ci    """
42bf215546Sopenharmony_ci    Remove prefix if string starts with it, and return the full string
43bf215546Sopenharmony_ci    otherwise.
44bf215546Sopenharmony_ci    """
45bf215546Sopenharmony_ci    if not string.startswith(prefix):
46bf215546Sopenharmony_ci        return string
47bf215546Sopenharmony_ci    return string[len(prefix):]
48bf215546Sopenharmony_ci
49bf215546Sopenharmony_ci
50bf215546Sopenharmony_ci# Function from https://stackoverflow.com/a/312464
51bf215546Sopenharmony_cidef chunks(lst: list, n: int):
52bf215546Sopenharmony_ci    """
53bf215546Sopenharmony_ci    Yield successive n-sized chunks from lst.
54bf215546Sopenharmony_ci    """
55bf215546Sopenharmony_ci    for i in range(0, len(lst), n):
56bf215546Sopenharmony_ci        yield lst[i:i + n]
57bf215546Sopenharmony_ci
58bf215546Sopenharmony_ci
59bf215546Sopenharmony_cidef main(check_only: bool):
60bf215546Sopenharmony_ci    """
61bf215546Sopenharmony_ci    Entrypoint; perform the search for all the aliases, and if `check_only`
62bf215546Sopenharmony_ci    is not True, replace them.
63bf215546Sopenharmony_ci    """
64bf215546Sopenharmony_ci    def prepare_identifier(identifier: str) -> str:
65bf215546Sopenharmony_ci        # vk_find_struct() prepends `VK_STRUCTURE_TYPE_`, so that prefix
66bf215546Sopenharmony_ci        # might not appear in the code
67bf215546Sopenharmony_ci        identifier = remove_prefix(identifier, 'VK_STRUCTURE_TYPE_')
68bf215546Sopenharmony_ci        return identifier
69bf215546Sopenharmony_ci
70bf215546Sopenharmony_ci    aliases = {}
71bf215546Sopenharmony_ci    for old_name, alias_for in get_aliases(VK_XML):
72bf215546Sopenharmony_ci        old_name = prepare_identifier(old_name)
73bf215546Sopenharmony_ci        alias_for = prepare_identifier(alias_for)
74bf215546Sopenharmony_ci        aliases[old_name] = alias_for
75bf215546Sopenharmony_ci
76bf215546Sopenharmony_ci    print(f'Found {len(aliases)} aliases in {VK_XML.name}')
77bf215546Sopenharmony_ci
78bf215546Sopenharmony_ci    # Some aliases have aliases
79bf215546Sopenharmony_ci    recursion_needs_checking = True
80bf215546Sopenharmony_ci    while recursion_needs_checking:
81bf215546Sopenharmony_ci        recursion_needs_checking = False
82bf215546Sopenharmony_ci        for old, new in aliases.items():
83bf215546Sopenharmony_ci            if new in aliases:
84bf215546Sopenharmony_ci                aliases[old] = aliases[new]
85bf215546Sopenharmony_ci                recursion_needs_checking = True
86bf215546Sopenharmony_ci
87bf215546Sopenharmony_ci    # Doing the whole search in a single command breaks grep, so only
88bf215546Sopenharmony_ci    # look for 500 aliases at a time. Searching them one at a time would
89bf215546Sopenharmony_ci    # be extremely slow.
90bf215546Sopenharmony_ci    files_with_aliases = set()
91bf215546Sopenharmony_ci    for aliases_chunk in chunks([*aliases], 500):
92bf215546Sopenharmony_ci        search_output = subprocess.check_output([
93bf215546Sopenharmony_ci            'git',
94bf215546Sopenharmony_ci            'grep',
95bf215546Sopenharmony_ci            '-rlP',
96bf215546Sopenharmony_ci            '|'.join(aliases_chunk),
97bf215546Sopenharmony_ci            'src/'
98bf215546Sopenharmony_ci        ], stderr=subprocess.DEVNULL).decode()
99bf215546Sopenharmony_ci        files_with_aliases.update(search_output.splitlines())
100bf215546Sopenharmony_ci
101bf215546Sopenharmony_ci    def file_matches_path(file: str, path: str) -> bool:
102bf215546Sopenharmony_ci        # if path is a folder; match any file within
103bf215546Sopenharmony_ci        if path.endswith('/') and file.startswith(path):
104bf215546Sopenharmony_ci            return True
105bf215546Sopenharmony_ci        return file == path
106bf215546Sopenharmony_ci
107bf215546Sopenharmony_ci    for excluded_path in EXCLUDE_PATHS:
108bf215546Sopenharmony_ci        files_with_aliases = {
109bf215546Sopenharmony_ci            file for file in files_with_aliases
110bf215546Sopenharmony_ci            if not file_matches_path(file, excluded_path)
111bf215546Sopenharmony_ci        }
112bf215546Sopenharmony_ci
113bf215546Sopenharmony_ci    if not files_with_aliases:
114bf215546Sopenharmony_ci        print('No alias found in any file.')
115bf215546Sopenharmony_ci        sys.exit(0)
116bf215546Sopenharmony_ci
117bf215546Sopenharmony_ci    print(f'{len(files_with_aliases)} files contain aliases:')
118bf215546Sopenharmony_ci    print('\n'.join(f'- {file}' for file in files_with_aliases))
119bf215546Sopenharmony_ci
120bf215546Sopenharmony_ci    if check_only:
121bf215546Sopenharmony_ci        print('You can automatically fix this by running '
122bf215546Sopenharmony_ci              f'`{THIS_FILE.relative_to(CWD)}`.')
123bf215546Sopenharmony_ci        sys.exit(1)
124bf215546Sopenharmony_ci
125bf215546Sopenharmony_ci    command = [
126bf215546Sopenharmony_ci        'sed',
127bf215546Sopenharmony_ci        '-i',
128bf215546Sopenharmony_ci        ";".join([f's/{old}/{new}/g' for old, new in aliases.items()]),
129bf215546Sopenharmony_ci    ]
130bf215546Sopenharmony_ci    command += files_with_aliases
131bf215546Sopenharmony_ci    subprocess.check_call(command, stderr=subprocess.DEVNULL)
132bf215546Sopenharmony_ci    print('All aliases have been replaced')
133bf215546Sopenharmony_ci
134bf215546Sopenharmony_ci
135bf215546Sopenharmony_ciif __name__ == '__main__':
136bf215546Sopenharmony_ci    parser = argparse.ArgumentParser()
137bf215546Sopenharmony_ci    parser.add_argument('--check-only',
138bf215546Sopenharmony_ci                        action='store_true',
139bf215546Sopenharmony_ci                        help='Replace aliases found')
140bf215546Sopenharmony_ci    args = parser.parse_args()
141bf215546Sopenharmony_ci    main(**vars(args))
142