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