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