blob: 39ca181a6645759e92ca76eb43035c3c6620d202 [file] [log] [blame]
Aurimas Liutikas0383fc22016-12-14 19:09:31 -08001#!/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
31def _FindFoldersContaining(root, wanted):
32 """Searches recursively from root to find directories that has a file with
33 the given name.
34
35 Args:
36 root: Root folder to start the search from.
37 wanted: The filename that we are looking for.
38
39 Returns:
40 List of folders that has a file with the given name
41 """
42
43 if os.path.islink(root):
44 return []
45 result = []
46 for fileName in os.listdir(root):
47 filePath = os.path.join(root, fileName)
48 if os.path.isdir(filePath):
49 subResult = _FindFoldersContaining(filePath, wanted)
50 result.extend(subResult)
51 else:
52 if fileName == wanted:
53 result.append(root)
54 return result
55
56MAIN_DIRECTORY = os.path.normpath(os.path.dirname(__file__))
57CHECKSTYLE_JAR = os.path.join(MAIN_DIRECTORY, 'checkstyle.jar')
58CHECKSTYLE_STYLE = os.path.join(MAIN_DIRECTORY, 'android-style.xml')
59FORCED_RULES = ['com.puppycrawl.tools.checkstyle.checks.imports.ImportOrderCheck',
60 'com.puppycrawl.tools.checkstyle.checks.imports.UnusedImportsCheck']
61SKIPPED_RULES_FOR_TEST_FILES = ['com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTypeCheck',
62 'com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocMethodCheck']
63SUBPATH_FOR_TEST_FILES = ['/tests/', '/test/', '/androidTest/']
64SUBPATH_FOR_TEST_DATA_FILES = _FindFoldersContaining(os.path.dirname(os.getcwd()),
65 "IGNORE_CHECKSTYLE")
66ERROR_UNCOMMITTED = 'You need to commit all modified files before running Checkstyle\n'
67ERROR_UNTRACKED = 'You have untracked java files that are not being checked:\n'
68
69
70def RunCheckstyleOnFiles(java_files, classpath=CHECKSTYLE_JAR, config_xml=CHECKSTYLE_STYLE):
71 """Runs Checkstyle checks on a given set of java_files.
72
73 Args:
74 java_files: A list of files to check.
75 classpath: The colon-delimited list of JARs in the classpath.
76 config_xml: Path of the checkstyle XML configuration file.
77
78 Returns:
79 A tuple of errors and warnings.
80 """
81 print 'Running Checkstyle on inputted files'
82 java_files = map(os.path.abspath, java_files)
83 stdout = _ExecuteCheckstyle(java_files, classpath, config_xml)
84 (errors, warnings) = _ParseAndFilterOutput(stdout)
85 _PrintErrorsAndWarnings(errors, warnings)
86 return errors, warnings
87
88
89def RunCheckstyleOnACommit(commit,
90 classpath=CHECKSTYLE_JAR,
91 config_xml=CHECKSTYLE_STYLE,
92 file_whitelist=None):
93 """Runs Checkstyle checks on a given commit.
94
95 It will run Checkstyle on the changed Java files in a specified commit SHA-1
96 and if that is None it will fallback to check the latest commit of the
97 currently checked out branch.
98
99 Args:
100 commit: A full 40 character SHA-1 of a commit to check.
101 classpath: The colon-delimited list of JARs in the classpath.
102 config_xml: Path of the checkstyle XML configuration file.
103 file_whitelist: A list of whitelisted file paths that should be checked.
104
105 Returns:
106 A tuple of errors and warnings.
107 """
108 explicit_commit = commit is not None
109 if not explicit_commit:
110 _WarnIfUntrackedFiles()
111 commit = git.last_commit()
112 print 'Running Checkstyle on %s commit' % commit
113 commit_modified_files = _GetModifiedFiles(commit, explicit_commit)
114 commit_modified_files = _FilterFiles(commit_modified_files, file_whitelist)
115 if not commit_modified_files.keys():
116 print 'No Java files to check'
117 return [], []
118
119 (tmp_dir, tmp_file_map) = _GetTempFilesForCommit(
120 commit_modified_files.keys(), commit)
121
122 java_files = tmp_file_map.keys()
123 stdout = _ExecuteCheckstyle(java_files, classpath, config_xml)
124
125 # Remove all the temporary files.
126 shutil.rmtree(tmp_dir)
127
128 (errors, warnings) = _ParseAndFilterOutput(stdout,
129 commit,
130 commit_modified_files,
131 tmp_file_map)
132 _PrintErrorsAndWarnings(errors, warnings)
133 return errors, warnings
134
135
136def _WarnIfUntrackedFiles(out=sys.stdout):
137 """Prints a warning and a list of untracked files if needed."""
138 root = git.repository_root()
139 untracked_files = git.modified_files(root, False)
140 untracked_files = {f for f in untracked_files if f.endswith('.java')}
141 if untracked_files:
142 out.write(ERROR_UNTRACKED)
143 for untracked_file in untracked_files:
144 out.write(untracked_file + '\n')
145 out.write('\n')
146
147
148def _PrintErrorsAndWarnings(errors, warnings):
149 """Prints given errors and warnings."""
150 if errors:
151 print 'ERRORS:'
152 print '\n'.join(errors)
153 if warnings:
154 print 'WARNINGS:'
155 print '\n'.join(warnings)
156
157
158def _ExecuteCheckstyle(java_files, classpath, config_xml):
159 """Runs Checkstyle to check give Java files for style errors.
160
161 Args:
162 java_files: A list of Java files that needs to be checked.
163 classpath: The colon-delimited list of JARs in the classpath.
164 config_xml: Path of the checkstyle XML configuration file.
165
166 Returns:
167 Checkstyle output in XML format.
168 """
169 # Run checkstyle
170 checkstyle_env = os.environ.copy()
171 checkstyle_env['JAVA_CMD'] = 'java'
172 try:
173 check = subprocess.Popen(['java', '-cp', classpath,
174 'com.puppycrawl.tools.checkstyle.Main', '-c',
175 config_xml, '-f', 'xml'] + java_files,
176 stdout=subprocess.PIPE, env=checkstyle_env)
177 stdout, _ = check.communicate()
178 except OSError as e:
179 if e.errno == errno.ENOENT:
180 print 'Error running Checkstyle!'
181 sys.exit(1)
182
183 # A work-around for Checkstyle printing error count to stdio.
184 if 'Checkstyle ends with' in stdout.splitlines()[-1]:
185 stdout = '\n'.join(stdout.splitlines()[:-1])
186 return stdout
187
188
189def _ParseAndFilterOutput(stdout,
190 sha=None,
191 commit_modified_files=None,
192 tmp_file_map=None):
193 result_errors = []
194 result_warnings = []
195 root = xml.dom.minidom.parseString(stdout)
196 for file_element in root.getElementsByTagName('file'):
197 file_name = file_element.attributes['name'].value
198 if tmp_file_map:
199 file_name = tmp_file_map[file_name]
200 modified_lines = None
201 if commit_modified_files:
202 modified_lines = git.modified_lines(file_name,
203 commit_modified_files[file_name],
204 sha)
205 test_class = any(substring in file_name for substring
206 in SUBPATH_FOR_TEST_FILES)
207 test_data_class = any(substring in file_name for substring
208 in SUBPATH_FOR_TEST_DATA_FILES)
209 file_name = os.path.relpath(file_name)
210 errors = file_element.getElementsByTagName('error')
211 for error in errors:
212 line = int(error.attributes['line'].value)
213 rule = error.attributes['source'].value
214 if _ShouldSkip(commit_modified_files, modified_lines, line, rule,
215 test_class, test_data_class):
216 continue
217
218 column = ''
219 if error.hasAttribute('column'):
220 column = '%s:' % error.attributes['column'].value
221 message = error.attributes['message'].value
222 project = ''
223 if os.environ.get('REPO_PROJECT'):
224 project = '[' + os.environ.get('REPO_PROJECT') + '] '
225
226 result = ' %s%s:%s:%s %s' % (project, file_name, line, column, message)
227
228 severity = error.attributes['severity'].value
229 if severity == 'error':
230 result_errors.append(result)
231 elif severity == 'warning':
232 result_warnings.append(result)
233 return result_errors, result_warnings
234
235
236def _ShouldSkip(commit_check, modified_lines, line, rule, test_class=False,
237 test_data_class=False):
238 """Returns whether an error on a given line should be skipped.
239
240 Args:
241 commit_check: Whether Checkstyle is being run on a specific commit.
242 modified_lines: A list of lines that has been modified.
243 line: The line that has a rule violation.
244 rule: The type of rule that a given line is violating.
245 test_class: Whether the file being checked is a test class.
246 test_data_class: Whether the file being check is a class used as test data.
247
248 Returns:
249 A boolean whether a given line should be skipped in the reporting.
250 """
251 # None modified_lines means checked file is new and nothing should be skipped.
252 if test_data_class:
253 return True
254 if test_class and rule in SKIPPED_RULES_FOR_TEST_FILES:
255 return True
256 if not commit_check:
257 return False
258 if modified_lines is None:
259 return False
260 return line not in modified_lines and rule not in FORCED_RULES
261
262
263def _GetModifiedFiles(commit, explicit_commit=False, out=sys.stdout):
264 root = git.repository_root()
265 pending_files = git.modified_files(root, True)
266 if pending_files and not explicit_commit:
267 out.write(ERROR_UNCOMMITTED)
268 sys.exit(1)
269
270 modified_files = git.modified_files(root, True, commit)
271 modified_files = {f: modified_files[f] for f
272 in modified_files if f.endswith('.java')}
273 return modified_files
274
275
276def _FilterFiles(files, file_whitelist):
277 if not file_whitelist:
278 return files
279 return {f: files[f] for f in files
280 for whitelist in file_whitelist if whitelist in f}
281
282
283def _GetTempFilesForCommit(file_names, commit):
284 """Creates a temporary snapshot of the files in at a commit.
285
286 Retrieves the state of every file in file_names at a given commit and writes
287 them all out to a temporary directory.
288
289 Args:
290 file_names: A list of files that need to be retrieved.
291 commit: A full 40 character SHA-1 of a commit.
292
293 Returns:
294 A tuple of temprorary directory name and a directionary of
295 temp_file_name: filename. For example:
296
297 ('/tmp/random/', {'/tmp/random/blarg.java': 'real/path/to/file.java' }
298 """
299 tmp_dir_name = tempfile.mkdtemp()
300 tmp_file_names = {}
301 for file_name in file_names:
302 rel_path = os.path.relpath(file_name)
303 content = subprocess.check_output(
304 ['git', 'show', commit + ':' + rel_path])
305
306 tmp_file_name = os.path.join(tmp_dir_name, rel_path)
307 # create directory for the file if it doesn't exist
308 if not os.path.exists(os.path.dirname(tmp_file_name)):
309 os.makedirs(os.path.dirname(tmp_file_name))
310
311 tmp_file = open(tmp_file_name, 'w')
312 tmp_file.write(content)
313 tmp_file.close()
314 tmp_file_names[tmp_file_name] = file_name
315 return tmp_dir_name, tmp_file_names
316
317
318def main(args=None):
319 """Runs Checkstyle checks on a given set of java files or a commit.
320
321 It will run Checkstyle on the list of java files first, if unspecified,
322 then the check will be run on a specified commit SHA-1 and if that
323 is None it will fallback to check the latest commit of the currently checked
324 out branch.
325 """
326 parser = argparse.ArgumentParser()
327 parser.add_argument('--file', '-f', nargs='+')
328 parser.add_argument('--sha', '-s')
329 parser.add_argument('--config_xml', '-c')
330 parser.add_argument('--file_whitelist', '-fw', nargs='+')
331 parser.add_argument('--add_classpath', '-p')
332 args = parser.parse_args()
333
334 config_xml = args.config_xml or CHECKSTYLE_STYLE
335
336 if not os.path.exists(config_xml):
337 print 'Java checkstyle configuration file is missing'
338 sys.exit(1)
339
340 classpath = CHECKSTYLE_JAR
341
342 if args.add_classpath:
343 classpath = args.add_classpath + ':' + classpath
344
345 if args.file:
346 # Files to check were specified via command line.
347 (errors, warnings) = RunCheckstyleOnFiles(args.file, classpath, config_xml)
348 else:
349 (errors, warnings) = RunCheckstyleOnACommit(args.sha, classpath, config_xml,
350 args.file_whitelist)
351
352 if errors or warnings:
353 sys.exit(1)
354
355 print 'SUCCESS! NO ISSUES FOUND'
356 sys.exit(0)
357
358
359if __name__ == '__main__':
360 main()