Reviewers should probably either review the entire result of running the scripted diff or review the script itself thoroughly.
Made an attempt at reviewing the script. Leaned on mypy contrib/devtools/reg-settings.py --check-untyped-defs
and ruff check
to help elucidate types.
0diff --git a/contrib/devtools/reg-settings.py b/contrib/devtools/reg-settings.py
1index f006fd6b1f..40d5ff07af 100644
2--- a/contrib/devtools/reg-settings.py
3+++ b/contrib/devtools/reg-settings.py
4@@ -37,7 +37,7 @@ DefaultValue = str | bool | None
5 class SettingType:
6 name: str
7 primary: bool = False
8- defaults: set[str | None] = field(default_factory=set)
9+ defaults: set[str | bool | None] = field(default_factory=set)
10 default_value: DefaultValue = False
11 includes: set[str] = field(default_factory=set)
12 """Include paths needed in setting file for default expressions"""
13@@ -90,22 +90,26 @@ class Setting: [@dataclass](/bitcoin-bitcoin/contributor/dataclass/)
14 class Range:
15 start: int
16- end: int
17+ end: int | None
18
19-def get_files_with_args(src_dir):
20+def get_files_with_args(src_dir: str) -> list[str]:
21 # Run git grep to find files containing AddArg/GetArg/GetIntArg/GetBoolArg/GetArgs
22 result = subprocess.run(
23 [
24- "git", "grep", "-l", "AddArg(\|AddHiddenArgs(\|GetArg(\|GetPathArg(\|GetIntArg(\|GetBoolArg(\|GetArgs(\|IsArgSet(\|IsArgNegated(", "--", src_dir
25+ "git", "grep", "-l", r"AddArg(\|AddHiddenArgs(\|GetArg(\|GetPathArg(\|GetIntArg(\|GetBoolArg(\|GetArgs(\|IsArgSet(\|IsArgNegated(", "--", src_dir
26 ],
27 capture_output=True,
28 text=True
29 )
30 return result.stdout.splitlines()
31
32-def parse_function_args(arg_str):
33- args = []
34- stack = []
35+# Converts something like:
36+# '("-foo", strprintf("helptext %i", DEFAULT), ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::OPTIONS)'
37+# into:
38+# (125, ['"-foo"', ' strprintf("helptext %i", DEFAULT', ' ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION', ' OptionsCategory::OPTIONS'])
39+def parse_function_args(arg_str: str) -> tuple[int, list[str]]:
40+ args: list[str] = []
41+ stack: list[str] = []
42 quot = False
43 for pos, c in enumerate(arg_str):
44 if c == '"':
45@@ -128,26 +132,28 @@ def parse_function_args(arg_str):
46 args[-1] += c
47 return pos, args
48
49-def parse_calls(file_path):
50- adds = []
51- gets = []
52+def parse_calls(file_path: str) -> tuple[list[AddArg], list[GetArg]]:
53+ adds: list[AddArg] = []
54+ gets: list[GetArg] = []
55 context = get_file_context(file_path)
56 namespace = get_file_namespace(file_path)
57 with open(file_path, 'r') as f:
58 content = f.read()
59 for match in re.finditer(r'\b(\w+)\.AddArg\((")', content):
60 call_len, (summary, help_text, flags, category) = parse_function_args(content[match.start(2)-1:])
61+ arg_name_match = re.match(r'"([^"=(]+).*', summary)
62+ assert arg_name_match
63 call = Call(
64 file=file_path,
65 position=match.start(),
66 call_text=content[match.start():match.start(2)+call_len],
67 obj_name=match.group(1),
68- arg_name=re.match(r'"([^"=(]+).*', summary).group(1),
69+ arg_name=arg_name_match.group(1),
70 context=context,
71 namespace=namespace,
72 )
73 help_text=help_text.strip()
74- help_args = []
75+ help_args: list[str] = []
76 if m := re.match(r"strprintf\(", help_text):
77 _, help_args = parse_function_args(help_text[m.end()-1:])
78 help_text = help_args[0].strip()
79@@ -162,17 +168,18 @@ def parse_calls(file_path):
80 ))
81 for match in re.finditer(r'( *)\b(\w+)\.AddHiddenArgs\((\{)', content):
82 call_len, call_args = parse_function_args(content[match.start(3):])
83- hidden_args = []
84+ hidden_args: list[AddArg] = []
85 for summary in call_args:
86 summary = summary.strip()
87 if not summary: continue
88- call_text=content[match.start():match.start(3)+call_len+1]
89+ arg_name_match = re.match(r'"([^"=(]+).*', summary)
90+ assert arg_name_match
91 call = Call(
92 file=file_path,
93 position=match.start(),
94 call_text=content[match.start():match.start(3)+call_len+2],
95 obj_name=match.group(2),
96- arg_name=re.match(r'"([^"=(]+).*', summary).group(1),
97+ arg_name=arg_name_match.group(1),
98 context=context,
99 namespace=namespace,
100 )
101@@ -215,6 +222,7 @@ def parse_calls(file_path):
102 DataType.DISABLED if function_name == "IsArgNegated" else
103 DataType.UNSET if function_name == "IsArgSet" else
104 None)
105+ assert data_type
106 default_arg = call_args[1].strip() if len(call_args) > 1 else None
107 default_value = (
108 True if data_type == DataType.STRING and default_arg == '""' else
109@@ -227,7 +235,7 @@ def parse_calls(file_path):
110 gets.append(GetArg(call=call, function_name=function_name, data_type=data_type, default_value=default_value, nots=len(match.group(1))))
111 return adds, gets
112
113-def make_setting(settings, call):
114+def make_setting(settings: dict[str, Setting], call: Call) -> Setting:
115 name = call.arg_name.lstrip("-")
116 if name in settings:
117 setting = settings[name]
118@@ -235,7 +243,7 @@ def make_setting(settings, call):
119 setting = settings[name] = Setting(name)
120 return setting
121
122-def flags_to_options(flag_str):
123+def flags_to_options(flag_str: str) -> list[str]:
124 flags = set()
125 for flag in flag_str.split("|"):
126 flag = flag.strip()
127@@ -263,9 +271,9 @@ def flags_to_options(flag_str):
128 raise Exception(f"Unknown flags {flags!r}")
129 return options
130
131-def collect_argument_information(src_dir):
132+def collect_argument_information(src_dir: str) -> dict[str, Setting]:
133 files = get_files_with_args(src_dir)
134- settings: Dict[str, Setting] = {}
135+ settings: dict[str, Setting] = {}
136 for file in files:
137 adds, gets = parse_calls(file)
138 for add in adds:
139@@ -290,7 +298,7 @@ def collect_argument_information(src_dir):
140 for get in setting.gets:
141 if get.add is None and same_context(add, get):
142 get.add = add
143- fake_adds = {} # map of (file path, arg name) -> AddArg
144+ fake_adds: dict[tuple[str, str], AddArg] = {} # map of (file path, arg name) -> AddArg
145 for get in setting.gets:
146 if get.add is None and get.call.arg_name:
147 key = get.call.file, get.call.arg_name
148@@ -303,7 +311,7 @@ def collect_argument_information(src_dir):
149
150 # Figure out setting types and default values.
151 setting_name = ''.join(NAME_MAP.get(word) or word.capitalize() for word in arg_name.split('-')) + "Setting"
152- counter = collections.Counter()
153+ counter: collections.Counter = collections.Counter()
154 for add in setting.adds:
155 add.include_path = add.call.file.replace(".cpp", "_settings.h")
156 add.rel_include_path = re.sub("^src/", "", add.include_path)
157@@ -387,7 +395,7 @@ def collect_argument_information(src_dir):
158
159 return settings
160
161-def setting_definitions(add):
162+def setting_definitions(add: AddArg) -> tuple[list[str], set[str]]:
163 defs = []
164 includes = set()
165 # If this is hidden argument and there is non-hidden argument with the same
166@@ -405,6 +413,7 @@ def setting_definitions(add):
167 "common::Disabled" if data_type == DataType.DISABLED else
168 "common::Unset" if data_type == DataType.UNSET else
169 None)
170+ assert ctype
171 if None in setting_type.defaults:
172 ctype = f"std::optional<{ctype}>"
173 help_str = ""
174@@ -447,26 +456,28 @@ def setting_definitions(add):
175 defs.append(f"\nusing {setting_type.name} = common::Setting<\n {add.summary}, {ctype}, {options_str}{help_str}>{extra};\n")
176 return defs, includes
177
178-def addarg_replacement(add):
179+def addarg_replacement(add: AddArg) -> str:
180 new_call = ""
181 if add.hidden_args is None:
182 new_call = register_str(add)
183 else:
184 for hadd in add.hidden_args:
185 if new_call: new_call += ";\n"
186- spaces=re.match(" *", hadd.call.call_text).group()
187+ hidden_match = re.match(" *", hadd.call.call_text)
188+ assert hidden_match
189+ spaces=hidden_match.group()
190 new_call += f"{spaces}{register_str(hadd.nonhidden_arg or hadd, hidden=hadd.nonhidden_arg is not None)}"
191 return new_call
192
193-def arg_name(add):
194+def arg_name(add: AddArg) -> str:
195 default_data_type = min(add.data_types.keys())
196 return add.data_types[default_data_type].name
197
198-def register_str(add, hidden=False):
199+def register_str(add: AddArg, hidden: bool=False) -> str:
200 register_args = ", ".join([add.call.obj_name] + add.extern_args)
201 return f"{arg_name(add)}{'::Hidden' if hidden else ''}::Register({register_args})"
202
203-def getarg_replacement(get):
204+def getarg_replacement(get: GetArg) -> str:
205 if get.data_type == DataType.UNSET:
206 method = "Value"
207 suffix = ".isNull()"
208@@ -476,6 +487,7 @@ def getarg_replacement(get):
209 else:
210 method = "Get"
211 suffix = ""
212+ assert get.add
213 setting_type = get.add.data_types.get(get.data_type) or get.add.data_types[min(get.add.data_types.keys())]
214 default_arg = ""
215 if get.default_value and not setting_type.default_value:
216@@ -490,7 +502,7 @@ def getarg_replacement(get):
217 new_call += f"{setting_type.name}::{method}({get.call.obj_name}{default_arg}){suffix}"
218 return new_call
219
220-def add_to_file(file_path, local_include, system_include=(), defs=()):
221+def add_to_file(file_path: str, local_include: list[str], system_include: list[str]=[], defs: list[str]=[]):
222 if os.path.exists(file_path):
223 with open(file_path, 'r') as f:
224 lines = f.readlines()
225@@ -510,17 +522,16 @@ def add_to_file(file_path, local_include, system_include=(), defs=()):
226 lines.extend(["\n", f"#endif // {guard}\n"])
227
228 # Identify include blocks and their positions
229- blocks = []
230+ blocks: list[Range] = []
231 self_include = f"#include <{file_path.replace('src/', '').replace('.cpp', '.h')}>"
232- first = last = self = None
233+ first = last = None
234 for i, line in enumerate(lines):
235 if line.startswith('#include') and "IWYU pragma: keep" not in line and not line.startswith(self_include):
236 if not blocks or blocks[-1].end is not None:
237 blocks.append(Range(i, None))
238 elif blocks and blocks[-1].end is None:
239 blocks[-1].end = i
240- elif line.startswith('#include'):
241- self = True
242+
243 if first is None and not line.startswith("//") and not line.startswith("#ifndef") and not line.startswith("#define") and line != "\n":
244 first = i
245 if line != "\n" and not line.startswith("#endif") and not line.startswith("} // namespace "):
246@@ -528,6 +539,8 @@ def add_to_file(file_path, local_include, system_include=(), defs=()):
247
248 if len(blocks) == 0:
249 # If there are no include blocks, add an empty one where includes should go.
250+ assert first
251+ assert last
252 m = min(first, last)
253 blocks.append(Range(m, m))
254 if len(blocks) == 1:
255@@ -535,6 +548,7 @@ def add_to_file(file_path, local_include, system_include=(), defs=()):
256 # local or system includes, but reasonably heuristic is to assume it
257 # contains local includes in .cpp files and system includes in .h files.
258 if file_path.endswith(".cpp"):
259+ assert blocks[0].end
260 blocks.append(Range(blocks[0].end, blocks[0].end))
261 else:
262 blocks.insert(0, Range(blocks[0].start, blocks[0].start))
263@@ -558,7 +572,7 @@ def add_to_file(file_path, local_include, system_include=(), defs=()):
264 with open(file_path, 'w') as f:
265 f.writelines(lines)
266
267-def replace_in_file(file_path, old, new, replacements=None):
268+def replace_in_file(file_path: str, old: str, new: str, replacements: list[tuple[str, str]] | None=None):
269 with open(file_path, 'r') as f:
270 content = f.read()
271 if replacements is not None:
272@@ -572,29 +586,33 @@ def replace_in_file(file_path, old, new, replacements=None):
273 with open(file_path, 'w') as f:
274 f.write(new_content)
275
276-def modify_source_files(settings, git_commit=False):
277+def modify_source_files(settings: dict[str, Setting], git_commit: bool=False):
278 # map file path->list of (old, new) tuples with GetArg->Get replacements made so far
279- replacements = collections.defaultdict(list)
280+ replacements: dict[str, list[tuple[str, str]]] = collections.defaultdict(list)
281
282 for setting in settings.values():
283 for add in setting.adds:
284 if not add.fake:
285 replace_in_file(add.call.file, add.call.call_text, addarg_replacement(add))
286+ assert add.rel_include_path
287 add_to_file(add.call.file, [add.rel_include_path])
288
289 for get in setting.gets:
290 if get.add is add:
291 replace_in_file(get.call.file, get.call.call_text, getarg_replacement(get),
292 replacements[get.call.file])
293+ assert get.add.rel_include_path
294 add_to_file(get.call.file, [get.add.rel_include_path])
295
296 defs, def_includes = setting_definitions(add)
297 if defs:
298+ assert add.include_path
299 add_to_file(add.include_path,
300 [include for include in def_includes | {"common/setting.h"}],
301 ["string", "vector"], defs)
302
303 if git_commit and subprocess.run(["git", "status", "--porcelain", "--untracked-files=no"], stdout=subprocess.PIPE, check=True).stdout:
304+ assert add.include_path
305 subprocess.run(["git", "add", "-N", add.include_path], check=True)
306 msg = f"{add.rel_include_path}: Add {arg_name(add.nonhidden_arg or add)}"
307 subprocess.run(["git", "commit", "-am", msg], check=True)
308@@ -644,7 +662,6 @@ ARG_PATTERNS = {
309 "DEFAULT_ASMAP_FILENAME": PatternOptions(include_path="init.h", runtime=True),
310 "DEFAULT_AVOIDPARTIALSPENDS": PatternOptions(include_path="wallet/coincontrol.h", runtime=True),
311 "DEFAULT_BENCH_FILTER": PatternOptions(runtime=True),
312- "DEFAULT_BLOCKFILTERINDEX": PatternOptions(include_path="index/blockfilterindex.h"),
313 "DEFAULT_BLOCKFILTERINDEX": PatternOptions(include_path="index/blockfilterindex.h", runtime=True),
314 "DEFAULT_BLOCK_RECONSTRUCTION_EXTRA_TXN": PatternOptions(include_path="net_processing.h"),
315 "DEFAULT_CHOOSE_DATADIR": PatternOptions(include_path="qt/intro.h"),