xref: /spdk/scripts/check_format.sh (revision cc6920a4763d4b9a43aa40583c8397d8f14fa100)
1#!/usr/bin/env bash
2
3rootdir=$(readlink -f "$(dirname "$0")")/..
4source "$rootdir/scripts/common.sh"
5
6cd "$rootdir"
7
8# exit on errors
9set -e
10
11if ! hash nproc 2> /dev/null; then
12
13	function nproc() {
14		echo 8
15	}
16
17fi
18
19function version_lt() {
20	[ $(echo -e "$1\n$2" | sort -V | head -1) != "$1" ]
21}
22
23function array_contains_string() {
24	name="$1[@]"
25	array=("${!name}")
26
27	for element in "${array[@]}"; do
28		if [ "$element" = "$2" ]; then
29			return $(true)
30		fi
31	done
32
33	return $(false)
34}
35
36rc=0
37
38function check_permissions() {
39	echo -n "Checking file permissions..."
40
41	local rc=0
42
43	while read -r perm _res0 _res1 path; do
44		if [ ! -f "$path" ]; then
45			continue
46		fi
47
48		# Skip symlinks
49		if [[ -L $path ]]; then
50			continue
51		fi
52		fname=$(basename -- "$path")
53
54		case ${fname##*.} in
55			c | h | cpp | cc | cxx | hh | hpp | md | html | js | json | svg | Doxyfile | yml | LICENSE | README | conf | in | Makefile | mk | gitignore | go | txt)
56				# These file types should never be executable
57				if [ "$perm" -eq 100755 ]; then
58					echo "ERROR: $path is marked executable but is a code file."
59					rc=1
60				fi
61				;;
62			*)
63				shebang=$(head -n 1 $path | cut -c1-3)
64
65				# git only tracks the execute bit, so will only ever return 755 or 644 as the permission.
66				if [ "$perm" -eq 100755 ]; then
67					# If the file has execute permission, it should start with a shebang.
68					if [ "$shebang" != "#!/" ]; then
69						echo "ERROR: $path is marked executable but does not start with a shebang."
70						rc=1
71					fi
72				else
73					# If the file doesnot have execute permissions, it should not start with a shebang.
74					if [ "$shebang" = "#!/" ]; then
75						echo "ERROR: $path is not marked executable but starts with a shebang."
76						rc=1
77					fi
78				fi
79				;;
80		esac
81
82	done <<< "$(git grep -I --name-only --untracked -e . | git ls-files -s)"
83
84	if [ $rc -eq 0 ]; then
85		echo " OK"
86	fi
87
88	return $rc
89}
90
91function check_c_style() {
92	local rc=0
93
94	if hash astyle; then
95		echo -n "Checking coding style..."
96		if [ "$(astyle -V)" \< "Artistic Style Version 3" ]; then
97			echo -n " Your astyle version is too old so skipping coding style checks. Please update astyle to at least 3.0.1 version..."
98		else
99			rm -f astyle.log
100			touch astyle.log
101			# Exclude rte_vhost code imported from DPDK - we want to keep the original code
102			#  as-is to enable ongoing work to synch with a generic upstream DPDK vhost library,
103			#  rather than making diffs more complicated by a lot of changes to follow SPDK
104			#  coding standards.
105			git ls-files '*.[ch]' '*.cpp' '*.cc' '*.cxx' '*.hh' '*.hpp' \
106				| grep -v rte_vhost | grep -v cpp_headers \
107				| xargs -P$(nproc) -n10 astyle --options=.astylerc >> astyle.log
108			if grep -q "^Formatted" astyle.log; then
109				echo " errors detected"
110				git diff --ignore-submodules=all
111				sed -i -e 's/  / /g' astyle.log
112				grep --color=auto "^Formatted.*" astyle.log
113				echo "Incorrect code style detected in one or more files."
114				echo "The files have been automatically formatted."
115				echo "Remember to add the files to your commit."
116				rc=1
117			else
118				echo " OK"
119			fi
120			rm -f astyle.log
121		fi
122	else
123		echo "You do not have astyle installed so your code style is not being checked!"
124	fi
125	return $rc
126}
127
128function check_comment_style() {
129	local rc=0
130
131	echo -n "Checking comment style..."
132
133	git grep --line-number -e '\/[*][^ *-]' -- '*.[ch]' > comment.log || true
134	git grep --line-number -e '[^ ][*]\/' -- '*.[ch]' ':!lib/rte_vhost*/*' >> comment.log || true
135	git grep --line-number -e '^[*]' -- '*.[ch]' >> comment.log || true
136	git grep --line-number -e '\s\/\/' -- '*.[ch]' >> comment.log || true
137	git grep --line-number -e '^\/\/' -- '*.[ch]' >> comment.log || true
138
139	if [ -s comment.log ]; then
140		echo " Incorrect comment formatting detected"
141		cat comment.log
142		rc=1
143	else
144		echo " OK"
145	fi
146	rm -f comment.log
147
148	return $rc
149}
150
151function check_spaces_before_tabs() {
152	local rc=0
153
154	echo -n "Checking for spaces before tabs..."
155	git grep --line-number $' \t' -- './*' ':!*.patch' > whitespace.log || true
156	if [ -s whitespace.log ]; then
157		echo " Spaces before tabs detected"
158		cat whitespace.log
159		rc=1
160	else
161		echo " OK"
162	fi
163	rm -f whitespace.log
164
165	return $rc
166}
167
168function check_trailing_whitespace() {
169	local rc=0
170
171	echo -n "Checking trailing whitespace in output strings..."
172
173	git grep --line-number -e ' \\n"' -- '*.[ch]' > whitespace.log || true
174
175	if [ -s whitespace.log ]; then
176		echo " Incorrect trailing whitespace detected"
177		cat whitespace.log
178		rc=1
179	else
180		echo " OK"
181	fi
182	rm -f whitespace.log
183
184	return $rc
185}
186
187function check_forbidden_functions() {
188	local rc=0
189
190	echo -n "Checking for use of forbidden library functions..."
191
192	git grep --line-number -w '\(atoi\|atol\|atoll\|strncpy\|strcpy\|strcat\|sprintf\|vsprintf\)' -- './*.c' ':!lib/rte_vhost*/**' > badfunc.log || true
193	if [ -s badfunc.log ]; then
194		echo " Forbidden library functions detected"
195		cat badfunc.log
196		rc=1
197	else
198		echo " OK"
199	fi
200	rm -f badfunc.log
201
202	return $rc
203}
204
205function check_cunit_style() {
206	local rc=0
207
208	echo -n "Checking for use of forbidden CUnit macros..."
209
210	git grep --line-number -w 'CU_ASSERT_FATAL' -- 'test/*' ':!test/spdk_cunit.h' > badcunit.log || true
211	if [ -s badcunit.log ]; then
212		echo " Forbidden CU_ASSERT_FATAL usage detected - use SPDK_CU_ASSERT_FATAL instead"
213		cat badcunit.log
214		rc=1
215	else
216		echo " OK"
217	fi
218	rm -f badcunit.log
219
220	return $rc
221}
222
223function check_eof() {
224	local rc=0
225
226	echo -n "Checking blank lines at end of file..."
227
228	if ! git grep -I -l -e . -z './*' ':!*.patch' \
229		| xargs -0 -P$(nproc) -n1 scripts/eofnl > eofnl.log; then
230		echo " Incorrect end-of-file formatting detected"
231		cat eofnl.log
232		rc=1
233	else
234		echo " OK"
235	fi
236	rm -f eofnl.log
237
238	return $rc
239}
240
241function check_posix_includes() {
242	local rc=0
243
244	echo -n "Checking for POSIX includes..."
245	git grep -I -i -f scripts/posix.txt -- './*' ':!include/spdk/stdinc.h' ':!include/linux/**' ':!lib/rte_vhost*/**' ':!scripts/posix.txt' ':!*.patch' > scripts/posix.log || true
246	if [ -s scripts/posix.log ]; then
247		echo "POSIX includes detected. Please include spdk/stdinc.h instead."
248		cat scripts/posix.log
249		rc=1
250	else
251		echo " OK"
252	fi
253	rm -f scripts/posix.log
254
255	return $rc
256}
257
258function check_naming_conventions() {
259	local rc=0
260
261	echo -n "Checking for proper function naming conventions..."
262	# commit_to_compare = HEAD - 1.
263	commit_to_compare="$(git log --pretty=oneline --skip=1 -n 1 | awk '{print $1}')"
264	failed_naming_conventions=false
265	changed_c_libs=()
266	declared_symbols=()
267
268	# Build an array of all the modified C files.
269	mapfile -t changed_c_libs < <(git diff --name-only HEAD $commit_to_compare -- lib/**/*.c module/**/*.c)
270	# Matching groups are 1. qualifiers / return type. 2. function name 3. argument list / comments and stuff after that.
271	# Capture just the names of newly added (or modified) function definitions.
272	mapfile -t declared_symbols < <(git diff -U0 $commit_to_compare HEAD -- include/spdk*/*.h | sed -En 's/(^[+].*)(spdk[a-z,A-Z,0-9,_]*)(\(.*)/\2/p')
273
274	for c_file in "${changed_c_libs[@]}"; do
275		lib_map_file="mk/spdk_blank.map"
276		defined_symbols=()
277		removed_symbols=()
278		exported_symbols=()
279		if ls "$(dirname $c_file)"/*.map &> /dev/null; then
280			lib_map_file="$(ls "$(dirname $c_file)"/*.map)"
281		fi
282		# Matching groups are 1. leading +sign. 2, function name 3. argument list / anything after that.
283		# Capture just the names of newly added (or modified) functions that start with "spdk_"
284		mapfile -t defined_symbols < <(git diff -U0 $commit_to_compare HEAD -- $c_file | sed -En 's/(^[+])(spdk[a-z,A-Z,0-9,_]*)(\(.*)/\2/p')
285		# Capture the names of removed symbols to catch edge cases where we just move definitions around.
286		mapfile -t removed_symbols < <(git diff -U0 $commit_to_compare HEAD -- $c_file | sed -En 's/(^[-])(spdk[a-z,A-Z,0-9,_]*)(\(.*)/\2/p')
287		for symbol in "${removed_symbols[@]}"; do
288			for i in "${!defined_symbols[@]}"; do
289				if [[ ${defined_symbols[i]} = "$symbol" ]]; then
290					unset -v 'defined_symbols[i]'
291				fi
292			done
293		done
294		# It's possible that we just modified a functions arguments so unfortunately we can't just look at changed lines in this function.
295		# matching groups are 1. All leading whitespace 2. function name. Capture just the symbol name.
296		mapfile -t exported_symbols < <(sed -En 's/(^[[:space:]]*)(spdk[a-z,A-Z,0-9,_]*);/\2/p' < $lib_map_file)
297		for defined_symbol in "${defined_symbols[@]}"; do
298			# if the list of defined symbols is equal to the list of removed symbols, then we are left with a single empty element. skip it.
299			if [ "$defined_symbol" = '' ]; then
300				continue
301			fi
302			not_exported=true
303			not_declared=true
304			if array_contains_string exported_symbols $defined_symbol; then
305				not_exported=false
306			fi
307
308			if array_contains_string declared_symbols $defined_symbol; then
309				not_declared=false
310			fi
311
312			if $not_exported || $not_declared; then
313				if ! $failed_naming_conventions; then
314					echo " found naming convention errors."
315				fi
316				echo "function $defined_symbol starts with spdk_ which is reserved for public API functions."
317				echo "Please add this function to its corresponding map file and a public header or remove the spdk_ prefix."
318				failed_naming_conventions=true
319				rc=1
320			fi
321		done
322	done
323
324	if ! $failed_naming_conventions; then
325		echo " OK"
326	fi
327
328	return $rc
329}
330
331function check_include_style() {
332	local rc=0
333
334	echo -n "Checking #include style..."
335	git grep -I -i --line-number "#include <spdk/" -- '*.[ch]' > scripts/includes.log || true
336	if [ -s scripts/includes.log ]; then
337		echo "Incorrect #include syntax. #includes of spdk/ files should use quotes."
338		cat scripts/includes.log
339		rc=1
340	else
341		echo " OK"
342	fi
343	rm -f scripts/includes.log
344
345	return $rc
346}
347
348function check_python_style() {
349	local rc=0
350
351	if hash pycodestyle 2> /dev/null; then
352		PEP8=pycodestyle
353	elif hash pep8 2> /dev/null; then
354		PEP8=pep8
355	fi
356
357	if [ -n "${PEP8}" ]; then
358		echo -n "Checking Python style..."
359
360		PEP8_ARGS+=" --max-line-length=140"
361
362		error=0
363		git ls-files '*.py' | xargs -P$(nproc) -n1 $PEP8 $PEP8_ARGS > pep8.log || error=1
364		if [ $error -ne 0 ]; then
365			echo " Python formatting errors detected"
366			cat pep8.log
367			rc=1
368		else
369			echo " OK"
370		fi
371		rm -f pep8.log
372	else
373		echo "You do not have pycodestyle or pep8 installed so your Python style is not being checked!"
374	fi
375
376	return $rc
377}
378
379function get_bash_files() {
380	local sh shebang
381
382	mapfile -t sh < <(git ls-files '*.sh')
383	mapfile -t shebang < <(git grep -l '^#!.*bash')
384
385	printf '%s\n' "${sh[@]}" "${shebang[@]}" | sort -u
386}
387
388function check_bash_style() {
389	local rc=0
390
391	# find compatible shfmt binary
392	shfmt_bins=$(compgen -c | grep '^shfmt' || true)
393	for bin in $shfmt_bins; do
394		if version_lt "$("$bin" --version)" "3.1.0"; then
395			shfmt=$bin
396			break
397		fi
398	done
399
400	if [ -n "$shfmt" ]; then
401		shfmt_cmdline=() sh_files=()
402
403		mapfile -t sh_files < <(get_bash_files)
404
405		if ((${#sh_files[@]})); then
406			printf 'Checking .sh formatting style...'
407
408			shfmt_cmdline+=(-i 0)     # indent_style = tab|indent_size = 0
409			shfmt_cmdline+=(-bn)      # binary_next_line = true
410			shfmt_cmdline+=(-ci)      # switch_case_indent = true
411			shfmt_cmdline+=(-ln bash) # shell_variant = bash (default)
412			shfmt_cmdline+=(-d)       # diffOut - print diff of the changes and exit with != 0
413			shfmt_cmdline+=(-sr)      # redirect operators will be followed by a space
414
415			diff=${output_dir:-$PWD}/$shfmt.patch
416
417			# Explicitly tell shfmt to not look for .editorconfig. .editorconfig is also not looked up
418			# in case any formatting arguments has been passed on its cmdline.
419			if ! SHFMT_NO_EDITORCONFIG=true "$shfmt" "${shfmt_cmdline[@]}" "${sh_files[@]}" > "$diff"; then
420				# In case shfmt detects an actual syntax error it will write out a proper message on
421				# its stderr, hence the diff file should remain empty.
422				if [[ -s $diff ]]; then
423					diff_out=$(< "$diff")
424				fi
425
426				cat <<- ERROR_SHFMT
427
428					* Errors in style formatting have been detected.
429					${diff_out:+* Please, review the generated patch at $diff
430
431					# _START_OF_THE_DIFF
432
433					${diff_out:-ERROR}
434
435					# _END_OF_THE_DIFF
436					}
437
438				ERROR_SHFMT
439				rc=1
440			else
441				rm -f "$diff"
442				printf ' OK\n'
443			fi
444		fi
445	else
446		echo "shfmt not detected, Bash style formatting check is skipped"
447	fi
448
449	return $rc
450}
451
452function check_bash_static_analysis() {
453	local rc=0
454
455	if hash shellcheck 2> /dev/null; then
456		echo -n "Checking Bash style..."
457
458		shellcheck_v=$(shellcheck --version | grep -P "version: [0-9\.]+" | cut -d " " -f2)
459
460		# SHCK_EXCLUDE contains a list of all of the spellcheck errors found in SPDK scripts
461		# currently. New errors should only be added to this list if the cost of fixing them
462		# is deemed too high. For more information about the errors, go to:
463		# https://github.com/koalaman/shellcheck/wiki/Checks
464		# Error descriptions can also be found at: https://github.com/koalaman/shellcheck/wiki
465		# SPDK fails some error checks which have been deprecated in later versions of shellcheck.
466		# We will not try to fix these error checks, but instead just leave the error types here
467		# so that we can still run with older versions of shellcheck.
468		SHCK_EXCLUDE="SC1117"
469		# SPDK has decided to not fix violations of these errors.
470		# We are aware about below exclude list and we want this errors to be excluded.
471		# SC1083: This {/} is literal. Check expression (missing ;/\n?) or quote it.
472		# SC1090: Can't follow non-constant source. Use a directive to specify location.
473		# SC1091: Not following: (error message here)
474		# SC2001: See if you can use ${variable//search/replace} instead.
475		# SC2010: Don't use ls | grep. Use a glob or a for loop with a condition to allow non-alphanumeric filenames.
476		# SC2015: Note that A && B || C is not if-then-else. C may run when A is true.
477		# SC2016: Expressions don't expand in single quotes, use double quotes for that.
478		# SC2034: foo appears unused. Verify it or export it.
479		# SC2046: Quote this to prevent word splitting.
480		# SC2086: Double quote to prevent globbing and word splitting.
481		# SC2119: Use foo "$@" if function's $1 should mean script's $1.
482		# SC2120: foo references arguments, but none are ever passed.
483		# SC2128: Expanding an array without an index only gives the first element.
484		# SC2148: Add shebang to the top of your script.
485		# SC2153: Possible Misspelling: MYVARIABLE may not be assigned, but MY_VARIABLE is.
486		# SC2154: var is referenced but not assigned.
487		# SC2164: Use cd ... || exit in case cd fails.
488		# SC2174: When used with -p, -m only applies to the deepest directory.
489		# SC2178: Variable was used as an array but is now assigned a string.
490		# SC2206: Quote to prevent word splitting/globbing,
491		#         or split robustly with mapfile or read -a.
492		# SC2207: Prefer mapfile or read -a to split command output (or quote to avoid splitting).
493		# SC2223: This default assignment may cause DoS due to globbing. Quote it.
494		SHCK_EXCLUDE="$SHCK_EXCLUDE,SC1083,SC1090,SC1091,SC2010,SC2015,SC2016,SC2034,SC2046,SC2086,\
495SC2119,SC2120,SC2128,SC2148,SC2153,SC2154,SC2164,SC2174,SC2178,SC2001,SC2206,SC2207,SC2223"
496
497		SHCK_FORMAT="tty"
498		SHCK_APPLY=false
499		SHCH_ARGS="-e $SHCK_EXCLUDE -f $SHCK_FORMAT"
500
501		if ge "$shellcheck_v" 0.4.0; then
502			SHCH_ARGS+=" -x"
503		else
504			echo "shellcheck $shellcheck_v detected, recommended >= 0.4.0."
505		fi
506
507		get_bash_files | xargs -P$(nproc) -n1 shellcheck $SHCH_ARGS &> shellcheck.log
508		if [[ -s shellcheck.log ]]; then
509			echo " Bash formatting errors detected!"
510
511			cat shellcheck.log
512			if $SHCK_APPLY; then
513				git apply shellcheck.log
514				echo "Bash errors were automatically corrected."
515				echo "Please remember to add the changes to your commit."
516			fi
517			rc=1
518		else
519			echo " OK"
520		fi
521		rm -f shellcheck.log
522	else
523		echo "You do not have shellcheck installed so your Bash style is not being checked!"
524	fi
525
526	return $rc
527}
528
529function check_changelog() {
530	local rc=0
531
532	# Check if any of the public interfaces were modified by this patch.
533	# Warn the user to consider updating the changelog any changes
534	# are detected.
535	echo -n "Checking whether CHANGELOG.md should be updated..."
536	staged=$(git diff --name-only --cached .)
537	working=$(git status -s --porcelain --ignore-submodules | grep -iv "??" | awk '{print $2}')
538	files="$staged $working"
539	if [[ "$files" = " " ]]; then
540		files=$(git diff-tree --no-commit-id --name-only -r HEAD)
541	fi
542
543	has_changelog=0
544	for f in $files; do
545		if [[ $f == CHANGELOG.md ]]; then
546			# The user has a changelog entry, so exit.
547			has_changelog=1
548			break
549		fi
550	done
551
552	needs_changelog=0
553	if [ $has_changelog -eq 0 ]; then
554		for f in $files; do
555			if [[ $f == include/spdk/* ]] || [[ $f == scripts/rpc.py ]] || [[ $f == etc/* ]]; then
556				echo ""
557				echo -n "$f was modified. Consider updating CHANGELOG.md."
558				needs_changelog=1
559			fi
560		done
561	fi
562
563	if [ $needs_changelog -eq 0 ]; then
564		echo " OK"
565	else
566		echo ""
567	fi
568
569	return $rc
570}
571
572function check_json_rpc() {
573	local rc=0
574
575	echo -n "Checking that all RPCs are documented..."
576	while IFS='"' read -r _ rpc _; do
577		if ! grep -q "^### $rpc" doc/jsonrpc.md; then
578			echo "Missing JSON-RPC documentation for ${rpc}"
579			rc=1
580		fi
581	done < <(git grep -h -E "^SPDK_RPC_REGISTER\(" ':!test/*')
582
583	if [ $rc -eq 0 ]; then
584		echo " OK"
585	fi
586	return $rc
587}
588
589function check_markdown_format() {
590	local rc=0
591
592	if hash mdl 2> /dev/null; then
593		echo -n "Checking markdown files format..."
594		mdl -g -s $rootdir/mdl_rules.rb . > mdl.log || true
595		if [ -s mdl.log ]; then
596			echo " Errors in .md files detected:"
597			cat mdl.log
598			rc=1
599		else
600			echo " OK"
601		fi
602		rm -f mdl.log
603	else
604		echo "You do not have markdownlint installed so .md files not being checked!"
605	fi
606
607	return $rc
608}
609
610function check_rpc_args() {
611	local rc=0
612
613	echo -n "Checking rpc.py argument option names..."
614	grep add_argument scripts/rpc.py | grep -oP "(?<=--)[a-z0-9\-\_]*(?=\')" | grep "_" > badargs.log
615
616	if [[ -s badargs.log ]]; then
617		echo "rpc.py arguments with underscores detected!"
618		cat badargs.log
619		echo "Please convert the underscores to dashes."
620		rc=1
621	else
622		echo " OK"
623	fi
624	rm -f badargs.log
625	return $rc
626}
627
628rc=0
629
630check_permissions || rc=1
631check_c_style || rc=1
632
633GIT_VERSION=$(git --version | cut -d' ' -f3)
634
635if version_lt "1.9.5" "${GIT_VERSION}"; then
636	# git <1.9.5 doesn't support pathspec magic exclude
637	echo " Your git version is too old to perform all tests. Please update git to at least 1.9.5 version..."
638	exit $rc
639fi
640
641check_comment_style || rc=1
642check_markdown_format || rc=1
643check_spaces_before_tabs || rc=1
644check_trailing_whitespace || rc=1
645check_forbidden_functions || rc=1
646check_cunit_style || rc=1
647check_eof || rc=1
648check_posix_includes || rc=1
649check_naming_conventions || rc=1
650check_include_style || rc=1
651check_python_style || rc=1
652check_bash_style || rc=1
653check_bash_static_analysis || rc=1
654check_changelog || rc=1
655check_json_rpc || rc=1
656check_rpc_args || rc=1
657
658exit $rc
659