blob: 38cda6b1e10188f98cbc76f1debc2959f2746c28 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2020 The Chromium OS Authors. All rights reserved.
# 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
from typing import List
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.
data = json.loads(old_data, object_pairs_hook=collections.OrderedDict)
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.
data = json.loads(old_data)
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_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)
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:]))