| #!/usr/bin/python |
| |
| # |
| # Copyright 2015, The Android Open Source Project |
| # |
| # 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. |
| # |
| |
| """Script that is used by developers to run style checks on Java files.""" |
| |
| import argparse |
| import errno |
| import os |
| import shutil |
| import subprocess |
| import sys |
| import tempfile |
| import xml.dom.minidom |
| import gitlint.git as git |
| |
| |
| def _FindFoldersContaining(root, wanted): |
| """Recursively finds directories that have a file with the given name. |
| |
| Args: |
| root: Root folder to start the search from. |
| wanted: The filename that we are looking for. |
| |
| Returns: |
| List of folders that has a file with the given name |
| """ |
| |
| if not root: |
| return [] |
| if os.path.islink(root): |
| return [] |
| result = [] |
| for file_name in os.listdir(root): |
| file_path = os.path.join(root, file_name) |
| if os.path.isdir(file_path): |
| sub_result = _FindFoldersContaining(file_path, wanted) |
| result.extend(sub_result) |
| else: |
| if file_name == wanted: |
| result.append(root) |
| return result |
| |
| MAIN_DIRECTORY = os.path.normpath(os.path.dirname(__file__)) |
| CHECKSTYLE_JAR = os.path.join(MAIN_DIRECTORY, 'checkstyle.jar') |
| CHECKSTYLE_STYLE = os.path.join(MAIN_DIRECTORY, 'android-style.xml') |
| FORCED_RULES = ['com.puppycrawl.tools.checkstyle.checks.imports.ImportOrderCheck', |
| 'com.puppycrawl.tools.checkstyle.checks.imports.UnusedImportsCheck'] |
| SKIPPED_RULES_FOR_TEST_FILES = ['com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTypeCheck', |
| 'com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocMethodCheck'] |
| SUBPATH_FOR_TEST_FILES = ['/tests/', '/test/', '/androidTest/'] |
| SUBPATH_FOR_TEST_DATA_FILES = _FindFoldersContaining(git.repository_root(), |
| 'IGNORE_CHECKSTYLE') |
| ERROR_UNCOMMITTED = 'You need to commit all modified files before running Checkstyle\n' |
| ERROR_UNTRACKED = 'You have untracked java files that are not being checked:\n' |
| |
| |
| def RunCheckstyleOnFiles(java_files, classpath=CHECKSTYLE_JAR, config_xml=CHECKSTYLE_STYLE): |
| """Runs Checkstyle checks on a given set of java_files. |
| |
| Args: |
| java_files: A list of files to check. |
| classpath: The colon-delimited list of JARs in the classpath. |
| config_xml: Path of the checkstyle XML configuration file. |
| |
| Returns: |
| A tuple of errors and warnings. |
| """ |
| print 'Running Checkstyle on inputted files' |
| java_files = map(os.path.abspath, java_files) |
| stdout = _ExecuteCheckstyle(java_files, classpath, config_xml) |
| (errors, warnings) = _ParseAndFilterOutput(stdout) |
| _PrintErrorsAndWarnings(errors, warnings) |
| return errors, warnings |
| |
| |
| def RunCheckstyleOnACommit(commit, |
| classpath=CHECKSTYLE_JAR, |
| config_xml=CHECKSTYLE_STYLE, |
| file_whitelist=None): |
| """Runs Checkstyle checks on a given commit. |
| |
| It will run Checkstyle on the changed Java files in a specified commit SHA-1 |
| and if that is None it will fallback to check the latest commit of the |
| currently checked out branch. |
| |
| Args: |
| commit: A full 40 character SHA-1 of a commit to check. |
| classpath: The colon-delimited list of JARs in the classpath. |
| config_xml: Path of the checkstyle XML configuration file. |
| file_whitelist: A list of whitelisted file paths that should be checked. |
| |
| Returns: |
| A tuple of errors and warnings. |
| """ |
| if not git.repository_root(): |
| print 'FAILURE: not inside a git repository' |
| sys.exit(1) |
| explicit_commit = commit is not None |
| if not explicit_commit: |
| _WarnIfUntrackedFiles() |
| commit = git.last_commit() |
| print 'Running Checkstyle on %s commit' % commit |
| commit_modified_files = _GetModifiedFiles(commit, explicit_commit) |
| commit_modified_files = _FilterFiles(commit_modified_files, file_whitelist) |
| if not commit_modified_files.keys(): |
| print 'No Java files to check' |
| return [], [] |
| |
| (tmp_dir, tmp_file_map) = _GetTempFilesForCommit( |
| commit_modified_files.keys(), commit) |
| |
| java_files = tmp_file_map.keys() |
| stdout = _ExecuteCheckstyle(java_files, classpath, config_xml) |
| |
| # Remove all the temporary files. |
| shutil.rmtree(tmp_dir) |
| |
| (errors, warnings) = _ParseAndFilterOutput(stdout, |
| commit, |
| commit_modified_files, |
| tmp_file_map) |
| _PrintErrorsAndWarnings(errors, warnings) |
| return errors, warnings |
| |
| |
| def _WarnIfUntrackedFiles(out=sys.stdout): |
| """Prints a warning and a list of untracked files if needed.""" |
| root = git.repository_root() |
| untracked_files = git.modified_files(root, False) |
| untracked_files = {f for f in untracked_files if f.endswith('.java')} |
| if untracked_files: |
| out.write(ERROR_UNTRACKED) |
| for untracked_file in untracked_files: |
| out.write(untracked_file + '\n') |
| out.write('\n') |
| |
| |
| def _PrintErrorsAndWarnings(errors, warnings): |
| """Prints given errors and warnings.""" |
| if errors: |
| print 'ERRORS:' |
| print '\n'.join(errors) |
| if warnings: |
| print 'WARNINGS:' |
| print '\n'.join(warnings) |
| |
| |
| def _ExecuteCheckstyle(java_files, classpath, config_xml): |
| """Runs Checkstyle to check give Java files for style errors. |
| |
| Args: |
| java_files: A list of Java files that needs to be checked. |
| classpath: The colon-delimited list of JARs in the classpath. |
| config_xml: Path of the checkstyle XML configuration file. |
| |
| Returns: |
| Checkstyle output in XML format. |
| """ |
| # Run checkstyle |
| checkstyle_env = os.environ.copy() |
| checkstyle_env['JAVA_CMD'] = 'java' |
| try: |
| check = subprocess.Popen(['java', '-cp', classpath, |
| 'com.puppycrawl.tools.checkstyle.Main', '-c', |
| config_xml, '-f', 'xml'] + java_files, |
| stdout=subprocess.PIPE, env=checkstyle_env) |
| stdout, _ = check.communicate() |
| except OSError as e: |
| if e.errno == errno.ENOENT: |
| print 'Error running Checkstyle!' |
| sys.exit(1) |
| |
| # A work-around for Checkstyle printing error count to stdio. |
| if 'Checkstyle ends with' in stdout.splitlines()[-1]: |
| stdout = '\n'.join(stdout.splitlines()[:-1]) |
| return stdout |
| |
| |
| def _ParseAndFilterOutput(stdout, |
| sha=None, |
| commit_modified_files=None, |
| tmp_file_map=None): |
| result_errors = [] |
| result_warnings = [] |
| root = xml.dom.minidom.parseString(stdout) |
| for file_element in root.getElementsByTagName('file'): |
| file_name = file_element.attributes['name'].value |
| if tmp_file_map: |
| file_name = tmp_file_map[file_name] |
| modified_lines = None |
| if commit_modified_files: |
| modified_lines = git.modified_lines(file_name, |
| commit_modified_files[file_name], |
| sha) |
| test_class = any(substring in file_name for substring |
| in SUBPATH_FOR_TEST_FILES) |
| test_data_class = any(substring in file_name for substring |
| in SUBPATH_FOR_TEST_DATA_FILES) |
| file_name = os.path.relpath(file_name) |
| errors = file_element.getElementsByTagName('error') |
| for error in errors: |
| line = int(error.attributes['line'].value) |
| rule = error.attributes['source'].value |
| if _ShouldSkip(commit_modified_files, modified_lines, line, rule, |
| test_class, test_data_class): |
| continue |
| |
| column = '' |
| if error.hasAttribute('column'): |
| column = '%s:' % error.attributes['column'].value |
| message = error.attributes['message'].value |
| project = '' |
| if os.environ.get('REPO_PROJECT'): |
| project = '[' + os.environ.get('REPO_PROJECT') + '] ' |
| |
| result = ' %s%s:%s:%s %s' % (project, file_name, line, column, message) |
| |
| severity = error.attributes['severity'].value |
| if severity == 'error': |
| result_errors.append(result) |
| elif severity == 'warning': |
| result_warnings.append(result) |
| return result_errors, result_warnings |
| |
| |
| def _ShouldSkip(commit_check, modified_lines, line, rule, test_class=False, |
| test_data_class=False): |
| """Returns whether an error on a given line should be skipped. |
| |
| Args: |
| commit_check: Whether Checkstyle is being run on a specific commit. |
| modified_lines: A list of lines that has been modified. |
| line: The line that has a rule violation. |
| rule: The type of rule that a given line is violating. |
| test_class: Whether the file being checked is a test class. |
| test_data_class: Whether the file being check is a class used as test data. |
| |
| Returns: |
| A boolean whether a given line should be skipped in the reporting. |
| """ |
| # None modified_lines means checked file is new and nothing should be skipped. |
| if test_data_class: |
| return True |
| if test_class and rule in SKIPPED_RULES_FOR_TEST_FILES: |
| return True |
| if not commit_check: |
| return False |
| if modified_lines is None: |
| return False |
| return line not in modified_lines and rule not in FORCED_RULES |
| |
| |
| def _GetModifiedFiles(commit, explicit_commit=False, out=sys.stdout): |
| root = git.repository_root() |
| pending_files = git.modified_files(root, True) |
| if pending_files and not explicit_commit: |
| out.write(ERROR_UNCOMMITTED) |
| sys.exit(1) |
| |
| modified_files = git.modified_files(root, True, commit) |
| modified_files = {f: modified_files[f] for f |
| in modified_files if f.endswith('.java')} |
| return modified_files |
| |
| |
| def _FilterFiles(files, file_whitelist): |
| if not file_whitelist: |
| return files |
| return {f: files[f] for f in files |
| for whitelist in file_whitelist if whitelist in f} |
| |
| |
| def _GetTempFilesForCommit(file_names, commit): |
| """Creates a temporary snapshot of the files in at a commit. |
| |
| Retrieves the state of every file in file_names at a given commit and writes |
| them all out to a temporary directory. |
| |
| Args: |
| file_names: A list of files that need to be retrieved. |
| commit: A full 40 character SHA-1 of a commit. |
| |
| Returns: |
| A tuple of temprorary directory name and a directionary of |
| temp_file_name: filename. For example: |
| |
| ('/tmp/random/', {'/tmp/random/blarg.java': 'real/path/to/file.java' } |
| """ |
| tmp_dir_name = tempfile.mkdtemp() |
| tmp_file_names = {} |
| for file_name in file_names: |
| rel_path = os.path.relpath(file_name) |
| content = subprocess.check_output( |
| ['git', 'show', commit + ':' + rel_path]) |
| |
| tmp_file_name = os.path.join(tmp_dir_name, rel_path) |
| # create directory for the file if it doesn't exist |
| if not os.path.exists(os.path.dirname(tmp_file_name)): |
| os.makedirs(os.path.dirname(tmp_file_name)) |
| |
| tmp_file = open(tmp_file_name, 'w') |
| tmp_file.write(content) |
| tmp_file.close() |
| tmp_file_names[tmp_file_name] = file_name |
| return tmp_dir_name, tmp_file_names |
| |
| |
| def main(args=None): |
| """Runs Checkstyle checks on a given set of java files or a commit. |
| |
| It will run Checkstyle on the list of java files first, if unspecified, |
| then the check will be run on a specified commit SHA-1 and if that |
| is None it will fallback to check the latest commit of the currently checked |
| out branch. |
| """ |
| parser = argparse.ArgumentParser() |
| parser.add_argument('--file', '-f', nargs='+') |
| parser.add_argument('--sha', '-s') |
| parser.add_argument('--config_xml', '-c') |
| parser.add_argument('--file_whitelist', '-fw', nargs='+') |
| parser.add_argument('--add_classpath', '-p') |
| args = parser.parse_args() |
| |
| config_xml = args.config_xml or CHECKSTYLE_STYLE |
| |
| if not os.path.exists(config_xml): |
| print 'Java checkstyle configuration file is missing' |
| sys.exit(1) |
| |
| classpath = CHECKSTYLE_JAR |
| |
| if args.add_classpath: |
| classpath = args.add_classpath + ':' + classpath |
| |
| if args.file: |
| # Files to check were specified via command line. |
| (errors, warnings) = RunCheckstyleOnFiles(args.file, classpath, config_xml) |
| else: |
| (errors, warnings) = RunCheckstyleOnACommit(args.sha, classpath, config_xml, |
| args.file_whitelist) |
| |
| if errors or warnings: |
| sys.exit(1) |
| |
| print 'SUCCESS! NO ISSUES FOUND' |
| sys.exit(0) |
| |
| |
| if __name__ == '__main__': |
| main() |