diff options
author | Klaus Aehlig <klaus.aehlig@huawei.com> | 2025-06-17 15:31:00 +0200 |
---|---|---|
committer | Klaus Aehlig <klaus.aehlig@huawei.com> | 2025-06-18 10:40:51 +0200 |
commit | 0686e2fd30aeccd44c12a99151b73f070d9f2fac (patch) | |
tree | 8cbde0ce587e3f1565d5b69de5579a11020622eb | |
parent | b8817c7e04aea832b73be415a96cada5d07457f1 (diff) | |
download | justbuild-0686e2fd30aeccd44c12a99151b73f070d9f2fac.tar.gz |
Add json formater for target files
... that is aware of the order-independent fields in our C++ rules.
Co-authored-by: Maksim Denisov <denisov.maksim@huawei.com>
Co-authored-by: Oliver Reiche <oliver.reiche@huawei.com>
Co-authored-by: Paul Cristian Sarbu <paul.cristian.sarbu@huawei.com>
Co-authored-by: Alberto Sartori <alberto.sartori@huawei.com>
-rwxr-xr-x | bin/json-format.py | 269 |
1 files changed, 269 insertions, 0 deletions
diff --git a/bin/json-format.py b/bin/json-format.py new file mode 100755 index 00000000..02c5f2b8 --- /dev/null +++ b/bin/json-format.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python3 +# Copyright 2025 Huawei Cloud Computing Technology Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys +import json +import difflib +from functools import cmp_to_key +from typing import Any, Dict, List, Tuple, Union, cast +from argparse import ArgumentParser +try: + from pygments import console # type: ignore + has_pygments = True +except ImportError: + has_pygments = False + +JSON = Union[str, int, float, bool, None, Dict[str, 'JSON'], List['JSON']] + + +def is_simple(entry: JSON) -> bool: + if isinstance(entry, list): + return len(entry) == 0 + if isinstance(entry, dict): + return len(entry) == 0 + return True + + +def is_short(entry: JSON, indent: int) -> bool: + return len(json.dumps(entry)) + indent < 80 + + +def hdumps(entry: JSON, *, _current_indent: int = 0) -> str: + if is_short(entry, _current_indent): + return json.dumps(entry) + if isinstance(entry, list) and entry: + result = "[ " + hdumps(entry[0], _current_indent=_current_indent + 2) + for x in entry[1:]: + result += "\n" + " " * _current_indent + ", " + result += hdumps(x, _current_indent=_current_indent + 2) + result += "\n" + " " * _current_indent + "]" + return result + if isinstance(entry, dict) and entry: + result = "{ " + is_first = True + for k in entry.keys(): + if not is_first: + result += "\n" + " " * _current_indent + ", " + result += json.dumps(k) + ":" + if is_simple(entry[k]): + result += " " + json.dumps(entry[k]) + elif is_short(entry[k], _current_indent + len(json.dumps(k)) + 4): + result += " " + json.dumps(entry[k]) + else: + result += "\n" + " " * _current_indent + " " + result += hdumps(entry[k], _current_indent=_current_indent + 2) + is_first = False + result += "\n" + " " * _current_indent + "}" + return result + return json.dumps(entry) + + +def compare_deps(lhs: JSON, rhs: JSON) -> int: + # Regular strings appear before everything else + if isinstance(lhs, str) != isinstance(rhs, str): + if isinstance(lhs, str): + return -1 + else: + return 1 + + # Regular strings are ordered in the alphabetic order + if isinstance(lhs, str) and isinstance(rhs, str): + if lhs < rhs: + return -1 + elif lhs > rhs: + return 1 + + if isinstance(lhs, list) and isinstance(rhs, list): + # Third-party dependencies appear before other dependencies + if cast(Any, lhs[0]) != cast(Any, rhs[0]): + if lhs[0] == "@": + return -1 + if rhs[0] == "@": + return 1 + + # Dependencies are ordered in the alphabetic order + if lhs < rhs: + return -1 + elif lhs > rhs: + return 1 + return 0 + + +def sort_list_of_dependencies(deps: JSON) -> JSON: + if not isinstance(deps, list): + return deps + deps = sorted(deps, key=cmp_to_key(compare_deps)) + + # Remove duplicated dependencies + i = 0 + while i < len(deps) - 1: + if deps[i] == deps[i + 1]: + deps.pop(i + 1) + else: + i += 1 + return deps + + +# Get indices of intersecting entries in two sorted lists( [[lhs_index, rhs_index],...] ) +# Resulting pairs are ordered in the non-descending order for both lhs and rhs. +def get_intersecting_indices(lhs: JSON, rhs: JSON) -> list[Tuple[int, int]]: + if not isinstance(lhs, list) or not isinstance(rhs, list): + return list() + intersection_indices: list[Tuple[int, int]] = list() + i = 0 + j = 0 + while i < len(lhs) and j < len(rhs): + compare_result = compare_deps(lhs[i], rhs[j]) + if compare_result < 0: + i += 1 + elif compare_result > 0: + j += 1 + else: + intersection_indices.append((i, j)) + j += 1 + return intersection_indices + + +def sort_dependencies(content: dict[str, JSON]) -> JSON: + if "deps" in content: + content["deps"] = sort_list_of_dependencies(content["deps"]) + if "private-deps" in content: + content["private-deps"] = sort_list_of_dependencies( + content["private-deps"]) + + # Remove intersecting dependencies between public and private dependencies, + # if both are present in the target: + # Typically an intersection occurs when a developer makes a private + # dependency public, but forgets to remove the private-deps entry. + # That's why private-deps entries are deleted here. + if "deps" in content and "private-deps" in content: + intersection = get_intersecting_indices(content["deps"], + content["private-deps"]) + for _, j in reversed(intersection): + cast(list[JSON], content["private-deps"]).pop(j) + return content + + +def sort_targets_dependencies(data: str) -> str: + targets: dict[str, JSON] = json.loads(data) + for target_name, content in targets.items(): + targets[target_name] = sort_dependencies(cast(Dict[str, JSON], content)) + return json.dumps(targets) + + +def color_diff(before: str, after: str): + next_lines = 0 + lines: List[str] = [] + for line in difflib.ndiff(before.splitlines(keepends=True), + after.splitlines(keepends=True)): + if line.startswith('+'): + next_lines = 3 + prev_lines = lines[-3:] + lines.clear() + yield "".join(prev_lines + [ + console.colorize("green", line) # type: ignore + if has_pygments else line + ]) + elif line.startswith('-'): + next_lines = 3 + prev_lines = lines[-3:] + lines.clear() + yield "".join(prev_lines + [ + console.colorize("red", line) # type: ignore + if has_pygments else line + ]) + elif line.startswith('?'): + next_lines = 3 + prev_lines = lines[-3:] + lines.clear() + yield "".join(prev_lines + [ + console.colorize("blue", line) # type: ignore + if has_pygments else line + ]) + else: + if next_lines > 0: + next_lines -= 1 + yield line + else: + lines.append(line) + + +if __name__ == "__main__": + parser = ArgumentParser() + parser.add_argument("in_file", + help="input file (omit for stdin)", + nargs='?', + default=None) + parser.add_argument("-c", + "--check", + action='store_true', + help="just verify format of input", + default=False) + parser.add_argument("-d", + "--diff", + action='store_true', + help="with -c, print colored diff", + default=False) + parser.add_argument("-i", + "--in-place", + action='store_true', + help="modify input file in-place", + default=False) + parser.add_argument("-s", + "--sort", + action='store_true', + help="sort dependencies and remove duplicates", + default=False) + + options = parser.parse_args() + + if options.in_file: + with open(options.in_file, 'r') as f: + data = f.read() + else: + data = sys.stdin.read() + + try: + data_formatted: str = data + if options.sort: + data_formatted = sort_targets_dependencies(data_formatted) + data_formatted: str = hdumps(json.loads(data_formatted)) + except Exception as e: + # if the file contains syntax errors, we print the file name + if options.in_file: + print("Found syntax issues in", options.in_file) + print(e) + exit(1) + + data_newline = data_formatted + '\n' + + if options.check: + if data != data_newline: + print("Found format issues" + ( + (" in file: " + options.in_file) if options.in_file else ":")) + if options.diff: + print("".join(color_diff(data, data_newline))) + exit(1) + exit(0) + + if options.in_place and options.in_file: + out_file = f"{options.in_file}.out" + with open(out_file, 'w') as f: + f.write(data_newline) + os.rename(out_file, options.in_file) + exit(0) + + print(data_formatted) |