James Feist | 3cb5fec | 2018-01-23 14:41:51 -0800 | [diff] [blame] | 1 | #!/usr/bin/python3 |
| 2 | # all arguments to this script are considered as json files |
| 3 | # and attempted to be formatted alphabetically |
| 4 | |
| 5 | import json |
James Feist | 5ffd8b4 | 2019-10-25 11:22:10 -0700 | [diff] [blame] | 6 | import os |
Potin Lai | 0f3a4d9 | 2023-12-05 00:13:55 +0800 | [diff] [blame] | 7 | import re |
James Feist | 3cb5fec | 2018-01-23 14:41:51 -0800 | [diff] [blame] | 8 | from sys import argv |
Potin Lai | 0f3a4d9 | 2023-12-05 00:13:55 +0800 | [diff] [blame] | 9 | from 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 | |
| 31 | class 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 | |
James Feist | 3cb5fec | 2018-01-23 14:41:51 -0800 | [diff] [blame] | 89 | |
Patrick Williams | ae74f23 | 2024-09-05 14:51:04 -0400 | [diff] [blame] | 90 | files = [] |
James Feist | 5ffd8b4 | 2019-10-25 11:22:10 -0700 | [diff] [blame] | 91 | |
Patrick Williams | ae74f23 | 2024-09-05 14:51:04 -0400 | [diff] [blame] | 92 | for 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)) |
James Feist | 5ffd8b4 | 2019-10-25 11:22:10 -0700 | [diff] [blame] | 99 | |
| 100 | for file in files: |
Patrick Williams | cad2d1f | 2022-12-04 14:38:16 -0600 | [diff] [blame] | 101 | if not file.endswith(".json"): |
Brad Bishop | ca000e5 | 2019-12-19 15:43:06 -0500 | [diff] [blame] | 102 | continue |
James Feist | c4e5694 | 2019-04-19 12:15:19 -0700 | [diff] [blame] | 103 | print("formatting file {}".format(file)) |
Potin Lai | 0f3a4d9 | 2023-12-05 00:13:55 +0800 | [diff] [blame] | 104 | |
| 105 | comments = CommentTracker() |
| 106 | |
| 107 | with open(file) as fp: |
| 108 | j = json.loads(comments.extract_comments(fp.read())) |
James Feist | 3cb5fec | 2018-01-23 14:41:51 -0800 | [diff] [blame] | 109 | |
James Feist | c4e5694 | 2019-04-19 12:15:19 -0700 | [diff] [blame] | 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 | |
Potin Lai | 0f3a4d9 | 2023-12-05 00:13:55 +0800 | [diff] [blame] | 116 | with open(file, "w") as fp: |
| 117 | contents = json.dumps( |
| 118 | j, indent=4, sort_keys=True, separators=(",", ": ") |
Patrick Williams | cad2d1f | 2022-12-04 14:38:16 -0600 | [diff] [blame] | 119 | ) |
Potin Lai | 0f3a4d9 | 2023-12-05 00:13:55 +0800 | [diff] [blame] | 120 | |
| 121 | fp.write(comments.insert_comments(contents)) |
| 122 | fp.write("\n") |