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 = argv[1:]
91
92for file in files[:]:
93    if os.path.isdir(file):
94        files.remove(file)
95        for f in os.listdir(file):
96            files.append(os.path.join(file, f))
97
98for file in files:
99    if not file.endswith(".json"):
100        continue
101    print("formatting file {}".format(file))
102
103    comments = CommentTracker()
104
105    with open(file) as fp:
106        j = json.loads(comments.extract_comments(fp.read()))
107
108    if isinstance(j, list):
109        for item in j:
110            item["Exposes"] = sorted(item["Exposes"], key=lambda k: k["Type"])
111    else:
112        j["Exposes"] = sorted(j["Exposes"], key=lambda k: k["Type"])
113
114    with open(file, "w") as fp:
115        contents = json.dumps(
116            j, indent=4, sort_keys=True, separators=(",", ": ")
117        )
118
119        fp.write(comments.insert_comments(contents))
120        fp.write("\n")
121