| #!/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:])) |