| #!/usr/bin/env python3 |
| # Copyright 2013 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Script that is used by PRESUBMIT.py to run style checks on Java files.""" |
| |
| import argparse |
| import collections |
| import os |
| import subprocess |
| import sys |
| import xml.dom.minidom |
| |
| |
| _SELF_DIR = os.path.dirname(__file__) |
| CHROMIUM_SRC = os.path.normpath(os.path.join(_SELF_DIR, '..', '..', '..')) |
| _CHECKSTYLE_ROOT = os.path.join(CHROMIUM_SRC, 'third_party', 'checkstyle', |
| 'cipd', 'checkstyle-all.jar') |
| _JAVA_PATH = os.path.join(CHROMIUM_SRC, 'third_party', 'jdk', 'current', 'bin', |
| 'java') |
| _STYLE_FILE = os.path.join(_SELF_DIR, 'chromium-style-5.0.xml') |
| _REMOVE_UNUSED_IMPORTS_PATH = os.path.join(_SELF_DIR, |
| 'remove_unused_imports.py') |
| _INCLUSIVE_WARNING_IDENTIFIER = 'Please use inclusive language' |
| |
| |
| class Violation( |
| collections.namedtuple('Violation', |
| 'file,line,column,message,severity')): |
| def __str__(self): |
| column = f'{self.column}:' if self.column else '' |
| return f'{self.file}:{self.line}:{column} {self.message}' |
| |
| def is_warning(self): |
| return self.severity == 'warning' |
| |
| def is_error(self): |
| return self.severity == 'error' |
| |
| |
| def run_checkstyle(local_path, style_file, java_files): |
| cmd = [ |
| _JAVA_PATH, '-cp', _CHECKSTYLE_ROOT, |
| 'com.puppycrawl.tools.checkstyle.Main', '-c', style_file, '-f', 'xml' |
| ] + java_files |
| result = subprocess.run(cmd, capture_output=True, check=False, text=True) |
| |
| stderr_lines = result.stderr.splitlines() |
| # One line is always: "Checkstyle ends with # warnings/errors". |
| if len(stderr_lines) > 1 or (stderr_lines |
| and 'ends with' not in stderr_lines[0]): |
| sys.stderr.write(result.stderr) |
| sys.stderr.write( |
| f'\nCheckstyle failed with returncode={result.returncode}.\n') |
| sys.stderr.write('This might mean you have a syntax error\n') |
| sys.exit(-1) |
| |
| try: |
| root = xml.dom.minidom.parseString(result.stdout) |
| except Exception: |
| sys.stderr.write('Tried to parse:\n') |
| sys.stderr.write(result.stdout) |
| sys.stderr.write('\n') |
| raise |
| |
| inclusive_files = [] |
| inclusive_warning = '' |
| results = [] |
| for fileElement in root.getElementsByTagName('file'): |
| filename = fileElement.attributes['name'].value |
| if filename.startswith(local_path): |
| filename = filename[len(local_path) + 1:] |
| errors = fileElement.getElementsByTagName('error') |
| for error in errors: |
| severity = error.attributes['severity'].value |
| if severity not in ('warning', 'error'): |
| continue |
| message = error.attributes['message'].value |
| line = int(error.attributes['line'].value) |
| column = None |
| if error.hasAttribute('column'): |
| column = int(error.attributes['column'].value) |
| if _INCLUSIVE_WARNING_IDENTIFIER in message: |
| inclusive_warning = message |
| inclusive_files.append(f'{filename}:{str(line)}\n ') |
| continue |
| results.append(Violation(filename, line, column, message, |
| severity)) |
| |
| if inclusive_files: |
| results.append( |
| Violation( |
| ''.join(str(filename) for filename in inclusive_files) + '\n', |
| ' ^^^ The above edited file(s) contain non-inclusive language (may be pre-existing). ^^^ ', |
| '', inclusive_warning, 'warning')) |
| |
| return results |
| |
| |
| def run_presubmit(input_api, output_api, files_to_skip=None): |
| # Android toolchain is only available on Linux. |
| if not sys.platform.startswith('linux'): |
| return [] |
| |
| # Filter out non-Java files and files that were deleted. |
| java_files = [ |
| x.AbsoluteLocalPath() for x in |
| input_api.AffectedSourceFiles(lambda f: input_api.FilterSourceFile( |
| f, files_to_skip=files_to_skip)) if x.LocalPath().endswith('.java') |
| ] |
| if not java_files: |
| return [] |
| |
| local_path = input_api.PresubmitLocalPath() |
| violations = run_checkstyle(local_path, _STYLE_FILE, java_files) |
| warnings = [' ' + str(v) for v in violations if v.is_warning()] |
| errors = [' ' + str(v) for v in violations if v.is_error()] |
| |
| ret = [] |
| if warnings: |
| ret.append(output_api.PresubmitPromptWarning('\n'.join(warnings))) |
| if errors: |
| msg = '\n'.join(errors) |
| if 'Unused import:' in msg or 'Duplicate import' in msg: |
| msg += """ |
| |
| To remove unused imports: """ + input_api.os_path.relpath( |
| _REMOVE_UNUSED_IMPORTS_PATH, local_path) |
| ret.append(output_api.PresubmitError(msg)) |
| return ret |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument('java_files', nargs='+') |
| args = parser.parse_args() |
| |
| violations = run_checkstyle(CHROMIUM_SRC, _STYLE_FILE, args.java_files) |
| for v in violations: |
| print(f'{v} ({v.severity})') |
| |
| if any(v.is_error() for v in violations): |
| sys.exit(1) |
| |
| |
| if __name__ == '__main__': |
| main() |