scripts: Fix to not skip some option parameters for rpi4 fusing script
[platform/kernel/u-boot.git] / scripts / style.py
1 #!/usr/bin/env python3
2 # SPDX-License-Identifier: GPL-2.0+
3 #
4 # Copyright 2021 Google LLC
5 #
6
7 """Changes the functions and class methods in a file to use snake case, updating
8 other tools which use them"""
9
10 from argparse import ArgumentParser
11 import glob
12 import os
13 import re
14 import subprocess
15
16 import camel_case
17
18 # Exclude functions with these names
19 EXCLUDE_NAMES = set(['setUp', 'tearDown', 'setUpClass', 'tearDownClass'])
20
21 # Find function definitions in a file
22 RE_FUNC = re.compile(r' *def (\w+)\(')
23
24 # Where to find files that might call the file being converted
25 FILES_GLOB = 'tools/**/*.py'
26
27 def collect_funcs(fname):
28     """Collect a list of functions in a file
29
30     Args:
31         fname (str): Filename to read
32
33     Returns:
34         tuple:
35             str: contents of file
36             list of str: List of function names
37     """
38     with open(fname, encoding='utf-8') as inf:
39         data = inf.read()
40         funcs = RE_FUNC.findall(data)
41     return data, funcs
42
43 def get_module_name(fname):
44     """Convert a filename to a module name
45
46     Args:
47         fname (str): Filename to convert, e.g. 'tools/patman/command.py'
48
49     Returns:
50         tuple:
51             str: Full module name, e.g. 'patman.command'
52             str: Leaf module name, e.g. 'command'
53             str: Program name, e.g. 'patman'
54     """
55     parts = os.path.splitext(fname)[0].split('/')[1:]
56     module_name = '.'.join(parts)
57     return module_name, parts[-1], parts[0]
58
59 def process_caller(data, conv, module_name, leaf):
60     """Process a file that might call another module
61
62     This converts all the camel-case references in the provided file contents
63     with the corresponding snake-case references.
64
65     Args:
66         data (str): Contents of file to convert
67         conv (dict): Identifies to convert
68             key: Current name in camel case, e.g. 'DoIt'
69             value: New name in snake case, e.g. 'do_it'
70         module_name: Name of module as referenced by the file, e.g.
71             'patman.command'
72         leaf: Leaf module name, e.g. 'command'
73
74     Returns:
75         str: New file contents, or None if it was not modified
76     """
77     total = 0
78
79     # Update any simple functions calls into the module
80     for name, new_name in conv.items():
81         newdata, count = re.subn(fr'{leaf}.{name}\(',
82                                  f'{leaf}.{new_name}(', data)
83         total += count
84         data = newdata
85
86     # Deal with files that import symbols individually
87     imports = re.findall(fr'from {module_name} import (.*)\n', data)
88     for item in imports:
89         #print('item', item)
90         names = [n.strip() for n in item.split(',')]
91         new_names = [conv.get(n) or n for n in names]
92         new_line = f"from {module_name} import {', '.join(new_names)}\n"
93         data = re.sub(fr'from {module_name} import (.*)\n', new_line, data)
94         for name in names:
95             new_name = conv.get(name)
96             if new_name:
97                 newdata = re.sub(fr'\b{name}\(', f'{new_name}(', data)
98                 data = newdata
99
100     # Deal with mocks like:
101     # unittest.mock.patch.object(module, 'Function', ...
102     for name, new_name in conv.items():
103         newdata, count = re.subn(fr"{leaf}, '{name}'",
104                                  f"{leaf}, '{new_name}'", data)
105         total += count
106         data = newdata
107
108     if total or imports:
109         return data
110     return None
111
112 def process_file(srcfile, do_write, commit):
113     """Process a file to rename its camel-case functions
114
115     This renames the class methods and functions in a file so that they use
116     snake case. Then it updates other modules that call those functions.
117
118     Args:
119         srcfile (str): Filename to process
120         do_write (bool): True to write back to files, False to do a dry run
121         commit (bool): True to create a commit with the changes
122     """
123     data, funcs = collect_funcs(srcfile)
124     module_name, leaf, prog = get_module_name(srcfile)
125     #print('module_name', module_name)
126     #print(len(funcs))
127     #print(funcs[0])
128     conv = {}
129     for name in funcs:
130         if name not in EXCLUDE_NAMES:
131             conv[name] = camel_case.to_snake(name)
132
133     # Convert name to new_name in the file
134     for name, new_name in conv.items():
135         #print(name, new_name)
136         # Don't match if it is preceded by a '.', since that indicates that
137         # it is calling this same function name but in a different module
138         newdata = re.sub(fr'(?<!\.){name}\(', f'{new_name}(', data)
139         data = newdata
140
141         # But do allow self.xxx
142         newdata = re.sub(fr'self.{name}\(', f'self.{new_name}(', data)
143         data = newdata
144     if do_write:
145         with open(srcfile, 'w', encoding='utf-8') as out:
146             out.write(data)
147
148     # Now find all files which use these functions and update them
149     for fname in glob.glob(FILES_GLOB, recursive=True):
150         with open(fname, encoding='utf-8') as inf:
151             data = inf.read()
152         newdata = process_caller(fname, conv, module_name, leaf)
153         if do_write and newdata:
154             with open(fname, 'w', encoding='utf-8') as out:
155                 out.write(newdata)
156
157     if commit:
158         subprocess.call(['git', 'add', '-u'])
159         subprocess.call([
160             'git', 'commit', '-s', '-m',
161             f'''{prog}: Convert camel case in {os.path.basename(srcfile)}
162
163 Convert this file to snake case and update all files which use it.
164 '''])
165
166
167 def main():
168     """Main program"""
169     epilog = 'Convert camel case function names to snake in a file and callers'
170     parser = ArgumentParser(epilog=epilog)
171     parser.add_argument('-c', '--commit', action='store_true',
172                         help='Add a commit with the changes')
173     parser.add_argument('-n', '--dry_run', action='store_true',
174                         help='Dry run, do not write back to files')
175     parser.add_argument('-s', '--srcfile', type=str, required=True, help='Filename to convert')
176     args = parser.parse_args()
177     process_file(args.srcfile, not args.dry_run, args.commit)
178
179 if __name__ == '__main__':
180     main()