diff --git a/test/lint/lint-format-strings-tests-skip-arguments.txt b/test/lint/lint-format-strings-tests-skip-arguments.txt index 963394df12..6b141ae7a2 100644 --- a/test/lint/lint-format-strings-tests-skip-arguments.txt +++ b/test/lint/lint-format-strings-tests-skip-arguments.txt @@ -1,25 +1,26 @@ ++++++++++ With --skip_arguments=1 ++++++++++ The following tests will throw an error if skip-argument is not 1 --- Good ones --- -printme(skipped, "%d", 1); -printme(skipped, "%a%b%z", 1, "anything", 3); +fprintf(skipped, "%d", 1); +fprintf(skipped, "%a%b%z", 1, "anything", 3); -- Too many arguments --- -printme(skipped, "%d", 1, 2); => error +fprintf(skipped, "%d", 1, 2); => error --- Not enough argument --- -printme(skipped, "%d"); should throw => error +fprintf(skipped, "%d"); should throw => error ++++++++++ With --skip_arguments=2 ++++++++++ The following tests will throw an error if skip-argument is not 2 --- Good ones --- -printme(skip1, skip2, "%d", 1); -printme(skip1, skip2, "%a%b%z", 1, "anything", 3); +snprintf(skip1, skip2, "%d", 1); +snprintf(skip1, skip2, "%a%b%z", 1, "anything", 3); -- Too many arguments --- -printme(skip1, skip2, "%d", 1, 2); => error +snprintf(skip1, skip2, "%d", 1, 2); => error --- Not enough argument --- -printme(skip1, skip2, "%d"); should throw => error \ No newline at end of file +snprintf(skip1, skip2, "%d"); should throw => error +snprintf(skip1, "%d"); should throw => parse error \ No newline at end of file diff --git a/test/lint/lint-format-strings-tests.txt b/test/lint/lint-format-strings-tests.txt index d1456dc74b..b8b542c206 100644 --- a/test/lint/lint-format-strings-tests.txt +++ b/test/lint/lint-format-strings-tests.txt @@ -1,18 +1,18 @@ --- Good ones --- -printme("%d", 1); => no error -printme("%d%d%d", 1, 2, 3); => no error -printme("%a%b%z", 1, "anything", 3); => no error +printf("%d", 1); => no error +printf("%d%d%d", 1, 2, 3); => no error +printf("%a%b%z", 1, "anything", 3); => no error --- Comments --- -// This is a c++ comment, should not throw printme("%d", 1, 2); => no error +// This is a c++ comment, should not throw printf("%d", 1, 2); => no error --- Preprocessor --- -#define printme(%d, 1, 2); => no error +#define printf(%d, 1, 2); => no error -- Too many arguments --- -printme("%d", 1, 2); => error -printme("%a %b", 1, 2, "anything"); => error +printf("%d", 1, 2); => error +printf("%a %b", 1, 2, "anything"); => error --- Not enough argument --- -printme("%d"); should throw => error -printme("%a%b%z", 1, "anything"); => error \ No newline at end of file +printf("%d"); should throw => error +printf("%a%b%z", 1, "anything"); => error \ No newline at end of file diff --git a/test/lint/lint-format-strings.py b/test/lint/lint-format-strings.py index 99735a839e..550c843747 100755 --- a/test/lint/lint-format-strings.py +++ b/test/lint/lint-format-strings.py @@ -1,302 +1,293 @@ #!/usr/bin/env python3 # # Copyright (c) 2018 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. # # Lint format strings: This program checks that the number of arguments passed # to a variadic format string function matches the number of format specifiers # in the format string. import argparse import re import sys FALSE_POSITIVES = [ ("src/dbwrapper.cpp", "vsnprintf(p, limit - p, format, backup_ap)"), ("src/index/base.cpp", "FatalError(const char* fmt, const Args&... args)"), ("src/netbase.cpp", "LogConnectFailure(bool manual_connection, const char* fmt, const Args&... args)"), ("src/util.cpp", "strprintf(_(COPYRIGHT_HOLDERS), _(COPYRIGHT_HOLDERS_SUBSTITUTION))"), ("src/util.cpp", "strprintf(COPYRIGHT_HOLDERS, COPYRIGHT_HOLDERS_SUBSTITUTION)"), ("src/seeder/main.cpp", "fprintf(stderr, help, argv[0])"), ("src/tinyformat.h", "printf(const char *fmt, const Args &... args)"), ("src/tinyformat.h", "printf(const char *fmt, TINYFORMAT_VARARGS(n))"), ] +FUNCTION_NAMES_AND_NUMBER_OF_LEADING_ARGUMENTS = [ + ("FatalError", 0), + ("fprintf", 1), + ("LogConnectFailure", 1), + ("LogPrint", 1), + ("LogPrintf", 0), + ("printf", 0), + ("snprintf", 2), + ("sprintf", 1), + ("strprintf", 0), + ("vfprintf", 1), + ("vprintf", 1), + ("vsnprintf", 1), + ("vsprintf", 1), +] + def parse_function_calls(function_name, source_code): """Return an array with all calls to function function_name in string source_code. Preprocessor directives and C++ style comments ("//") in source_code are removed. >>> len(parse_function_calls("foo", "foo();bar();foo();bar();")) 2 >>> parse_function_calls("foo", "foo(1);bar(1);foo(2);bar(2);")[0].startswith("foo(1);") True >>> parse_function_calls("foo", "foo(1);bar(1);foo(2);bar(2);")[1].startswith("foo(2);") True >>> len(parse_function_calls("foo", "foo();bar();// foo();bar();")) 1 >>> len(parse_function_calls("foo", "#define FOO foo();")) 0 """ assert(type(function_name) is str and type( source_code) is str and function_name) lines = [re.sub("// .*", " ", line).strip() for line in source_code.split("\n") if not line.strip().startswith("#")] return re.findall(r"[^a-zA-Z_](?=({}\(.*).*)".format(function_name), " " + " ".join(lines)) def normalize(s): """Return a normalized version of string s with newlines, tabs and C style comments ("/* ... */") replaced with spaces. Multiple spaces are replaced with a single space. >>> normalize(" /* nothing */ foo\tfoo /* bar */ foo ") 'foo foo foo' """ assert(type(s) is str) s = s.replace("\n", " ") s = s.replace("\t", " ") s = re.sub("/\*.*?\*/", " ", s) s = re.sub(" {2,}", " ", s) return s.strip() ESCAPE_MAP = { r"\n": "[escaped-newline]", r"\t": "[escaped-tab]", r'\"': "[escaped-quote]", } def escape(s): """Return the escaped version of string s with "\\\"", "\\n" and "\\t" escaped as "[escaped-backslash]", "[escaped-newline]" and "[escaped-tab]". >>> unescape(escape("foo")) == "foo" True >>> escape(r'foo \\t foo \\n foo \\\\ foo \\ foo \\"bar\\"') 'foo [escaped-tab] foo [escaped-newline] foo \\\\\\\\ foo \\\\ foo [escaped-quote]bar[escaped-quote]' """ assert(type(s) is str) for raw_value, escaped_value in ESCAPE_MAP.items(): s = s.replace(raw_value, escaped_value) return s def unescape(s): """Return the unescaped version of escaped string s. Reverses the replacements made in function escape(s). >>> unescape(escape("bar")) 'bar' >>> unescape("foo [escaped-tab] foo [escaped-newline] foo \\\\\\\\ foo \\\\ foo [escaped-quote]bar[escaped-quote]") 'foo \\\\t foo \\\\n foo \\\\\\\\ foo \\\\ foo \\\\"bar\\\\"' """ assert(type(s) is str) for raw_value, escaped_value in ESCAPE_MAP.items(): s = s.replace(escaped_value, raw_value) return s def parse_function_call_and_arguments(function_name, function_call): """Split string function_call into an array of strings consisting of: * the string function_call followed by "(" * the function call argument #1 * ... * the function call argument #n * a trailing ");" The strings returned are in escaped form. See escape(...). >>> parse_function_call_and_arguments("foo", 'foo("%s", "foo");') ['foo(', '"%s",', ' "foo"', ')'] >>> parse_function_call_and_arguments("foo", 'foo("%s", "foo");') ['foo(', '"%s",', ' "foo"', ')'] >>> parse_function_call_and_arguments("foo", 'foo("%s %s", "foo", "bar");') ['foo(', '"%s %s",', ' "foo",', ' "bar"', ')'] >>> parse_function_call_and_arguments("fooprintf", 'fooprintf("%050d", i);') ['fooprintf(', '"%050d",', ' i', ')'] >>> parse_function_call_and_arguments("foo", 'foo(bar(foobar(barfoo("foo"))), foobar); barfoo') ['foo(', 'bar(foobar(barfoo("foo"))),', ' foobar', ')'] >>> parse_function_call_and_arguments("foo", "foo()") ['foo(', '', ')'] >>> parse_function_call_and_arguments("foo", "foo(123)") ['foo(', '123', ')'] >>> parse_function_call_and_arguments("foo", 'foo("foo")') ['foo(', '"foo"', ')'] """ assert(type(function_name) is str and type( function_call) is str and function_name) remaining = normalize(escape(function_call)) expected_function_call = "{}(".format(function_name) assert(remaining.startswith(expected_function_call)) parts = [expected_function_call] remaining = remaining[len(expected_function_call):] open_parentheses = 1 in_string = False parts.append("") for char in remaining: parts.append(parts.pop() + char) if char == "\"": in_string = not in_string continue if in_string: continue if char == "(": open_parentheses += 1 continue if char == ")": open_parentheses -= 1 if open_parentheses > 1: continue if open_parentheses == 0: parts.append(parts.pop()[:-1]) parts.append(char) break if char == ",": parts.append("") return parts def parse_string_content(argument): """Return the text within quotes in string argument. >>> parse_string_content('1 "foo %d bar" 2') 'foo %d bar' >>> parse_string_content('1 foobar 2') '' >>> parse_string_content('1 "bar" 2') 'bar' >>> parse_string_content('1 "foo" 2 "bar" 3') 'foobar' >>> parse_string_content('1 "foo" 2 " " "bar" 3') 'foo bar' >>> parse_string_content('""') '' >>> parse_string_content('') '' >>> parse_string_content('1 2 3') '' """ assert(type(argument) is str) string_content = "" in_string = False for char in normalize(escape(argument)): if char == "\"": in_string = not in_string elif in_string: string_content += char return string_content def count_format_specifiers(format_string): """Return the number of format specifiers in string format_string. >>> count_format_specifiers("foo bar foo") 0 >>> count_format_specifiers("foo %d bar foo") 1 >>> count_format_specifiers("foo %d bar %i foo") 2 >>> count_format_specifiers("foo %d bar %i foo %% foo") 2 >>> count_format_specifiers("foo %d bar %i foo %% foo %d foo") 3 >>> count_format_specifiers("foo %d bar %i foo %% foo %*d foo") 4 """ assert(type(format_string) is str) n = 0 in_specifier = False for i, char in enumerate(format_string): if format_string[i - 1:i + 1] == "%%" or format_string[i:i + 2] == "%%": pass elif char == "%": in_specifier = True n += 1 elif char in "aAcdeEfFgGinopsuxX": in_specifier = False elif in_specifier and char == "*": n += 1 return n def main(args_in): """ Return a string output with information on string format errors - >>> main(["printit", "test/lint/lint-format-strings-tests.txt"]) - 0 - - >>> main(["printme", "test/lint/lint-format-strings-tests.txt"]) - test/lint/lint-format-strings-tests.txt: Expected 1 argument(s) after format string but found 2 argument(s): printme("%d", 1, 2) - test/lint/lint-format-strings-tests.txt: Expected 2 argument(s) after format string but found 3 argument(s): printme("%a %b", 1, 2, "anything") - test/lint/lint-format-strings-tests.txt: Expected 1 argument(s) after format string but found 0 argument(s): printme("%d") - test/lint/lint-format-strings-tests.txt: Expected 3 argument(s) after format string but found 2 argument(s): printme("%a%b%z", 1, "anything") - 1 - - >>> main(["printme", "test/lint/lint-format-strings-tests-skip-arguments.txt"]) - test/lint/lint-format-strings-tests-skip-arguments.txt: Expected 0 argument(s) after format string but found 2 argument(s): printme(skipped, "%d", 1) - test/lint/lint-format-strings-tests-skip-arguments.txt: Expected 0 argument(s) after format string but found 4 argument(s): printme(skipped, "%a%b%z", 1, "anything", 3) - test/lint/lint-format-strings-tests-skip-arguments.txt: Expected 0 argument(s) after format string but found 3 argument(s): printme(skipped, "%d", 1, 2) - test/lint/lint-format-strings-tests-skip-arguments.txt: Expected 0 argument(s) after format string but found 1 argument(s): printme(skipped, "%d") - test/lint/lint-format-strings-tests-skip-arguments.txt: Expected 0 argument(s) after format string but found 3 argument(s): printme(skip1, skip2, "%d", 1) - test/lint/lint-format-strings-tests-skip-arguments.txt: Expected 0 argument(s) after format string but found 5 argument(s): printme(skip1, skip2, "%a%b%z", 1, "anything", 3) - test/lint/lint-format-strings-tests-skip-arguments.txt: Expected 0 argument(s) after format string but found 4 argument(s): printme(skip1, skip2, "%d", 1, 2) - test/lint/lint-format-strings-tests-skip-arguments.txt: Expected 0 argument(s) after format string but found 2 argument(s): printme(skip1, skip2, "%d") - 1 - - >>> main(["--skip-arguments=1", "printme", "test/lint/lint-format-strings-tests-skip-arguments.txt"]) - test/lint/lint-format-strings-tests-skip-arguments.txt: Expected 1 argument(s) after format string but found 2 argument(s): printme(skipped, "%d", 1, 2) - test/lint/lint-format-strings-tests-skip-arguments.txt: Expected 1 argument(s) after format string but found 0 argument(s): printme(skipped, "%d") - test/lint/lint-format-strings-tests-skip-arguments.txt: Expected 0 argument(s) after format string but found 2 argument(s): printme(skip1, skip2, "%d", 1) - test/lint/lint-format-strings-tests-skip-arguments.txt: Expected 0 argument(s) after format string but found 4 argument(s): printme(skip1, skip2, "%a%b%z", 1, "anything", 3) - test/lint/lint-format-strings-tests-skip-arguments.txt: Expected 0 argument(s) after format string but found 3 argument(s): printme(skip1, skip2, "%d", 1, 2) - test/lint/lint-format-strings-tests-skip-arguments.txt: Expected 0 argument(s) after format string but found 1 argument(s): printme(skip1, skip2, "%d") + >>> main(["test/lint/lint-format-strings-tests.txt"]) + test/lint/lint-format-strings-tests.txt: Expected 1 argument(s) after format string but found 2 argument(s): printf("%d", 1, 2) + test/lint/lint-format-strings-tests.txt: Expected 2 argument(s) after format string but found 3 argument(s): printf("%a %b", 1, 2, "anything") + test/lint/lint-format-strings-tests.txt: Expected 1 argument(s) after format string but found 0 argument(s): printf("%d") + test/lint/lint-format-strings-tests.txt: Expected 3 argument(s) after format string but found 2 argument(s): printf("%a%b%z", 1, "anything") 1 - >>> main(["--skip-arguments=2", "printme", "test/lint/lint-format-strings-tests-skip-arguments.txt"]) - test/lint/lint-format-strings-tests-skip-arguments.txt: Expected 0 argument(s) after format string but found 2 argument(s): printme(skipped, "%a%b%z", 1, "anything", 3) - test/lint/lint-format-strings-tests-skip-arguments.txt: Expected 0 argument(s) after format string but found 1 argument(s): printme(skipped, "%d", 1, 2) - test/lint/lint-format-strings-tests-skip-arguments.txt: Could not parse function call string "printme(...)": printme(skipped, "%d") - test/lint/lint-format-strings-tests-skip-arguments.txt: Expected 1 argument(s) after format string but found 2 argument(s): printme(skip1, skip2, "%d", 1, 2) - test/lint/lint-format-strings-tests-skip-arguments.txt: Expected 1 argument(s) after format string but found 0 argument(s): printme(skip1, skip2, "%d") + >>> main(["test/lint/lint-format-strings-tests-skip-arguments.txt"]) + test/lint/lint-format-strings-tests-skip-arguments.txt: Expected 1 argument(s) after format string but found 2 argument(s): fprintf(skipped, "%d", 1, 2) + test/lint/lint-format-strings-tests-skip-arguments.txt: Expected 1 argument(s) after format string but found 0 argument(s): fprintf(skipped, "%d") + test/lint/lint-format-strings-tests-skip-arguments.txt: Expected 1 argument(s) after format string but found 2 argument(s): snprintf(skip1, skip2, "%d", 1, 2) + test/lint/lint-format-strings-tests-skip-arguments.txt: Expected 1 argument(s) after format string but found 0 argument(s): snprintf(skip1, skip2, "%d") + test/lint/lint-format-strings-tests-skip-arguments.txt: Could not parse function call string "snprintf(...)": snprintf(skip1, "%d") 1 """ parser = argparse.ArgumentParser(description="This program checks that the number of arguments passed " "to a variadic format string function matches the number of format " "specifiers in the format string.") - parser.add_argument("--skip-arguments", type=int, help="number of arguments before the format string " - "argument (e.g. 1 in the case of fprintf)", default=0) - parser.add_argument( - "function_name", help="function name (e.g. fprintf)", default=None) parser.add_argument("file", type=argparse.FileType( "r", encoding="utf-8"), nargs="*", help="C++ source code file (e.g. foo.cpp)") args = parser.parse_args(args_in) exit_code = 0 for f in args.file: - for function_call_str in parse_function_calls(args.function_name, f.read()): - parts = parse_function_call_and_arguments( - args.function_name, function_call_str) - relevant_function_call_str = unescape("".join(parts))[:512] - if (f.name, relevant_function_call_str) in FALSE_POSITIVES: - continue - if len(parts) < 3 + args.skip_arguments: - exit_code = 1 - print("{}: Could not parse function call string \"{}(...)\": {}".format( - f.name, args.function_name, relevant_function_call_str)) - continue - argument_count = len(parts) - 3 - args.skip_arguments - format_str = parse_string_content(parts[1 + args.skip_arguments]) - format_specifier_count = count_format_specifiers(format_str) - if format_specifier_count != argument_count: - exit_code = 1 - print("{}: Expected {} argument(s) after format string but found {} argument(s): {}".format( - f.name, format_specifier_count, argument_count, relevant_function_call_str)) - continue + file_content = f.read() + for (function_name, skip_arguments) in FUNCTION_NAMES_AND_NUMBER_OF_LEADING_ARGUMENTS: + for function_call_str in parse_function_calls(function_name, file_content): + parts = parse_function_call_and_arguments( + function_name, function_call_str) + relevant_function_call_str = unescape("".join(parts))[:512] + if (f.name, relevant_function_call_str) in FALSE_POSITIVES: + continue + if len(parts) < 3 + skip_arguments: + exit_code = 1 + print("{}: Could not parse function call string \"{}(...)\": {}".format( + f.name, function_name, relevant_function_call_str)) + continue + argument_count = len(parts) - 3 - skip_arguments + format_str = parse_string_content(parts[1 + skip_arguments]) + format_specifier_count = count_format_specifiers(format_str) + if format_specifier_count != argument_count: + exit_code = 1 + print("{}: Expected {} argument(s) after format string but found {} argument(s): {}".format( + f.name, format_specifier_count, argument_count, relevant_function_call_str)) + continue return exit_code if __name__ == "__main__": sys.exit(main(sys.argv[1:])) diff --git a/test/lint/lint-format-strings.sh b/test/lint/lint-format-strings.sh index 2fae1a61e8..1f56f48180 100755 --- a/test/lint/lint-format-strings.sh +++ b/test/lint/lint-format-strings.sh @@ -1,38 +1,20 @@ #!/usr/bin/env bash # # Copyright (c) 2018 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. # # Lint format strings: This program checks that the number of arguments passed # to a variadic format string function matches the number of format specifiers # in the format string. export LC_ALL=C -FUNCTION_NAMES_AND_NUMBER_OF_LEADING_ARGUMENTS=( - FatalError,0 - fprintf,1 - LogConnectFailure,1 - LogPrint,1 - LogPrintf,0 - printf,0 - snprintf,2 - sprintf,1 - strprintf,0 - vfprintf,1 - vprintf,1 - vsnprintf,1 - vsprintf,1 -) - EXIT_CODE=0 if ! python3 -m doctest test/lint/lint-format-strings.py; then EXIT_CODE=1 fi -for S in "${FUNCTION_NAMES_AND_NUMBER_OF_LEADING_ARGUMENTS[@]}"; do - IFS="," read -r FUNCTION_NAME SKIP_ARGUMENTS <<< "${S}" - test/lint/lint-format-strings.py --skip-arguments "${SKIP_ARGUMENTS}" "${FUNCTION_NAME}" "${1}" -done + +test/lint/lint-format-strings.py "${1}" exit ${EXIT_CODE}