1#!/usr/bin/python 2 3# 4# Copyright 2015, The Android Open Source Project 5# 6# Licensed under the Apache License, Version 2.0 (the "License"); 7# you may not use this file except in compliance with the License. 8# You may obtain a copy of the License at 9# 10# http://www.apache.org/licenses/LICENSE-2.0 11# 12# Unless required by applicable law or agreed to in writing, software 13# distributed under the License is distributed on an "AS IS" BASIS, 14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15# See the License for the specific language governing permissions and 16# limitations under the License. 17# 18 19"""Script that is used by developers to run style checks on Java files.""" 20 21import argparse 22import errno 23import os 24import shutil 25import subprocess 26import sys 27import tempfile 28import xml.dom.minidom 29import gitlint.git as git 30 31 32MAIN_DIRECTORY = os.path.normpath(os.path.dirname(__file__)) 33CHECKSTYLE_JAR = os.path.join(MAIN_DIRECTORY, 'checkstyle.jar') 34CHECKSTYLE_STYLE = os.path.join(MAIN_DIRECTORY, 'android-style.xml') 35FORCED_RULES = ['com.puppycrawl.tools.checkstyle.checks.imports.ImportOrderCheck', 36 'com.puppycrawl.tools.checkstyle.checks.imports.UnusedImportsCheck'] 37SKIPPED_RULES_FOR_TEST_FILES = ['com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTypeCheck'] 38SUBPATH_FOR_TEST_FILES = ['/tests/java/', '/tests/src/'] 39ERROR_UNCOMMITTED = 'You need to commit all modified files before running Checkstyle\n' 40ERROR_UNTRACKED = 'You have untracked java files that are not being checked:\n' 41 42 43def RunCheckstyleOnFiles(java_files, config_xml=CHECKSTYLE_STYLE): 44 """Runs Checkstyle checks on a given set of java_files. 45 46 Args: 47 java_files: A list of files to check. 48 config_xml: Path of the checkstyle XML configuration file. 49 50 Returns: 51 A tuple of errors and warnings. 52 """ 53 print 'Running Checkstyle on inputted files' 54 java_files = map(os.path.abspath, java_files) 55 stdout = _ExecuteCheckstyle(java_files, config_xml) 56 (errors, warnings) = _ParseAndFilterOutput(stdout) 57 _PrintErrorsAndWarnings(errors, warnings) 58 return errors, warnings 59 60 61def RunCheckstyleOnACommit(commit, config_xml=CHECKSTYLE_STYLE): 62 """Runs Checkstyle checks on a given commit. 63 64 It will run Checkstyle on the changed Java files in a specified commit SHA-1 65 and if that is None it will fallback to check the latest commit of the 66 currently checked out branch. 67 68 Args: 69 commit: A full 40 character SHA-1 of a commit to check. 70 config_xml: Path of the checkstyle XML configuration file. 71 72 Returns: 73 A tuple of errors and warnings. 74 """ 75 if not commit: 76 _WarnIfUntrackedFiles() 77 commit = git.last_commit() 78 print 'Running Checkstyle on %s commit' % commit 79 commit_modified_files = _GetModifiedFiles(commit) 80 if not commit_modified_files.keys(): 81 print 'No Java files to check' 82 return [], [] 83 84 (tmp_dir, tmp_file_map) = _GetTempFilesForCommit( 85 commit_modified_files.keys(), commit) 86 87 java_files = tmp_file_map.keys() 88 stdout = _ExecuteCheckstyle(java_files, config_xml) 89 90 # Remove all the temporary files. 91 shutil.rmtree(tmp_dir) 92 93 (errors, warnings) = _ParseAndFilterOutput(stdout, 94 commit, 95 commit_modified_files, 96 tmp_file_map) 97 _PrintErrorsAndWarnings(errors, warnings) 98 return errors, warnings 99 100 101def _WarnIfUntrackedFiles(out=sys.stdout): 102 """Prints a warning and a list of untracked files if needed.""" 103 root = git.repository_root() 104 untracked_files = git.modified_files(root, False) 105 untracked_files = {f for f in untracked_files if f.endswith('.java')} 106 if untracked_files: 107 out.write(ERROR_UNTRACKED) 108 for untracked_file in untracked_files: 109 out.write(untracked_file + '\n') 110 out.write('\n') 111 112 113def _PrintErrorsAndWarnings(errors, warnings): 114 """Prints given errors and warnings.""" 115 if errors: 116 print 'ERRORS:' 117 print '\n'.join(errors) 118 if warnings: 119 print 'WARNINGS:' 120 print '\n'.join(warnings) 121 122 123def _ExecuteCheckstyle(java_files, config_xml): 124 """Runs Checkstyle to check give Java files for style errors. 125 126 Args: 127 java_files: A list of Java files that needs to be checked. 128 config_xml: Path of the checkstyle XML configuration file. 129 130 Returns: 131 Checkstyle output in XML format. 132 """ 133 # Run checkstyle 134 checkstyle_env = os.environ.copy() 135 checkstyle_env['JAVA_CMD'] = 'java' 136 try: 137 check = subprocess.Popen(['java', '-cp', 138 CHECKSTYLE_JAR, 139 'com.puppycrawl.tools.checkstyle.Main', '-c', 140 config_xml, '-f', 'xml'] + java_files, 141 stdout=subprocess.PIPE, env=checkstyle_env) 142 stdout, _ = check.communicate() 143 except OSError as e: 144 if e.errno == errno.ENOENT: 145 print 'Error running Checkstyle!' 146 sys.exit(1) 147 148 # A work-around for Checkstyle printing error count to stdio. 149 if 'Checkstyle ends with' in stdout.splitlines()[-1]: 150 stdout = '\n'.join(stdout.splitlines()[:-1]) 151 return stdout 152 153 154def _ParseAndFilterOutput(stdout, 155 sha=None, 156 commit_modified_files=None, 157 tmp_file_map=None): 158 result_errors = [] 159 result_warnings = [] 160 root = xml.dom.minidom.parseString(stdout) 161 for file_element in root.getElementsByTagName('file'): 162 file_name = file_element.attributes['name'].value 163 if tmp_file_map: 164 file_name = tmp_file_map[file_name] 165 modified_lines = None 166 if commit_modified_files: 167 modified_lines = git.modified_lines(file_name, 168 commit_modified_files[file_name], 169 sha) 170 test_class = any(substring in file_name for substring 171 in SUBPATH_FOR_TEST_FILES) 172 file_name = os.path.relpath(file_name) 173 errors = file_element.getElementsByTagName('error') 174 for error in errors: 175 line = int(error.attributes['line'].value) 176 rule = error.attributes['source'].value 177 if _ShouldSkip(commit_modified_files, modified_lines, line, rule, 178 test_class): 179 continue 180 181 column = '' 182 if error.hasAttribute('column'): 183 column = '%s:' % error.attributes['column'].value 184 message = error.attributes['message'].value 185 result = ' %s:%s:%s %s' % (file_name, line, column, message) 186 187 severity = error.attributes['severity'].value 188 if severity == 'error': 189 result_errors.append(result) 190 elif severity == 'warning': 191 result_warnings.append(result) 192 return result_errors, result_warnings 193 194 195def _ShouldSkip(commit_check, modified_lines, line, rule, test_class=False): 196 """Returns whether an error on a given line should be skipped. 197 198 Args: 199 commit_check: Whether Checkstyle is being run on a specific commit. 200 modified_lines: A list of lines that has been modified. 201 line: The line that has a rule violation. 202 rule: The type of rule that a given line is violating. 203 test_class: Whether the file being checked is a test class. 204 205 Returns: 206 A boolean whether a given line should be skipped in the reporting. 207 """ 208 # None modified_lines means checked file is new and nothing should be skipped. 209 if test_class and rule in SKIPPED_RULES_FOR_TEST_FILES: 210 return True 211 if not commit_check: 212 return False 213 if modified_lines is None: 214 return False 215 return line not in modified_lines and rule not in FORCED_RULES 216 217 218def _GetModifiedFiles(commit, out=sys.stdout): 219 root = git.repository_root() 220 pending_files = git.modified_files(root, True) 221 if pending_files: 222 out.write(ERROR_UNCOMMITTED) 223 sys.exit(1) 224 225 modified_files = git.modified_files(root, True, commit) 226 modified_files = {f: modified_files[f] for f 227 in modified_files if f.endswith('.java')} 228 return modified_files 229 230 231def _GetTempFilesForCommit(file_names, commit): 232 """Creates a temporary snapshot of the files in at a commit. 233 234 Retrieves the state of every file in file_names at a given commit and writes 235 them all out to a temporary directory. 236 237 Args: 238 file_names: A list of files that need to be retrieved. 239 commit: A full 40 character SHA-1 of a commit. 240 241 Returns: 242 A tuple of temprorary directory name and a directionary of 243 temp_file_name: filename. For example: 244 245 ('/tmp/random/', {'/tmp/random/blarg.java': 'real/path/to/file.java' } 246 """ 247 tmp_dir_name = tempfile.mkdtemp() 248 tmp_file_names = {} 249 for file_name in file_names: 250 rel_path = os.path.relpath(file_name) 251 content = subprocess.check_output( 252 ['git', 'show', commit + ':' + rel_path]) 253 254 tmp_file_name = os.path.join(tmp_dir_name, rel_path) 255 # create directory for the file if it doesn't exist 256 if not os.path.exists(os.path.dirname(tmp_file_name)): 257 os.makedirs(os.path.dirname(tmp_file_name)) 258 259 tmp_file = open(tmp_file_name, 'w') 260 tmp_file.write(content) 261 tmp_file.close() 262 tmp_file_names[tmp_file_name] = file_name 263 return tmp_dir_name, tmp_file_names 264 265 266def main(args=None): 267 """Runs Checkstyle checks on a given set of java files or a commit. 268 269 It will run Checkstyle on the list of java files first, if unspecified, 270 then the check will be run on a specified commit SHA-1 and if that 271 is None it will fallback to check the latest commit of the currently checked 272 out branch. 273 """ 274 parser = argparse.ArgumentParser() 275 parser.add_argument('--file', '-f', nargs='+') 276 parser.add_argument('--sha', '-s') 277 parser.add_argument('--config_xml', '-c') 278 args = parser.parse_args() 279 280 config_xml = args.config_xml or CHECKSTYLE_STYLE 281 if not os.path.exists(config_xml): 282 print 'Java checkstyle configuration file is missing' 283 sys.exit(1) 284 285 if args.file: 286 # Files to check were specified via command line. 287 (errors, warnings) = RunCheckstyleOnFiles(args.file, config_xml) 288 else: 289 (errors, warnings) = RunCheckstyleOnACommit(args.sha, config_xml) 290 291 if errors or warnings: 292 sys.exit(1) 293 294 print 'SUCCESS! NO ISSUES FOUND' 295 sys.exit(0) 296 297 298if __name__ == '__main__': 299 main() 300