blob: 06e239066e15c2ecbcdd0bf9a85e87d471d709a4 [file] [log] [blame]
Ember Rose54a6f912021-10-27 16:44:14 -04001#!/usr/bin/env python3
Aurimas Liutikas0383fc22016-12-14 19:09:31 -08002
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
Luca Stefani4ebbfec2020-02-09 14:08:24 +010021from __future__ import print_function
22
Aurimas Liutikas0383fc22016-12-14 19:09:31 -080023import argparse
24import errno
25import os
26import shutil
27import subprocess
28import sys
29import tempfile
30import xml.dom.minidom
31import gitlint.git as git
32
Aurimas Liutikas4a7cc7e2017-01-05 17:41:25 -080033
Aurimas Liutikas0383fc22016-12-14 19:09:31 -080034def _FindFoldersContaining(root, wanted):
Aurimas Liutikas4a7cc7e2017-01-05 17:41:25 -080035 """Recursively finds directories that have a file with the given name.
Aurimas Liutikas0383fc22016-12-14 19:09:31 -080036
37 Args:
38 root: Root folder to start the search from.
39 wanted: The filename that we are looking for.
40
41 Returns:
42 List of folders that has a file with the given name
43 """
44
Aurimas Liutikas4a7cc7e2017-01-05 17:41:25 -080045 if not root:
46 return []
Aurimas Liutikas0383fc22016-12-14 19:09:31 -080047 if os.path.islink(root):
48 return []
49 result = []
Aurimas Liutikas4a7cc7e2017-01-05 17:41:25 -080050 for file_name in os.listdir(root):
51 file_path = os.path.join(root, file_name)
52 if os.path.isdir(file_path):
53 sub_result = _FindFoldersContaining(file_path, wanted)
54 result.extend(sub_result)
Aurimas Liutikas0383fc22016-12-14 19:09:31 -080055 else:
Aurimas Liutikas4a7cc7e2017-01-05 17:41:25 -080056 if file_name == wanted:
Aurimas Liutikas0383fc22016-12-14 19:09:31 -080057 result.append(root)
58 return result
59
60MAIN_DIRECTORY = os.path.normpath(os.path.dirname(__file__))
61CHECKSTYLE_JAR = os.path.join(MAIN_DIRECTORY, 'checkstyle.jar')
62CHECKSTYLE_STYLE = os.path.join(MAIN_DIRECTORY, 'android-style.xml')
63FORCED_RULES = ['com.puppycrawl.tools.checkstyle.checks.imports.ImportOrderCheck',
64 'com.puppycrawl.tools.checkstyle.checks.imports.UnusedImportsCheck']
65SKIPPED_RULES_FOR_TEST_FILES = ['com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTypeCheck',
66 'com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocMethodCheck']
Nandana Dutt167a9602020-11-24 10:01:27 +000067SUBPATH_FOR_TEST_FILES = ['/tests/', '/test/', '/androidTest/', '/perftests/', '/gts-tests/',
Aurimas Liutikas6b2c40c2022-03-17 23:33:39 +000068 '/hostsidetests/', '/jvmTest/']
Aurimas Liutikas4a7cc7e2017-01-05 17:41:25 -080069SUBPATH_FOR_TEST_DATA_FILES = _FindFoldersContaining(git.repository_root(),
70 'IGNORE_CHECKSTYLE')
Aurimas Liutikas0383fc22016-12-14 19:09:31 -080071ERROR_UNCOMMITTED = 'You need to commit all modified files before running Checkstyle\n'
72ERROR_UNTRACKED = 'You have untracked java files that are not being checked:\n'
73
74
75def RunCheckstyleOnFiles(java_files, classpath=CHECKSTYLE_JAR, config_xml=CHECKSTYLE_STYLE):
76 """Runs Checkstyle checks on a given set of java_files.
77
78 Args:
79 java_files: A list of files to check.
80 classpath: The colon-delimited list of JARs in the classpath.
81 config_xml: Path of the checkstyle XML configuration file.
82
83 Returns:
84 A tuple of errors and warnings.
85 """
Daniel Kutikc52010a2018-04-10 22:09:13 +080086 print('Running Checkstyle on inputted files')
Luca Stefani4ebbfec2020-02-09 14:08:24 +010087 java_files = list(map(os.path.abspath, java_files))
Aurimas Liutikas0383fc22016-12-14 19:09:31 -080088 stdout = _ExecuteCheckstyle(java_files, classpath, config_xml)
89 (errors, warnings) = _ParseAndFilterOutput(stdout)
90 _PrintErrorsAndWarnings(errors, warnings)
91 return errors, warnings
92
93
94def RunCheckstyleOnACommit(commit,
95 classpath=CHECKSTYLE_JAR,
96 config_xml=CHECKSTYLE_STYLE,
97 file_whitelist=None):
98 """Runs Checkstyle checks on a given commit.
99
100 It will run Checkstyle on the changed Java files in a specified commit SHA-1
101 and if that is None it will fallback to check the latest commit of the
102 currently checked out branch.
103
104 Args:
105 commit: A full 40 character SHA-1 of a commit to check.
106 classpath: The colon-delimited list of JARs in the classpath.
107 config_xml: Path of the checkstyle XML configuration file.
108 file_whitelist: A list of whitelisted file paths that should be checked.
109
110 Returns:
111 A tuple of errors and warnings.
112 """
Aurimas Liutikas4a7cc7e2017-01-05 17:41:25 -0800113 if not git.repository_root():
Daniel Kutikc52010a2018-04-10 22:09:13 +0800114 print('FAILURE: not inside a git repository')
Aurimas Liutikas4a7cc7e2017-01-05 17:41:25 -0800115 sys.exit(1)
Aurimas Liutikas0383fc22016-12-14 19:09:31 -0800116 explicit_commit = commit is not None
117 if not explicit_commit:
118 _WarnIfUntrackedFiles()
119 commit = git.last_commit()
Daniel Kutikc52010a2018-04-10 22:09:13 +0800120 print('Running Checkstyle on %s commit' % commit)
Aurimas Liutikas0383fc22016-12-14 19:09:31 -0800121 commit_modified_files = _GetModifiedFiles(commit, explicit_commit)
122 commit_modified_files = _FilterFiles(commit_modified_files, file_whitelist)
Luca Stefani4ebbfec2020-02-09 14:08:24 +0100123 if not list(commit_modified_files.keys()):
Daniel Kutikc52010a2018-04-10 22:09:13 +0800124 print('No Java files to check')
Aurimas Liutikas0383fc22016-12-14 19:09:31 -0800125 return [], []
126
127 (tmp_dir, tmp_file_map) = _GetTempFilesForCommit(
Luca Stefani4ebbfec2020-02-09 14:08:24 +0100128 list(commit_modified_files.keys()), commit)
Aurimas Liutikas0383fc22016-12-14 19:09:31 -0800129
Luca Stefani4ebbfec2020-02-09 14:08:24 +0100130 java_files = list(tmp_file_map.keys())
Aurimas Liutikas0383fc22016-12-14 19:09:31 -0800131 stdout = _ExecuteCheckstyle(java_files, classpath, config_xml)
132
133 # Remove all the temporary files.
134 shutil.rmtree(tmp_dir)
135
136 (errors, warnings) = _ParseAndFilterOutput(stdout,
137 commit,
138 commit_modified_files,
139 tmp_file_map)
140 _PrintErrorsAndWarnings(errors, warnings)
141 return errors, warnings
142
143
144def _WarnIfUntrackedFiles(out=sys.stdout):
145 """Prints a warning and a list of untracked files if needed."""
146 root = git.repository_root()
147 untracked_files = git.modified_files(root, False)
148 untracked_files = {f for f in untracked_files if f.endswith('.java')}
149 if untracked_files:
150 out.write(ERROR_UNTRACKED)
151 for untracked_file in untracked_files:
152 out.write(untracked_file + '\n')
153 out.write('\n')
154
155
156def _PrintErrorsAndWarnings(errors, warnings):
157 """Prints given errors and warnings."""
158 if errors:
Luca Stefani4ebbfec2020-02-09 14:08:24 +0100159 print('ERRORS:\n' + '\n'.join(errors))
Aurimas Liutikas0383fc22016-12-14 19:09:31 -0800160 if warnings:
Luca Stefani4ebbfec2020-02-09 14:08:24 +0100161 print('WARNINGS:\n' + '\n'.join(warnings))
Aurimas Liutikas0383fc22016-12-14 19:09:31 -0800162
163
164def _ExecuteCheckstyle(java_files, classpath, config_xml):
165 """Runs Checkstyle to check give Java files for style errors.
166
167 Args:
168 java_files: A list of Java files that needs to be checked.
169 classpath: The colon-delimited list of JARs in the classpath.
170 config_xml: Path of the checkstyle XML configuration file.
171
172 Returns:
173 Checkstyle output in XML format.
174 """
175 # Run checkstyle
176 checkstyle_env = os.environ.copy()
177 checkstyle_env['JAVA_CMD'] = 'java'
Daniel Kutikc52010a2018-04-10 22:09:13 +0800178
Aurimas Liutikas0383fc22016-12-14 19:09:31 -0800179 try:
180 check = subprocess.Popen(['java', '-cp', classpath,
181 'com.puppycrawl.tools.checkstyle.Main', '-c',
182 config_xml, '-f', 'xml'] + java_files,
Luca Stefani4ebbfec2020-02-09 14:08:24 +0100183 stdout=subprocess.PIPE, env=checkstyle_env,
184 universal_newlines=True)
Aurimas Liutikas0383fc22016-12-14 19:09:31 -0800185 stdout, _ = check.communicate()
Sam Saccone222ffec2020-07-09 23:19:46 +0000186 stdout_lines = stdout.splitlines()
Daniel Kutikc52010a2018-04-10 22:09:13 +0800187 # A work-around for Checkstyle printing error count to stdio.
Sam Saccone222ffec2020-07-09 23:19:46 +0000188 if len(stdout_lines) < 2:
189 stdout = stdout_lines[0]
190 elif len(stdout_lines) >= 2 and '</checkstyle>' in stdout_lines[-2]:
191 stdout = '\n'.join(stdout_lines[:-1])
Daniel Kutikc52010a2018-04-10 22:09:13 +0800192 return stdout
Aurimas Liutikas0383fc22016-12-14 19:09:31 -0800193 except OSError as e:
194 if e.errno == errno.ENOENT:
Daniel Kutikc52010a2018-04-10 22:09:13 +0800195 print('Error running Checkstyle!')
Aurimas Liutikas0383fc22016-12-14 19:09:31 -0800196 sys.exit(1)
197
Aurimas Liutikas0383fc22016-12-14 19:09:31 -0800198
199def _ParseAndFilterOutput(stdout,
200 sha=None,
201 commit_modified_files=None,
202 tmp_file_map=None):
203 result_errors = []
204 result_warnings = []
205 root = xml.dom.minidom.parseString(stdout)
206 for file_element in root.getElementsByTagName('file'):
207 file_name = file_element.attributes['name'].value
208 if tmp_file_map:
209 file_name = tmp_file_map[file_name]
210 modified_lines = None
211 if commit_modified_files:
212 modified_lines = git.modified_lines(file_name,
213 commit_modified_files[file_name],
214 sha)
215 test_class = any(substring in file_name for substring
216 in SUBPATH_FOR_TEST_FILES)
217 test_data_class = any(substring in file_name for substring
218 in SUBPATH_FOR_TEST_DATA_FILES)
219 file_name = os.path.relpath(file_name)
220 errors = file_element.getElementsByTagName('error')
221 for error in errors:
222 line = int(error.attributes['line'].value)
223 rule = error.attributes['source'].value
224 if _ShouldSkip(commit_modified_files, modified_lines, line, rule,
225 test_class, test_data_class):
226 continue
227
228 column = ''
229 if error.hasAttribute('column'):
230 column = '%s:' % error.attributes['column'].value
231 message = error.attributes['message'].value
232 project = ''
233 if os.environ.get('REPO_PROJECT'):
234 project = '[' + os.environ.get('REPO_PROJECT') + '] '
235
236 result = ' %s%s:%s:%s %s' % (project, file_name, line, column, message)
237
238 severity = error.attributes['severity'].value
239 if severity == 'error':
240 result_errors.append(result)
241 elif severity == 'warning':
242 result_warnings.append(result)
243 return result_errors, result_warnings
244
245
246def _ShouldSkip(commit_check, modified_lines, line, rule, test_class=False,
247 test_data_class=False):
248 """Returns whether an error on a given line should be skipped.
249
250 Args:
251 commit_check: Whether Checkstyle is being run on a specific commit.
252 modified_lines: A list of lines that has been modified.
253 line: The line that has a rule violation.
254 rule: The type of rule that a given line is violating.
255 test_class: Whether the file being checked is a test class.
256 test_data_class: Whether the file being check is a class used as test data.
257
258 Returns:
259 A boolean whether a given line should be skipped in the reporting.
260 """
261 # None modified_lines means checked file is new and nothing should be skipped.
262 if test_data_class:
263 return True
264 if test_class and rule in SKIPPED_RULES_FOR_TEST_FILES:
265 return True
266 if not commit_check:
267 return False
268 if modified_lines is None:
269 return False
270 return line not in modified_lines and rule not in FORCED_RULES
271
272
273def _GetModifiedFiles(commit, explicit_commit=False, out=sys.stdout):
274 root = git.repository_root()
275 pending_files = git.modified_files(root, True)
276 if pending_files and not explicit_commit:
277 out.write(ERROR_UNCOMMITTED)
278 sys.exit(1)
279
280 modified_files = git.modified_files(root, True, commit)
281 modified_files = {f: modified_files[f] for f
282 in modified_files if f.endswith('.java')}
283 return modified_files
284
285
286def _FilterFiles(files, file_whitelist):
287 if not file_whitelist:
288 return files
289 return {f: files[f] for f in files
290 for whitelist in file_whitelist if whitelist in f}
291
292
293def _GetTempFilesForCommit(file_names, commit):
294 """Creates a temporary snapshot of the files in at a commit.
295
296 Retrieves the state of every file in file_names at a given commit and writes
297 them all out to a temporary directory.
298
299 Args:
300 file_names: A list of files that need to be retrieved.
301 commit: A full 40 character SHA-1 of a commit.
302
303 Returns:
304 A tuple of temprorary directory name and a directionary of
305 temp_file_name: filename. For example:
306
307 ('/tmp/random/', {'/tmp/random/blarg.java': 'real/path/to/file.java' }
308 """
309 tmp_dir_name = tempfile.mkdtemp()
310 tmp_file_names = {}
311 for file_name in file_names:
312 rel_path = os.path.relpath(file_name)
313 content = subprocess.check_output(
314 ['git', 'show', commit + ':' + rel_path])
315
316 tmp_file_name = os.path.join(tmp_dir_name, rel_path)
317 # create directory for the file if it doesn't exist
318 if not os.path.exists(os.path.dirname(tmp_file_name)):
319 os.makedirs(os.path.dirname(tmp_file_name))
320
Luca Stefani4ebbfec2020-02-09 14:08:24 +0100321 tmp_file = open(tmp_file_name, 'wb')
Aurimas Liutikas0383fc22016-12-14 19:09:31 -0800322 tmp_file.write(content)
323 tmp_file.close()
324 tmp_file_names[tmp_file_name] = file_name
325 return tmp_dir_name, tmp_file_names
326
327
328def main(args=None):
329 """Runs Checkstyle checks on a given set of java files or a commit.
330
331 It will run Checkstyle on the list of java files first, if unspecified,
332 then the check will be run on a specified commit SHA-1 and if that
333 is None it will fallback to check the latest commit of the currently checked
334 out branch.
335 """
336 parser = argparse.ArgumentParser()
337 parser.add_argument('--file', '-f', nargs='+')
338 parser.add_argument('--sha', '-s')
339 parser.add_argument('--config_xml', '-c')
340 parser.add_argument('--file_whitelist', '-fw', nargs='+')
341 parser.add_argument('--add_classpath', '-p')
342 args = parser.parse_args()
343
344 config_xml = args.config_xml or CHECKSTYLE_STYLE
345
346 if not os.path.exists(config_xml):
Daniel Kutikc52010a2018-04-10 22:09:13 +0800347 print('Java checkstyle configuration file is missing')
Aurimas Liutikas0383fc22016-12-14 19:09:31 -0800348 sys.exit(1)
349
350 classpath = CHECKSTYLE_JAR
351
352 if args.add_classpath:
353 classpath = args.add_classpath + ':' + classpath
354
355 if args.file:
356 # Files to check were specified via command line.
357 (errors, warnings) = RunCheckstyleOnFiles(args.file, classpath, config_xml)
358 else:
359 (errors, warnings) = RunCheckstyleOnACommit(args.sha, classpath, config_xml,
360 args.file_whitelist)
361
362 if errors or warnings:
363 sys.exit(1)
364
Daniel Kutikc52010a2018-04-10 22:09:13 +0800365 print('SUCCESS! NO ISSUES FOUND')
Aurimas Liutikas0383fc22016-12-14 19:09:31 -0800366 sys.exit(0)
367
368
369if __name__ == '__main__':
370 main()