371 lines
13 KiB
Python
Executable File
371 lines
13 KiB
Python
Executable File
#!/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."""
|
|
|
|
from __future__ import print_function
|
|
|
|
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/', '/perftests/', '/gts-tests/',
|
|
'/hostsidetests/']
|
|
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 = list(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 list(commit_modified_files.keys()):
|
|
print('No Java files to check')
|
|
return [], []
|
|
|
|
(tmp_dir, tmp_file_map) = _GetTempFilesForCommit(
|
|
list(commit_modified_files.keys()), commit)
|
|
|
|
java_files = list(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:\n' + '\n'.join(errors))
|
|
if warnings:
|
|
print('WARNINGS:\n' + '\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,
|
|
universal_newlines=True)
|
|
stdout, _ = check.communicate()
|
|
stdout_lines = stdout.splitlines()
|
|
# A work-around for Checkstyle printing error count to stdio.
|
|
if len(stdout_lines) < 2:
|
|
stdout = stdout_lines[0]
|
|
elif len(stdout_lines) >= 2 and '</checkstyle>' in stdout_lines[-2]:
|
|
stdout = '\n'.join(stdout_lines[:-1])
|
|
return stdout
|
|
except OSError as e:
|
|
if e.errno == errno.ENOENT:
|
|
print('Error running Checkstyle!')
|
|
sys.exit(1)
|
|
|
|
|
|
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, 'wb')
|
|
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()
|