Changeset View
Changeset View
Standalone View
Standalone View
test/lint/lint-format-strings.py
Show All 10 Lines | |||||
import argparse | import argparse | ||||
import doctest | import doctest | ||||
import re | import re | ||||
import sys | import sys | ||||
FALSE_POSITIVES = [ | FALSE_POSITIVES = [ | ||||
("src/dbwrapper.cpp", "vsnprintf(p, limit - p, format, backup_ap)"), | ("src/dbwrapper.cpp", "vsnprintf(p, limit - p, format, backup_ap)"), | ||||
("src/index/base.cpp", "FatalError(const char *fmt, const Args &...args)"), | ("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/system.cpp", | "src/netbase.cpp", | ||||
"strprintf(_(COPYRIGHT_HOLDERS).translated, COPYRIGHT_HOLDERS_SUBSTITUTION)"), | ( | ||||
("src/validationinterface.cpp", | "LogConnectFailure(bool manual_connection, const char *fmt, const Args" | ||||
"LogPrint(BCLog::VALIDATION, fmt \"\\n\", __VA_ARGS__)"), | " &...args)" | ||||
), | |||||
), | |||||
( | |||||
"src/util/system.cpp", | |||||
"strprintf(_(COPYRIGHT_HOLDERS).translated, COPYRIGHT_HOLDERS_SUBSTITUTION)", | |||||
), | |||||
( | |||||
"src/validationinterface.cpp", | |||||
'LogPrint(BCLog::VALIDATION, fmt "\\n", __VA_ARGS__)', | |||||
), | |||||
("src/tinyformat.h", "printf(const char *fmt, const Args &...args)"), | ("src/tinyformat.h", "printf(const char *fmt, const Args &...args)"), | ||||
("src/tinyformat.h", "printf(const char *fmt, TINYFORMAT_VARARGS(n))"), | ("src/tinyformat.h", "printf(const char *fmt, TINYFORMAT_VARARGS(n))"), | ||||
("src/wallet/wallet.h", | ( | ||||
"LogPrintf((\"%s \" + fmt).c_str(), GetDisplayName(), parameters...)"), | "src/wallet/wallet.h", | ||||
("src/wallet/scriptpubkeyman.h", | 'LogPrintf(("%s " + fmt).c_str(), GetDisplayName(), parameters...)', | ||||
"WalletLogPrintf(std::string fmt, Params... parameters)"), | ), | ||||
("src/wallet/scriptpubkeyman.h", | ( | ||||
"LogPrintf((\"%s \" + fmt).c_str(), m_storage.GetDisplayName(), parameters...)"), | "src/wallet/scriptpubkeyman.h", | ||||
("src/wallet/scriptpubkeyman.h", | "WalletLogPrintf(std::string fmt, Params... parameters)", | ||||
"WalletLogPrintf(const std::string& fmt, const Params&... parameters)"), | ), | ||||
( | |||||
"src/wallet/scriptpubkeyman.h", | |||||
'LogPrintf(("%s " + fmt).c_str(), m_storage.GetDisplayName(), parameters...)', | |||||
), | |||||
( | |||||
"src/wallet/scriptpubkeyman.h", | |||||
"WalletLogPrintf(const std::string& fmt, const Params&... parameters)", | |||||
), | |||||
] | ] | ||||
FUNCTION_NAMES_AND_NUMBER_OF_LEADING_ARGUMENTS = [ | FUNCTION_NAMES_AND_NUMBER_OF_LEADING_ARGUMENTS = [ | ||||
("FatalError", 0), | ("FatalError", 0), | ||||
("fprintf", 1), | ("fprintf", 1), | ||||
("LogConnectFailure", 1), | ("LogConnectFailure", 1), | ||||
("LogPrint", 1), | ("LogPrint", 1), | ||||
("LogPrintf", 0), | ("LogPrintf", 0), | ||||
Show All 19 Lines | def parse_function_calls(function_name, source_code): | ||||
True | True | ||||
>>> parse_function_calls("foo", "foo(1);bar(1);foo(2);bar(2);")[1].startswith("foo(2);") | >>> parse_function_calls("foo", "foo(1);bar(1);foo(2);bar(2);")[1].startswith("foo(2);") | ||||
True | True | ||||
>>> len(parse_function_calls("foo", "foo();bar();// foo();bar();")) | >>> len(parse_function_calls("foo", "foo();bar();// foo();bar();")) | ||||
1 | 1 | ||||
>>> len(parse_function_calls("foo", "#define FOO foo();")) | >>> len(parse_function_calls("foo", "#define FOO foo();")) | ||||
0 | 0 | ||||
""" | """ | ||||
assert isinstance(function_name, str) and isinstance( | assert ( | ||||
source_code, str) and function_name | isinstance(function_name, str) | ||||
lines = [re.sub("// .*", " ", line).strip() | and isinstance(source_code, str) | ||||
and function_name | |||||
) | |||||
lines = [ | |||||
re.sub("// .*", " ", line).strip() | |||||
for line in source_code.split("\n") | for line in source_code.split("\n") | ||||
if not line.strip().startswith("#")] | if not line.strip().startswith("#") | ||||
] | |||||
return re.findall( | return re.findall( | ||||
r"[^a-zA-Z_](?=({}\(.*).*)".format(function_name), f" {' '.join(lines)}") | r"[^a-zA-Z_](?=({}\(.*).*)".format(function_name), f" {' '.join(lines)}" | ||||
) | |||||
def normalize(s): | def normalize(s): | ||||
"""Return a normalized version of string s with newlines, tabs and C style comments ("/* ... */") | """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. | replaced with spaces. Multiple spaces are replaced with a single space. | ||||
>>> normalize(" /* nothing */ foo\tfoo /* bar */ foo ") | >>> normalize(" /* nothing */ foo\tfoo /* bar */ foo ") | ||||
'foo foo foo' | 'foo foo foo' | ||||
""" | """ | ||||
assert isinstance(s, str) | assert isinstance(s, str) | ||||
s = s.replace("\n", " ") | s = s.replace("\n", " ") | ||||
s = s.replace("\t", " ") | s = s.replace("\t", " ") | ||||
s = re.sub(r"/\*.*?\*/", " ", s) | s = re.sub(r"/\*.*?\*/", " ", s) | ||||
s = re.sub(" {2,}", " ", s) | s = re.sub(" {2,}", " ", s) | ||||
return s.strip() | return s.strip() | ||||
ESCAPE_MAP = { | ESCAPE_MAP = { | ||||
r"\n": "[escaped-newline]", | r"\n": "[escaped-newline]", | ||||
r"\t": "[escaped-tab]", | r"\t": "[escaped-tab]", | ||||
r'\"': "[escaped-quote]", | r"\"": "[escaped-quote]", | ||||
} | } | ||||
def escape(s): | def escape(s): | ||||
"""Return the escaped version of string s with "\\\"", "\\n" and "\\t" escaped as | """Return the escaped version of string s with "\\\"", "\\n" and "\\t" escaped as | ||||
"[escaped-backslash]", "[escaped-newline]" and "[escaped-tab]". | "[escaped-backslash]", "[escaped-newline]" and "[escaped-tab]". | ||||
>>> unescape(escape("foo")) == "foo" | >>> unescape(escape("foo")) == "foo" | ||||
▲ Show 20 Lines • Show All 70 Lines • ▼ Show 20 Lines | def parse_function_call_and_arguments(function_name, function_call): | ||||
['strprintf(', '"%s (%d)",', ' foo <= 1,', ' err', ')'] | ['strprintf(', '"%s (%d)",', ' foo <= 1,', ' err', ')'] | ||||
>>> parse_function_call_and_arguments("strprintf", 'strprintf("%s (%d)", foo <= bar<1, 2>(1, 2), err);') | >>> parse_function_call_and_arguments("strprintf", 'strprintf("%s (%d)", foo <= bar<1, 2>(1, 2), err);') | ||||
['strprintf(', '"%s (%d)",', ' foo <= bar<1, 2>(1, 2),', ' err', ')'] | ['strprintf(', '"%s (%d)",', ' foo <= bar<1, 2>(1, 2),', ' err', ')'] | ||||
>>> parse_function_call_and_arguments("strprintf", 'strprintf("%s (%d)", foo>foo<1,2>(1,2)?bar:foobar,err)'); | >>> parse_function_call_and_arguments("strprintf", 'strprintf("%s (%d)", foo>foo<1,2>(1,2)?bar:foobar,err)'); | ||||
['strprintf(', '"%s (%d)",', ' foo>foo<1,2>(1,2)?bar:foobar,', 'err', ')'] | ['strprintf(', '"%s (%d)",', ' foo>foo<1,2>(1,2)?bar:foobar,', 'err', ')'] | ||||
>>> parse_function_call_and_arguments("strprintf", 'strprintf("%s (%d)", foo>foo<1,2>(1,2),err)'); | >>> parse_function_call_and_arguments("strprintf", 'strprintf("%s (%d)", foo>foo<1,2>(1,2),err)'); | ||||
['strprintf(', '"%s (%d)",', ' foo>foo<1,2>(1,2),', 'err', ')'] | ['strprintf(', '"%s (%d)",', ' foo>foo<1,2>(1,2),', 'err', ')'] | ||||
""" | """ | ||||
assert isinstance(function_name, str) and isinstance( | assert ( | ||||
function_call, str) and function_name | isinstance(function_name, str) | ||||
and isinstance(function_call, str) | |||||
and function_name | |||||
) | |||||
remaining = normalize(escape(function_call)) | remaining = normalize(escape(function_call)) | ||||
expected_function_call = f"{function_name}(" | expected_function_call = f"{function_name}(" | ||||
assert remaining.startswith(expected_function_call) | assert remaining.startswith(expected_function_call) | ||||
parts = [expected_function_call] | parts = [expected_function_call] | ||||
remaining = remaining[len(expected_function_call):] | remaining = remaining[len(expected_function_call) :] | ||||
open_parentheses = 1 | open_parentheses = 1 | ||||
open_template_arguments = 0 | open_template_arguments = 0 | ||||
in_string = False | in_string = False | ||||
parts.append("") | parts.append("") | ||||
for i, char in enumerate(remaining): | for i, char in enumerate(remaining): | ||||
parts.append(parts.pop() + char) | parts.append(parts.pop() + char) | ||||
if char == "\"": | if char == '"': | ||||
in_string = not in_string | in_string = not in_string | ||||
continue | continue | ||||
if in_string: | if in_string: | ||||
continue | continue | ||||
if char == "(": | if char == "(": | ||||
open_parentheses += 1 | open_parentheses += 1 | ||||
continue | continue | ||||
if char == ")": | if char == ")": | ||||
open_parentheses -= 1 | open_parentheses -= 1 | ||||
if open_parentheses > 1: | if open_parentheses > 1: | ||||
continue | continue | ||||
if open_parentheses == 0: | if open_parentheses == 0: | ||||
parts.append(parts.pop()[:-1]) | parts.append(parts.pop()[:-1]) | ||||
parts.append(char) | parts.append(char) | ||||
break | break | ||||
prev_char = remaining[i - 1] if i - 1 >= 0 else None | prev_char = remaining[i - 1] if i - 1 >= 0 else None | ||||
next_char = remaining[i + 1] if i + 1 <= len(remaining) - 1 else None | next_char = remaining[i + 1] if i + 1 <= len(remaining) - 1 else None | ||||
if (char == "<" and next_char not in [" ", "<", "="] | if ( | ||||
and prev_char not in [" ", "<"]): | char == "<" | ||||
and next_char not in [" ", "<", "="] | |||||
and prev_char not in [" ", "<"] | |||||
): | |||||
open_template_arguments += 1 | open_template_arguments += 1 | ||||
continue | continue | ||||
if (char == ">" and next_char not in [" ", ">", "="] and | if ( | ||||
prev_char not in [" ", ">"] and open_template_arguments > 0): | char == ">" | ||||
and next_char not in [" ", ">", "="] | |||||
and prev_char not in [" ", ">"] | |||||
and open_template_arguments > 0 | |||||
): | |||||
open_template_arguments -= 1 | open_template_arguments -= 1 | ||||
if open_template_arguments > 0: | if open_template_arguments > 0: | ||||
continue | continue | ||||
if char == ",": | if char == ",": | ||||
parts.append("") | parts.append("") | ||||
return parts | return parts | ||||
Show All 16 Lines | def parse_string_content(argument): | ||||
'' | '' | ||||
>>> parse_string_content('1 2 3') | >>> parse_string_content('1 2 3') | ||||
'' | '' | ||||
""" | """ | ||||
assert isinstance(argument, str) | assert isinstance(argument, str) | ||||
string_content = "" | string_content = "" | ||||
in_string = False | in_string = False | ||||
for char in normalize(escape(argument)): | for char in normalize(escape(argument)): | ||||
if char == "\"": | if char == '"': | ||||
in_string = not in_string | in_string = not in_string | ||||
elif in_string: | elif in_string: | ||||
string_content += char | string_content += char | ||||
return string_content | return string_content | ||||
def count_format_specifiers(format_string): | def count_format_specifiers(format_string): | ||||
"""Return the number of format specifiers in string format_string. | """Return the number of format specifiers in string format_string. | ||||
Show All 27 Lines | for i, char in enumerate(format_string): | ||||
elif char in "aAcdeEfFgGinopsuxX": | elif char in "aAcdeEfFgGinopsuxX": | ||||
in_specifier = False | in_specifier = False | ||||
elif in_specifier and char == "*": | elif in_specifier and char == "*": | ||||
n += 1 | n += 1 | ||||
return n | return n | ||||
def main(args_in): | def main(args_in): | ||||
""" Return a string output with information on string format errors | """Return a string output with information on string format errors | ||||
>>> main(["test/lint/lint-format-strings-tests.txt"]) | >>> 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 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 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 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") | 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") | ||||
test/lint/lint-format-strings-tests.txt: Expected 0 argument(s) after format string but found 1 argument(s): strprintf("%%%%u", scope_id) | test/lint/lint-format-strings-tests.txt: Expected 0 argument(s) after format string but found 1 argument(s): strprintf("%%%%u", scope_id) | ||||
test/lint/lint-format-strings-tests.txt: Expected 1 argument(s) after format string but found 0 argument(s): strprintf("%%%u") | test/lint/lint-format-strings-tests.txt: Expected 1 argument(s) after format string but found 0 argument(s): strprintf("%%%u") | ||||
>>> main(["test/lint/lint-format-strings-tests-skip-arguments.txt"]) | >>> 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 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 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 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: 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") | test/lint/lint-format-strings-tests-skip-arguments.txt: Could not parse function call string "snprintf(...)": snprintf(skip1, "%d") | ||||
""" | """ | ||||
parser = argparse.ArgumentParser(description="This program checks that the number of arguments passed " | parser = argparse.ArgumentParser( | ||||
description=( | |||||
"This program checks that the number of arguments passed " | |||||
"to a variadic format string function matches the number of format " | "to a variadic format string function matches the number of format " | ||||
"specifiers in the format string.") | "specifiers in the format string." | ||||
parser.add_argument("file", type=argparse.FileType( | ) | ||||
"r", encoding="utf-8"), nargs="*", help="C++ source code file (e.g. foo.cpp)") | ) | ||||
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) | args = parser.parse_args(args_in) | ||||
for f in args.file: | for f in args.file: | ||||
file_content = f.read() | file_content = f.read() | ||||
for (function_name, | for ( | ||||
skip_arguments) in FUNCTION_NAMES_AND_NUMBER_OF_LEADING_ARGUMENTS: | function_name, | ||||
for function_call_str in parse_function_calls( | skip_arguments, | ||||
function_name, file_content): | ) 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( | parts = parse_function_call_and_arguments( | ||||
function_name, function_call_str) | function_name, function_call_str | ||||
) | |||||
relevant_function_call_str = unescape("".join(parts))[:512] | relevant_function_call_str = unescape("".join(parts))[:512] | ||||
if (f.name, relevant_function_call_str) in FALSE_POSITIVES: | if (f.name, relevant_function_call_str) in FALSE_POSITIVES: | ||||
continue | continue | ||||
if len(parts) < 3 + skip_arguments: | if len(parts) < 3 + skip_arguments: | ||||
print("{}: Could not parse function call string \"{}(...)\": {}".format( | print( | ||||
f.name, function_name, relevant_function_call_str)) | '{}: Could not parse function call string "{}(...)": {}'.format( | ||||
f.name, function_name, relevant_function_call_str | |||||
) | |||||
) | |||||
continue | continue | ||||
argument_count = len(parts) - 3 - skip_arguments | argument_count = len(parts) - 3 - skip_arguments | ||||
format_str = parse_string_content(parts[1 + skip_arguments]) | format_str = parse_string_content(parts[1 + skip_arguments]) | ||||
format_specifier_count = count_format_specifiers(format_str) | format_specifier_count = count_format_specifiers(format_str) | ||||
if format_specifier_count != argument_count: | if format_specifier_count != argument_count: | ||||
print("{}: Expected {} argument(s) after format string but found {} argument(s): {}".format( | print( | ||||
f.name, format_specifier_count, argument_count, relevant_function_call_str)) | "{}: Expected {} argument(s) after format string but found {}" | ||||
" argument(s): {}".format( | |||||
f.name, | |||||
format_specifier_count, | |||||
argument_count, | |||||
relevant_function_call_str, | |||||
) | |||||
) | |||||
continue | continue | ||||
if __name__ == "__main__": | if __name__ == "__main__": | ||||
doctest.testmod() | doctest.testmod() | ||||
main(sys.argv[1:]) | main(sys.argv[1:]) |