1#!/usr/bin/python3 2# all arguments to this script are considered as json files 3# and attempted to be formatted alphabetically 4 5import json 6import os 7import re 8from sys import argv 9from typing import List, Tuple, Union 10 11# Trying to parse JSON comments and then being able to re-insert them into 12# the correct location on a re-emitted and sorted JSON would be very difficult. 13# To make this somewhat manageable, we take a few shortcuts here: 14# 15# - Single-line style comments (//) can be on a new line or at the end of 16# a line with contents. 17# 18# - Multi-line style comments (/* */) use the must be free-standing. 19# 20# - Comments will get inserted back into the file in the line they came 21# from. If keys are resorted or the number of lines change, all bets 22# for correctness are off. 23# 24# - No attempts to re-indent multi-line comments will be made. 25# 26# In light of this, it is highly recommended to use a JSON formatter such as 27# prettier before using this script and planning to move multi-line comments 28# around after key resorting. 29 30 31class CommentTracker: 32 # Regex patterns used. 33 single_line_pattern = re.compile(r"\s*//.*$") 34 multi_line_start_pattern = re.compile(r"/\*") 35 multi_line_end_pattern = re.compile(r".*\*/", re.MULTILINE | re.DOTALL) 36 37 def __init__(self) -> None: 38 self.comments: List[Tuple[bool, int, str]] = [] 39 40 # Extract out the comments from a JSON-like string and save them away. 41 def extract_comments(self, contents: str) -> str: 42 result = [] 43 44 multi_line_segment: Union[str, None] = None 45 multi_line_start = 0 46 47 for idx, line in enumerate(contents.split("\n")): 48 single = CommentTracker.single_line_pattern.search(line) 49 if single: 50 do_append = False if line.startswith(single.group(0)) else True 51 line = line[: single.start(0)] 52 self.comments.append((do_append, idx, single.group(0))) 53 54 multi_start = CommentTracker.multi_line_start_pattern.search(line) 55 if not multi_line_segment and multi_start: 56 multi_line_start = idx 57 multi_line_segment = line 58 elif multi_line_segment: 59 multi_line_segment = multi_line_segment + "\n" + line 60 61 if not multi_line_segment: 62 result.append(line) 63 continue 64 65 multi_end = CommentTracker.multi_line_end_pattern.search( 66 multi_line_segment 67 ) 68 if multi_end: 69 self.comments.append( 70 (False, multi_line_start, multi_end.group(0)) 71 ) 72 result.append(multi_line_segment[multi_end.end(0) :]) 73 multi_line_segment = None 74 75 return "\n".join(result) 76 77 # Re-insert the saved off comments into a JSON-like string. 78 def insert_comments(self, contents: str) -> str: 79 result = contents.split("\n") 80 81 for append, idx, string in self.comments: 82 if append: 83 result[idx] = result[idx] + string 84 else: 85 result = result[:idx] + string.split("\n") + result[idx:] 86 87 return "\n".join(result) 88 89 90files = [] 91 92for file in argv[1:]: 93 if not os.path.isdir(file): 94 files.append(file) 95 continue 96 for root, _, filenames in os.walk(file): 97 for f in filenames: 98 files.append(os.path.join(root, f)) 99 100for file in files: 101 if not file.endswith(".json"): 102 continue 103 print("formatting file {}".format(file)) 104 105 comments = CommentTracker() 106 107 with open(file) as fp: 108 j = json.loads(comments.extract_comments(fp.read())) 109 110 if isinstance(j, list): 111 for item in j: 112 item["Exposes"] = sorted(item["Exposes"], key=lambda k: k["Type"]) 113 else: 114 j["Exposes"] = sorted(j["Exposes"], key=lambda k: k["Type"]) 115 116 with open(file, "w") as fp: 117 contents = json.dumps( 118 j, indent=4, sort_keys=True, separators=(",", ": ") 119 ) 120 121 fp.write(comments.insert_comments(contents)) 122 fp.write("\n") 123