88
99import difflib
1010import json
11+ import re
1112import subprocess as sp
1213import sys
1314from dataclasses import dataclass
14- from glob import glob
15+ from glob import glob , iglob
1516from pathlib import Path
16- from typing import Any , TypeAlias
17+ from typing import Any , Callable , TypeAlias
1718
18- ETC_DIR = Path (__file__ ).parent
19+ SELF_PATH = Path (__file__ )
20+ ETC_DIR = SELF_PATH .parent
1921ROOT_DIR = ETC_DIR .parent
2022
23+ # Loose approximation of what gets checked in to git, without needing `git ls-files`.
24+ DIRECTORIES = [".github" , "ci" , "crates" , "etc" , "src" ]
25+
2126# These files do not trigger a retest.
2227IGNORED_SOURCES = ["src/libm_helper.rs" ]
2328
2429IndexTy : TypeAlias = dict [str , dict [str , Any ]]
2530"""Type of the `index` item in rustdoc's JSON output"""
2631
2732
33+ def eprint (* args , ** kwargs ):
34+ """Print to stderr."""
35+ print (* args , file = sys .stderr , ** kwargs )
36+
37+
2838@dataclass
2939class Crate :
3040 """Representation of public interfaces and function defintion locations in
@@ -146,7 +156,7 @@ def write_function_list(self, check: bool) -> None:
146156 if check :
147157 with open (out_file , "r" ) as f :
148158 current = f .read ()
149- diff_and_exit (current , output )
159+ diff_and_exit (current , output , "function list" )
150160 else :
151161 with open (out_file , "w" ) as f :
152162 f .write (output )
@@ -171,26 +181,123 @@ def write_function_defs(self, check: bool) -> None:
171181 if check :
172182 with open (out_file , "r" ) as f :
173183 current = f .read ()
174- diff_and_exit (current , output )
184+ diff_and_exit (current , output , "source list" )
175185 else :
176186 with open (out_file , "w" ) as f :
177187 f .write (output )
178188
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+ )
179285
180- def diff_and_exit (actual : str , expected : str ):
286+
287+ def diff_and_exit (actual : str , expected : str , name : str ):
181288 """If the two strings are different, print a diff between them and then exit
182289 with an error.
183290 """
184291 if actual == expected :
185- print (" output matches expected; success" )
292+ print (f" { name } output matches expected; success" )
186293 return
187294
188295 a = [f"{ line } \n " for line in actual .splitlines ()]
189296 b = [f"{ line } \n " for line in expected .splitlines ()]
190297
191298 diff = difflib .unified_diff (a , b , "actual" , "expected" )
192299 sys .stdout .writelines (diff )
193- print ("mismatched function list " )
300+ print (f "mismatched { name } " )
194301 exit (1 )
195302
196303
@@ -223,23 +330,31 @@ def base_name(name: str) -> tuple[str, str]:
223330 return (name , "f64" )
224331
225332
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+
226345def main ():
227346 """By default overwrite the file. If `--check` is passed, print a diff instead and
228347 error if the files are different.
229348 """
230349 match sys .argv :
231350 case [_]:
232- check = False
351+ ensure_updated_list ( False )
233352 case [_, "--check" ]:
234- check = True
353+ ensure_updated_list ( True )
235354 case _:
236355 print ("unrecognized arguments" )
237356 exit (1 )
238357
239- crate = Crate ()
240- crate .write_function_list (check )
241- crate .write_function_defs (check )
242-
243358
244359if __name__ == "__main__" :
245360 main ()
0 commit comments