summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKlaus Aehlig <klaus.aehlig@huawei.com>2025-06-17 15:31:00 +0200
committerKlaus Aehlig <klaus.aehlig@huawei.com>2025-06-18 10:40:51 +0200
commit0686e2fd30aeccd44c12a99151b73f070d9f2fac (patch)
tree8cbde0ce587e3f1565d5b69de5579a11020622eb
parentb8817c7e04aea832b73be415a96cada5d07457f1 (diff)
downloadjustbuild-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-xbin/json-format.py269
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)