8
8
9
9
import difflib
10
10
import json
11
+ import re
11
12
import subprocess as sp
12
13
import sys
13
14
from dataclasses import dataclass
14
- from glob import glob
15
+ from glob import glob , iglob
15
16
from pathlib import Path
16
- from typing import Any , TypeAlias
17
+ from typing import Any , Callable , TypeAlias
17
18
18
- ETC_DIR = Path (__file__ ).parent
19
+ SELF_PATH = Path (__file__ )
20
+ ETC_DIR = SELF_PATH .parent
19
21
ROOT_DIR = ETC_DIR .parent
20
22
23
+ # Loose approximation of what gets checked in to git, without needing `git ls-files`.
24
+ DIRECTORIES = [".github" , "ci" , "crates" , "etc" , "src" ]
25
+
21
26
# These files do not trigger a retest.
22
27
IGNORED_SOURCES = ["src/libm_helper.rs" ]
23
28
24
29
IndexTy : TypeAlias = dict [str , dict [str , Any ]]
25
30
"""Type of the `index` item in rustdoc's JSON output"""
26
31
27
32
33
+ def eprint (* args , ** kwargs ):
34
+ """Print to stderr."""
35
+ print (* args , file = sys .stderr , ** kwargs )
36
+
37
+
28
38
@dataclass
29
39
class Crate :
30
40
"""Representation of public interfaces and function defintion locations in
@@ -146,7 +156,7 @@ def write_function_list(self, check: bool) -> None:
146
156
if check :
147
157
with open (out_file , "r" ) as f :
148
158
current = f .read ()
149
- diff_and_exit (current , output )
159
+ diff_and_exit (current , output , "function list" )
150
160
else :
151
161
with open (out_file , "w" ) as f :
152
162
f .write (output )
@@ -171,26 +181,123 @@ def write_function_defs(self, check: bool) -> None:
171
181
if check :
172
182
with open (out_file , "r" ) as f :
173
183
current = f .read ()
174
- diff_and_exit (current , output )
184
+ diff_and_exit (current , output , "source list" )
175
185
else :
176
186
with open (out_file , "w" ) as f :
177
187
f .write (output )
178
188
189
+ def tidy_lists (self ) -> None :
190
+ """In each file, check annotations indicating blocks of code should be sorted or should
191
+ include all public API.
192
+ """
193
+ for dirname in DIRECTORIES :
194
+ dir = ROOT_DIR .joinpath (dirname )
195
+ for fname in iglob ("**" , root_dir = dir , recursive = True ):
196
+ fpath = dir .joinpath (fname )
197
+ if fpath .is_dir () or fpath == SELF_PATH :
198
+ continue
199
+
200
+ lines = fpath .read_text ().splitlines ()
201
+
202
+ validate_delimited_block (
203
+ fpath ,
204
+ lines ,
205
+ "verify-sorted-start" ,
206
+ "verify-sorted-end" ,
207
+ ensure_sorted ,
208
+ )
209
+
210
+ validate_delimited_block (
211
+ fpath ,
212
+ lines ,
213
+ "verify-apilist-start" ,
214
+ "verify-apilist-end" ,
215
+ lambda p , n , lines : self .ensure_contains_api (p , n , lines ),
216
+ )
217
+
218
+ def ensure_contains_api (self , fpath : Path , line_num : int , lines : list [str ]):
219
+ """Given a list of strings, ensure that each public function we have is named
220
+ somewhere.
221
+ """
222
+ not_found = []
223
+ for func in self .public_functions :
224
+ # The function name may be on its own or somewhere in a snake case string.
225
+ pat = re .compile (rf"(\b|_){ func } (\b|_)" )
226
+ found = next ((line for line in lines if pat .search (line )), None )
227
+
228
+ if found is None :
229
+ not_found .append (func )
230
+
231
+ if len (not_found ) == 0 :
232
+ return
233
+
234
+ relpath = fpath .relative_to (ROOT_DIR )
235
+ eprint (f"functions not found at { relpath } :{ line_num } : { not_found } " )
236
+ exit (1 )
237
+
238
+
239
+ def validate_delimited_block (
240
+ fpath : Path ,
241
+ lines : list [str ],
242
+ start : str ,
243
+ end : str ,
244
+ validate : Callable [[Path , int , list [str ]], None ],
245
+ ) -> None :
246
+ """Identify blocks of code wrapped within `start` and `end`, collect their contents
247
+ to a list of strings, and call `validate` for each of those lists.
248
+ """
249
+ relpath = fpath .relative_to (ROOT_DIR )
250
+ block_lines = []
251
+ block_start_line : None | int = None
252
+ for line_num , line in enumerate (lines ):
253
+ line_num += 1
254
+
255
+ if start in line :
256
+ block_start_line = line_num
257
+ continue
258
+
259
+ if end in line :
260
+ if block_start_line is None :
261
+ eprint (f"`{ end } ` without `{ start } ` at { relpath } :{ line_num } " )
262
+ exit (1 )
263
+
264
+ validate (fpath , block_start_line , block_lines )
265
+ block_lines = []
266
+ block_start_line = None
267
+ continue
268
+
269
+ if block_start_line is not None :
270
+ block_lines .append (line )
271
+
272
+ if block_start_line is not None :
273
+ eprint (f"`{ start } ` without `{ end } ` at { relpath } :{ block_start_line } " )
274
+ exit (1 )
275
+
276
+
277
+ def ensure_sorted (fpath : Path , block_start_line : int , lines : list [str ]) -> None :
278
+ """Ensure that a list of lines is sorted, otherwise print a diff and exit."""
279
+ relpath = fpath .relative_to (ROOT_DIR )
280
+ diff_and_exit (
281
+ "" .join (lines ),
282
+ "" .join (sorted (lines )),
283
+ f"sorted block at { relpath } :{ block_start_line } " ,
284
+ )
179
285
180
- def diff_and_exit (actual : str , expected : str ):
286
+
287
+ def diff_and_exit (actual : str , expected : str , name : str ):
181
288
"""If the two strings are different, print a diff between them and then exit
182
289
with an error.
183
290
"""
184
291
if actual == expected :
185
- print (" output matches expected; success" )
292
+ print (f" { name } output matches expected; success" )
186
293
return
187
294
188
295
a = [f"{ line } \n " for line in actual .splitlines ()]
189
296
b = [f"{ line } \n " for line in expected .splitlines ()]
190
297
191
298
diff = difflib .unified_diff (a , b , "actual" , "expected" )
192
299
sys .stdout .writelines (diff )
193
- print ("mismatched function list " )
300
+ print (f "mismatched { name } " )
194
301
exit (1 )
195
302
196
303
@@ -223,23 +330,31 @@ def base_name(name: str) -> tuple[str, str]:
223
330
return (name , "f64" )
224
331
225
332
333
+ def ensure_updated_list (check : bool ) -> None :
334
+ """Runner to update the function list and JSON, or check that it is already up
335
+ to date.
336
+ """
337
+ crate = Crate ()
338
+ crate .write_function_list (check )
339
+ crate .write_function_defs (check )
340
+
341
+ if check :
342
+ crate .tidy_lists ()
343
+
344
+
226
345
def main ():
227
346
"""By default overwrite the file. If `--check` is passed, print a diff instead and
228
347
error if the files are different.
229
348
"""
230
349
match sys .argv :
231
350
case [_]:
232
- check = False
351
+ ensure_updated_list ( False )
233
352
case [_, "--check" ]:
234
- check = True
353
+ ensure_updated_list ( True )
235
354
case _:
236
355
print ("unrecognized arguments" )
237
356
exit (1 )
238
357
239
- crate = Crate ()
240
- crate .write_function_list (check )
241
- crate .write_function_defs (check )
242
-
243
358
244
359
if __name__ == "__main__" :
245
360
main ()
0 commit comments