blob: cba9a40eee0008296714795ce11c7c2e2743a4df [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2020 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Lint various JSON files."""
import collections
import difflib
import json
import logging
from pathlib import Path
import sys
import libdot
def filter_known_files(paths: list[Path]) -> list[Path]:
"""Figure out what files this linter supports."""
ret = []
for path in paths:
path = Path(path)
if path.suffix[1:] in {"json"} and path.exists():
ret += [path]
return [str(x) for x in ret]
def setup():
"""Initialize the tool settings."""
def check_common(
path: Path, old_data: bytes, new_data: bytes, fix: bool = False
) -> bool:
"""Verify the style is what we want."""
# We want a trailing new line on all text files.
new_data += "\n"
if old_data == new_data:
return True
if fix:
with open(path, "w", encoding="utf-8") as fp:
fp.write(new_data)
return True
diff = difflib.unified_diff(
old_data.splitlines(), new_data.splitlines(), n=2, lineterm=""
)
# Skip the --- line.
next(diff)
# Skip the +++ line.
next(diff)
logging.error("%s: needs reformatting\n%s", path, "\n".join(diff))
return False
def check_generic(path: Path, fix: bool = False) -> bool:
"""Check style on a JSON file."""
with open(path, "r", encoding="utf-8") as fp:
old_data = fp.read()
# We use 2 space tab indents for most files, and we do not require keys be
# sorted so they can be logically grouped.
try:
data = json.loads(old_data, object_pairs_hook=collections.OrderedDict)
except json.decoder.JSONDecodeError as e:
logging.error("%s: %s", path, e)
return False
new_data = json.dumps(data, ensure_ascii=False, indent=2, sort_keys=False)
return check_common(path, old_data, new_data, fix=fix)
def check_messages(path: Path, fix: bool = False) -> bool:
"""Check style on a messages.json file."""
with open(path, "r", encoding="utf-8") as fp:
old_data = fp.read()
# We use tabs for messages.json to save a little on disk, and keep the keys
# sorted as this content is mostly machine managed.
try:
data = json.loads(old_data)
except json.decoder.JSONDecodeError as e:
logging.error("%s: %s", path, e)
return False
new_data = json.dumps(data, ensure_ascii=False, indent="\t", sort_keys=True)
return check_common(path, old_data, new_data, fix=fix)
def check_package(path: Path, fix: bool = False) -> bool:
"""Check style on a package.json file."""
with open(path, "r", encoding="utf-8") as fp:
old_data = fp.read()
new_data = libdot.sync_package_json.format_data(path, old_data)
return check_common(path, old_data, new_data, fix=fix)
def check_files(paths: list[Path], fix: bool = False) -> bool:
"""Check all the JSON files."""
ret = True
for path in paths:
path = Path(path)
if not path.exists():
logging.error("%s: file does not exist", path)
ret = False
continue
logging.debug("Checking %s", path)
if path.name == "messages.json":
ret &= check_messages(path, fix=fix)
elif path.name == "package.json":
ret &= check_package(path, fix=fix)
else:
ret &= check_generic(path, fix=fix)
return ret
def run(argv=(), **kwargs):
"""Run the tool directly."""
setup()
logging.info("Linting JSON files %s", libdot.cmdstr(argv))
return check_files(argv, **kwargs)
def perform(
argv=(), paths=(), fix=False, gerrit_comments_file=None
): # pylint: disable=unused-argument
"""Run high level tool logic."""
argv = list(argv)
paths = list(paths)
# TODO(vapier): Add support for Gerrit comments.
return check_files(argv + paths, fix=fix)
def get_parser():
"""Get a command line parser."""
parser = libdot.ArgumentParser(description=__doc__, short_options=False)
parser.add_argument(
"--fix", action="store_true", help="Autofix format errors."
)
parser.add_argument(
"--gerrit-comments-file",
help="Save errors for posting files to Gerrit.",
)
parser.add_argument("paths", nargs="*", help="Paths to lint.")
return parser
def main(argv):
"""The main func!"""
parser = get_parser()
opts, args = parser.parse_known_args(argv)
return (
0
if perform(
argv=args,
paths=opts.paths,
fix=opts.fix,
gerrit_comments_file=opts.gerrit_comments_file,
)
else 1
)
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))