blob: 263336efe324353e497575814b24403dd68caff8 [file] [log] [blame]
#!/usr/bin/python3
# all arguments to this script are considered as json files
# and attempted to be formatted alphabetically
import json
import os
import re
from sys import argv
from typing import List, Tuple, Union
# Trying to parse JSON comments and then being able to re-insert them into
# the correct location on a re-emitted and sorted JSON would be very difficult.
# To make this somewhat manageable, we take a few shortcuts here:
#
# - Single-line style comments (//) can be on a new line or at the end of
# a line with contents.
#
# - Multi-line style comments (/* */) use the must be free-standing.
#
# - Comments will get inserted back into the file in the line they came
# from. If keys are resorted or the number of lines change, all bets
# for correctness are off.
#
# - No attempts to re-indent multi-line comments will be made.
#
# In light of this, it is highly recommended to use a JSON formatter such as
# prettier before using this script and planning to move multi-line comments
# around after key resorting.
class CommentTracker:
# Regex patterns used.
single_line_pattern = re.compile(r"\s*//.*$")
multi_line_start_pattern = re.compile(r"/\*")
multi_line_end_pattern = re.compile(r".*\*/", re.MULTILINE | re.DOTALL)
def __init__(self) -> None:
self.comments: List[Tuple[bool, int, str]] = []
# Extract out the comments from a JSON-like string and save them away.
def extract_comments(self, contents: str) -> str:
result = []
multi_line_segment: Union[str, None] = None
multi_line_start = 0
for idx, line in enumerate(contents.split("\n")):
single = CommentTracker.single_line_pattern.search(line)
if single:
do_append = False if line.startswith(single.group(0)) else True
line = line[: single.start(0)]
self.comments.append((do_append, idx, single.group(0)))
multi_start = CommentTracker.multi_line_start_pattern.search(line)
if not multi_line_segment and multi_start:
multi_line_start = idx
multi_line_segment = line
elif multi_line_segment:
multi_line_segment = multi_line_segment + "\n" + line
if not multi_line_segment:
result.append(line)
continue
multi_end = CommentTracker.multi_line_end_pattern.search(
multi_line_segment
)
if multi_end:
self.comments.append(
(False, multi_line_start, multi_end.group(0))
)
result.append(multi_line_segment[multi_end.end(0) :])
multi_line_segment = None
return "\n".join(result)
# Re-insert the saved off comments into a JSON-like string.
def insert_comments(self, contents: str) -> str:
result = contents.split("\n")
for append, idx, string in self.comments:
if append:
result[idx] = result[idx] + string
else:
result = result[:idx] + string.split("\n") + result[idx:]
return "\n".join(result)
files = []
for file in argv[1:]:
if not os.path.isdir(file):
files.append(file)
continue
for root, _, filenames in os.walk(file):
for f in filenames:
files.append(os.path.join(root, f))
for file in files:
if not file.endswith(".json"):
continue
print("formatting file {}".format(file))
comments = CommentTracker()
with open(file) as fp:
j = json.loads(comments.extract_comments(fp.read()))
if isinstance(j, list):
for item in j:
item["Exposes"] = sorted(item["Exposes"], key=lambda k: k["Type"])
else:
j["Exposes"] = sorted(j["Exposes"], key=lambda k: k["Type"])
with open(file, "w") as fp:
contents = json.dumps(
j, indent=4, sort_keys=True, separators=(",", ": ")
)
fp.write(comments.insert_comments(contents))
fp.write("\n")