/
githubmirror
/
fwupd
Обзор
Документация
Войти
/
githubmirror
/
fwupd
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
contrib/ci/check-source.py
1 754 строки
64 KB
Richard Hughes
Rename some source files for a future refactor
03 авг 2026, 12:49
03 авг 2026, 12:49
2268986
Код
Авторство
О чём код?
#!/usr/bin/env python3 # # Copyright 2023 Richard Hughes <richard@hughsie.com> # # SPDX-License-Identifier: LGPL-2.1-or-later # # pylint: disable=invalid-name,missing-module-docstring,missing-function-docstring # pylint: disable=too-many-lines,too-many-return-statements,missing-class-docstring # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-statements # pylint: disable=too-few-public-methods,too-many-branches,protected-access import glob import sys import os import argparse import multiprocessing from dataclasses import dataclass, field from typing import Dict, List, Optional, Tuple from ctokenizer import Tokenizer, Node, NodeHint, Token, TokenHint # convert a CamelCase name into snake_case def _camel_to_snake(name: str) -> str: name_snake: str = "" for char in name: if char.islower() or char.isnumeric(): name_snake += char continue if char == "_": name_snake += char continue if name_snake: # and not char.isnumeric(): name_snake += "_" name_snake += char.lower() return name_snake def _value_relaxed(data: str) -> str: if data in ["0x0", "0x00", "0x0000"]: return "0" if data in ["G_SOURCE_REMOVE"]: return "FALSE" if data in ["G_SOURCE_CONTINUE"]: return "TRUE" return data @dataclass class SourceFailure: fn: Optional[str] = None linecnt: Optional[int] = None message: Optional[str] = None nocheck: Optional[str] = None expected: bool = False @dataclass class Checker: verbose: bool = False failures: List[SourceFailure] = field(default_factory=list) _current_fn: Optional[str] = field(default=None, init=False) _current_nocheck: Optional[str] = field(default=None, init=False) _gtype_parents: Dict[str, str] = field(default_factory=dict) _klass_funcs: List[str] = field(default_factory=list, init=False) _expected_failure_prefixes: List[str] = field(default_factory=list, init=False) def add_expected_failure(self, message_prefix: str) -> None: self._expected_failure_prefixes.append(message_prefix) def _should_process_node(self, node: Node) -> bool: if self._current_nocheck: if node.tokens_pre.count_fuzzy([f"~/* {self._current_nocheck} */"]) > 0: return False if node.tokens.count_fuzzy([f"~/* {self._current_nocheck} */"]) > 0: return False return True def add_failure(self, message, linecnt: Optional[int] = None) -> None: # we were expecting this expected: bool = False for message_prefix in self._expected_failure_prefixes: if message.startswith(message_prefix): expected = True break # add self.failures.append( SourceFailure( expected=expected, fn=self._current_fn, linecnt=linecnt, message=message, nocheck=self._current_nocheck, ) ) def has_failure(self, message_prefix: str) -> bool: for failure in self.failures: if failure.message and failure.message.startswith(message_prefix): return True return False def _test_function_names_prefix_private(self, func_name: str, token: Token) -> None: if func_name.startswith("__"): return valid_prefixes = ["_fwupd_", "_fu_", "_g_", "_xb_"] for prefix in valid_prefixes: if func_name.startswith(prefix): return self.add_failure( f"invalid function name {func_name} should have {'|'.join(valid_prefixes)} prefix", linecnt=token.linecnt, ) def _test_function_names_prefix_valid(self, func_name: str, token: Token) -> None: # sanity check if not self._current_fn: return # ignore headers if self._current_fn.endswith(".h"): return # ignore all self tests FIXME: 335 failures if self._current_fn.endswith("-test.c"): return # namespacing is strange here if self._current_fn.endswith("fwupd-enums.c"): return # doh if func_name in ["main", "fu_plugin_init_vfuncs", "fu_plugin_get_data"]: return # this is stuff that should move to GLib if func_name.startswith("_g_"): return # remove some suffixes we do not care about prefix = os.path.basename(self._current_fn).split(".")[0].replace("-", "_") for suffix in [ "_common", "_darwin", "_freebsd", "_helper", "_impl", "_linux", "_sync", "_windows", ]: if prefix.endswith(suffix): prefix = prefix[: -len(suffix)] # allowed truncations valid_prefixes = [] valid_prefixes.append(prefix) for key, value in { "fu_crc": "fu_misr", # split out to fu-misr.[c|h]? "fu_darwin_efivars": "fu_efivars", "fu_dbus_daemon": "fu_daemon", "fu_dbxtool": "fu_util", "fu_freebsd_efivars": "fu_efivars", "fu_linux_efivars": "fu_efivars", "fu_logitech_hidpp_hidpp": "fu_logitech_hidpp", "fu_logitech_hidpp_hidpp_msg": "fu_logitech_hidpp", "fu_self_test": "fu_test", "fu_string": "fu_str,fu_utf", "fu_windows_efivars": "fu_efivars", }.items(): if prefix == key: valid_prefixes.extend(value.split(",")) for prefix in valid_prefixes: if func_name.startswith(prefix): return self.add_failure( f"invalid function name {func_name} should have {'|'.join(valid_prefixes)} prefix", linecnt=token.linecnt, ) def _discover_klass_functions(self, node: Node) -> None: """discover all the device_class-> functions""" if node.depth != 0: return idx = node.tokens_pre.find_fuzzy(["~fu_*_class_init@FUNCTION"]) if idx == -1: return idx = 0 while True: idx = node.tokens.find_fuzzy(["-", ">", "~*", "="], offset=idx + 1) if idx == -1: break self._klass_funcs.append(node.tokens[idx + 4].data) def _test_variable_case(self, node: Node) -> None: """disallow non-lowercase variables""" if node.hint == NodeHint.ENUM: return idx: int = 0 while True: idx = node.tokens.find_fuzzy(["="], offset=idx + 1) if idx == -1: return token = node.tokens[idx - 1] if token.data.find(".") != -1: continue if token.data.lower() != token.data: self.add_failure( f"mixed case variable {token.data}", linecnt=token.linecnt, ) def _test_variable_buffer_index(self, node: Node) -> None: """disallow buf[] and prefer *buf""" idx: int = 0 while True: idx = node.tokens_pre.find_fuzzy( ["guint8", "~*", "[", "]", ","], offset=idx + 1 ) if idx == -1: return token = node.tokens_pre[idx + 1] self.add_failure( f"do not use param guint8 {token.data}[], instead use 'guint8 *{token.data}'", linecnt=token.linecnt, ) def _test_struct_member_case(self, node: Node) -> None: """disallow non-lowercase struct members""" if node.hint not in [NodeHint.STRUCT, NodeHint.STRUCT_TYPEDEF]: return idx: int = 0 while True: idx = node.tokens.find_fuzzy([";"], offset=idx + 1) if idx == -1: return token = node.tokens[idx - 1] if token.data.lower() != token.data: self.add_failure( f"mixed case struct member {token.data}", linecnt=token.linecnt, ) def _test_param_self_native_device(self, node: Node) -> None: """use @self for native internal functions, not the basetype""" if self._current_fn and os.path.basename(self._current_fn) in [ "fu-device.c", "fu-device-locker.c", "fu-device-progress.c", ]: return if self._current_fn and os.path.basename(self._current_fn).endswith(".h"): return if node.depth != 0: return idx = node.tokens_pre.find_fuzzy(["@FUNCTION", "(", "FuDevice", "~*"]) if idx == -1: return token = node.tokens_pre[idx] if token.data.endswith("_cb"): return if token.data in self._klass_funcs: return token_param = node.tokens_pre[idx + 4] if token_param.data == "self": return if token.data.endswith("_new") and token_param.data == "proxy": return self.add_failure( f"native device functions should use self as the first parameter not {token_param.data}", linecnt=token.linecnt, ) def _test_param_self_native_firmware(self, node: Node) -> None: """use @self for native internal functions, not the basetype""" if self._current_fn and os.path.basename(self._current_fn) in [ "fu-firmware-progress.c", "fu-self-test.c", ]: return if self._current_fn and os.path.basename(self._current_fn).endswith(".h"): return if self._current_fn and os.path.basename(self._current_fn).endswith( "-common.c" ): return if node.depth != 0: return idx = node.tokens_pre.find_fuzzy( ["@FUNCTION", "(", "FuFirmware", "~*", "firmware"] ) if idx == -1: return token = node.tokens_pre[idx] if token.data in self._klass_funcs: return self.add_failure( "native firmware functions should use self as the first parameter not firmware", linecnt=token.linecnt, ) def _test_param_self_device(self, node: Node) -> None: """only use @self for device GTypes, not the basetype""" if self._current_fn and os.path.basename(self._current_fn) in [ "fu-device.c", "fu-device.h", "fu-device-poll-locker.h", "fu-device-private.h", ]: return if node.depth != 0: return idx = node.tokens_pre.find_fuzzy(["@FUNCTION", "(", "FuDevice", "*", "self"]) if idx == -1: return token = node.tokens_pre[idx + 4] self.add_failure( f"invalid parameter name {token.data} should be called 'device'", linecnt=token.linecnt, ) def _test_param_self_firmware(self, node: Node) -> None: """only use @self for firmware GTypes, not the basetype""" if self._current_fn and os.path.basename(self._current_fn) in [ "fu-firmware.c", "fu-firmware-private.h", ]: return if node.depth != 0: return idx = node.tokens_pre.find_fuzzy(["@FUNCTION", "(", "FuFirmware", "*", "self"]) if idx == -1: return token = node.tokens_pre[idx + 4] self.add_failure( f"invalid parameter name {token.data} should be called 'firmware'", linecnt=token.linecnt, ) def _test_function_names_ensure(self, node: Node) -> None: """setting internal state should be 'ensuring' the data, not setting it""" if node.depth != 0: return idx = node.tokens_pre.find_fuzzy( [ "gboolean", "~fu_*_set_*@FUNCTION", "(", "~*", "*", "self", ",", "GError", "*", "*", "error", ")", ] ) if idx != -1: token = node.tokens_pre[idx] self.add_failure( "function should be called ensure, not set", linecnt=token.linecnt, ) def _test_function_name_suffix(self, node: Node) -> None: """verb_noun, not noun_verb""" if node.depth != 0: return # ACTION_set idx = node.tokens_pre.find_fuzzy(["~fu_*_set@FUNCTION"]) if idx != -1: token = node.tokens_pre[idx] self.add_failure( "function should be called set_ACTION, not ACTION_set", linecnt=token.linecnt, ) # ACTION_get idx = node.tokens_pre.find_fuzzy(["~fu_*_get@FUNCTION"]) if idx != -1: token = node.tokens_pre[idx] self.add_failure( "function should be called get_ACTION, not ACTION_get", linecnt=token.linecnt, ) # THING_add idx = node.tokens_pre.find_fuzzy(["~fu_*_add@FUNCTION"]) if idx != -1: token = node.tokens_pre[idx] if ( token.data.find("_list_") == -1 and token.data.find("_array_") == -1 and token.data.find("_cache_") == -1 and token.data not in ["fu_size_checked_add"] ): self.add_failure( "function should be called add_THING, not THING_add", linecnt=token.linecnt, ) # THING_remove idx = node.tokens_pre.find_fuzzy(["~fu_*_remove@FUNCTION"]) if idx != -1: token = node.tokens_pre[idx] if token.data.find("_list_") == -1 and token.data.find("_cache_") == -1: self.add_failure( "function should be called remove_THING, not THING_remove", linecnt=token.linecnt, ) def _test_function_names_prefix(self, node: Node) -> None: if node.depth != 0: return idx: int = 0 func_name: str = "" while True: idx = node.tokens_pre.find_fuzzy(["@FUNCTION", "("], offset=idx + 1) if idx == -1: return # sanity check token = node.tokens_pre[idx] func_name = token.data if len(func_name) < 10: continue if func_name in ["__attribute__"]: continue if self.verbose: print("func_name", func_name) if func_name.upper() != func_name and func_name.lower() != func_name: self.add_failure( "mixed case function name", linecnt=token.linecnt, ) if func_name.startswith("_"): self._test_function_names_prefix_private(func_name, token) else: self._test_function_names_prefix_valid(func_name, token) def _test_missing_literal(self, node: Node) -> None: """test for missing literals""" idx = node.tokens.find_fuzzy( ["g_task_return_new_error", "(", "~*", ",", "~*", ",", "~*", ",", "~*", ")"] ) if idx != -1: token = node.tokens[idx] self.add_failure( "missing literal, use g_task_return_new_error_literal() instead", linecnt=token.linecnt, ) idx = node.tokens.find_fuzzy(["g_prefix_error", "(", "~*", ",", "~*", ")"]) if idx != -1: token = node.tokens[idx] self.add_failure( "missing literal, use g_prefix_error_literal() instead", linecnt=token.linecnt, ) idx = node.tokens.find_fuzzy( ["g_set_error", "(", "~*", ",", "~*", ",", "~*", ",", "~*", ")"] ) if idx != -1: if node.tokens[idx + 8].data.find("%m") == -1: self.add_failure( "missing literal, use g_set_error_literal() instead", linecnt=node.linecnt, ) def _test_missing_error_suffixes(self, node: Node) -> None: """test for missing : suffixes""" idx = node.tokens.find_fuzzy(["~g_prefix_error*", "(", "~*", ","]) if idx == -1: return token = node.tokens[idx + 4] if not token.data.endswith(': "'): self.add_failure("missing ': ' suffix", linecnt=token.linecnt) def _test_extra_error_suffixes(self, node: Node) -> None: """test for extra : suffixes""" idx = node.tokens.find_fuzzy(["~g_set_error*", "(", "~*[", ","]) if idx == -1: return token = node.tokens[idx + 8] if token.data.endswith(': "'): self.add_failure("extraneous ': ' suffix", linecnt=token.linecnt) def _test_enums(self, node: Node) -> None: if node.depth != 0: return # only consider the last token idx = node.tokens_pre.find_fuzzy(["@ENUM"], offset=len(node.tokens_pre) - 1) if idx == -1: return name = node.tokens_pre[idx].data if self.verbose: print("enum_name", name) # needs Fu prefix valid_prefixes = ["Fwupd", "Fu"] for prefix in valid_prefixes: if name.startswith(prefix): return self.add_failure( f"invalid enum name {name} should have {'|'.join(valid_prefixes)} prefix", linecnt=node.linecnt, ) def _test_struct(self, node: Node) -> None: if node.depth != 0: return # no limit on name if node.hint == NodeHint.STRUCT_CONST: return # only consider the last token name: Optional[str] = None if node.hint == NodeHint.STRUCT_TYPEDEF: name = node.tokens_pre[-1].data else: idx = node.tokens_pre.find_fuzzy( ["@STRUCT"], offset=len(node.tokens_pre) - 1 ) if idx != -1: name = node.tokens_pre[idx].data if not name: return if name.startswith("_"): name = name[1:] if self.verbose: print("struct_name", name) # only for Rust code if name.startswith("FuStruct"): self.add_failure( f"incorrect struct name {name} -- FuStruct is reserved for Rust code", linecnt=node.linecnt, ) # needs Fu prefix valid_prefixes = ["Fwupd", "Fu"] for prefix in valid_prefixes: if name.startswith(prefix): return self.add_failure( f"invalid struct name {name} should have {'|'.join(valid_prefixes)} prefix", linecnt=node.linecnt, ) def _test_magic_numbers_buffer(self, node: Node) -> None: # skip tests if self._current_fn: basename = os.path.basename(self._current_fn) if basename.endswith("-test.c"): return limit: int = 4 idx = node.tokens.find_fuzzy(["~*@INTEGER", ","] * limit) if idx == -1: return token = node.tokens[idx] self.add_failure( f"variable has too many magic values (limit of {limit})", linecnt=token.linecnt, ) def _test_static_vars(self, node: Node) -> None: if node.depth != 0: return idx = node.tokens_pre.find_fuzzy(["static", "~*", "~*", "="]) if idx == -1: return token = node.tokens_pre[idx + 2] if token.data in ["signals", "quarks"]: return self.add_failure( f"static variable {token.data} not allowed", linecnt=token.linecnt ) def _test_rustgen_bitshifts(self, node: Node) -> None: if node.hint == NodeHint.ENUM: return idx = node.tokens.find_fuzzy(["]", "<", "<", "16"]) if idx != -1 and node.tokens.find_fuzzy(["]", "<", "<", "8"]) != -1: token = node.tokens[idx + 3] self.add_failure( "endian unsafe construction; perhaps use fu_memread_uint32 or rustgen", linecnt=token.linecnt, ) def _test_rustgen_vars(self, node: Node) -> None: idx = node.tokens.find_fuzzy(["g_autoptr", "(", "~FuStruct*", ")", "~*", "="]) if idx == -1: return token = node.tokens[idx + 4] if not token.data.startswith("st"): self.add_failure( f"rustgen structure '{token.data}' has to have 'st' prefix", linecnt=token.linecnt, ) def _test_zero_init(self, node: Node) -> None: if node.hint in [ NodeHint.UNION, NodeHint.STRUCT, NodeHint.STRUCT_CONST, NodeHint.STRUCT_TYPEDEF, ]: return if node.tokens_pre.count_fuzzy(["struct"]) > 0: return idx = node.tokens.find_fuzzy(["~guint*", "~*", "[", "~*", "]", ";"]) if idx != -1: token = node.tokens[idx] self.add_failure( "buffer not zero init, use ` = {0}`", linecnt=token.linecnt ) def _test_debug_newlines(self, node: Node) -> None: for func in ["g_info", "g_debug", "g_message"]: idx: int = 0 while True: idx = node.tokens.find_fuzzy([func, "(", "~*\\n*"], offset=idx + 1) if idx == -1: break token = node.tokens[idx] self.add_failure( f"{func} should not contain newlines", linecnt=token.linecnt ) def _test_debug_fullstops(self, node: Node) -> None: for func in ["g_error", "g_info", "g_debug", "g_message"]: idx: int = 0 while True: idx = node.tokens.find_fuzzy([func, "(", '~*."'], offset=idx + 1) if idx == -1: break token = node.tokens[idx] self.add_failure( f"{func} should not end with a full stop", linecnt=token.linecnt ) def _test_debug_sentence_case(self, node: Node) -> None: for func in ["g_error", "g_info", "g_debug", "g_message"]: idx: int = 0 while True: idx = node.tokens.find_fuzzy([func], offset=idx + 1) if idx == -1: break token = node.tokens[idx + 2] first_word = token.data[1:].split(" ")[0] if first_word and first_word[0].isupper() and first_word[1:].islower(): self.add_failure( f"{func} should not use sentence case", linecnt=token.linecnt ) def _test_comment_cpp(self, node: Node) -> None: """/* C comments please */""" idx = node.tokens.find_fuzzy(["//"]) if idx != -1: token = node.tokens[idx] self.add_failure( "use C style comments, not C++, e.g. /* this */", linecnt=token.linecnt, ) def _test_comment_lines(self, node: Node) -> None: """/**** not boxes *****/""" for token in node.tokens_pre + node.tokens: if token.data.find("/***") != -1: self.add_failure( "do not use boxed comment lines, just use /* comment */", linecnt=token.linecnt, ) if token.data.find("/**@brief") != -1: self.add_failure( "do not use doxygen comment lines, just use /* comment */", linecnt=token.linecnt, ) def _test_comment_lower_case(self, node: Node) -> None: """single line comments are supposed to be lowercase""" idx: int = 0 while True: idx = node.tokens.find_fuzzy(["@COMMENT"], offset=idx + 1) if idx == -1: break token = node.tokens[idx] first_word = token.data[2:-2].strip().split(" ")[0] if ( first_word not in ["Windows", "Microsoft", "Thunderbolt", "Dell"] and token.linecnt == token.linecnt_end and first_word and first_word[0].isupper() and first_word[1:].islower() ): self.add_failure( "single line comments should not use sentence case", linecnt=token.linecnt, ) def _test_equals_true(self, node: Node) -> None: """do not allow `if (foo == TRUE)`""" idx = node.tokens_pre.find_fuzzy(["==", "TRUE", ")"]) if idx != -1: token = node.tokens_pre[idx] self.add_failure( "do not compare a boolean to TRUE", linecnt=token.linecnt, ) def _test_blocked_goto(self, node: Node) -> None: idx = node.tokens.find_fuzzy(["goto"]) if idx != -1: token = node.tokens[idx] self.add_failure( "do not use goto, refactor into a new block", linecnt=token.linecnt, ) def _test_device_display(self, node: Node) -> None: """use fu_device_get_id_display rather than the two different commands""" idx1 = node.tokens.find_fuzzy(["fu_device_get_name", "(", "~*", ")"]) idx2 = node.tokens.find_fuzzy(["fu_device_get_id", "(", "~*", ")"]) if idx1 != -1 and idx2 != -1: # check this isn't an assert if node.tokens[idx1 - 2].data == "g_assert_cmpstr": return # same FuDevice within a limited number of lines? token1 = node.tokens[idx1 + 2] token2 = node.tokens[idx2 + 2] limit: int = 3 if token1.data == token2.data: if abs(token1.linecnt - token2.linecnt) < limit: self.add_failure( "use fu_device_get_id_display() rather than " "fu_device_get_name()+fu_device_get_id()", linecnt=token1.linecnt, ) def _test_debug_fns(self, node: Node) -> None: # no console output expected if self._current_fn and os.path.basename(self._current_fn) in [ "fu-console.c", "fu-daemon.c", "fu-dbxtool.c", "fu-debug.c", "fu-fuzzer-main.c", "fu-gcab.c", "fu-main.c", "fu-main-windows.c", "fu-self-test.c", "fu-tpm-eventlog.c", "fwupd-self-test.c", ]: return for token, msg in { "~g_print*": "Use g_debug() instead", }.items(): idx = node.tokens.find_fuzzy([token]) if idx != -1: token = node.tokens[idx] self.add_failure( f"contains blocked token {token.data}: {msg}", linecnt=token.linecnt ) def _test_gobject_finalize(self, node: Node) -> None: if node.tokens_pre.endswith_fuzzy( ["void", "~*_finalize", "(", "GObject", "*", "~*", ")"] ): token = node.tokens_pre[-1] idx = node.tokens.find_fuzzy( ["G_OBJECT_CLASS", "(", "~*_parent_class", ")", "-", ">", "finalize"] ) if idx == -1: self.add_failure( "did not have parent ->finalize()", linecnt=token.linecnt ) def _test_gobject_constructed(self, node: Node) -> None: if node.tokens_pre.endswith_fuzzy( ["void", "~*_constructed", "(", "GObject", "*", "~*", ")"] ): token = node.tokens_pre[-1] idx = node.tokens.find_fuzzy( ["G_OBJECT_CLASS", "(", "~*_parent_class", ")", "-", ">", "constructed"] ) if idx == -1: self.add_failure( "did not have parent ->constructed()", linecnt=token.linecnt ) def _test_blocked_funcs(self, node: Node) -> None: for token, msg in { "g_error": "Use GError instead", "g_ascii_strtoull": "Use fu_strtoull() instead", "g_ascii_strtoll": "Use fu_strtoll() instead", "g_find_program_in_path": "Use fu_path_store_find_program() instead", "g_strerror": "Use fwupd_strerror() instead", "gnutls_malloc": "Use gnutls_calloc() instead", "g_base64_encode": "Use fu_base64_encode() instead", "g_spawn_command_line_sync": "Do not call out to external tools", "g_spawn_sync": "Do not call out to external tools", "g_variant_get_uint64": "Use fwupd_variant_get_uint64() instead", "g_variant_get_uint32": "Use fwupd_variant_get_uint32() instead", "g_variant_get_string": "Use fwupd_variant_get_string() instead", "g_variant_get_boolean": "Use fwupd_variant_get_boolean() instead", "g_variant_get_strv": "Use fwupd_variant_get_strv() instead", "g_variant_get_double": "Use fwupd_variant_get_double() instead", "g_variant_get_int32": "Use fwupd_variant_get_int32() instead", "g_ptr_array_copy": "Use fu_ptr_array_copy() instead", "g_random_int_range": "Use fu_common_get_random() or a predictable token instead", "g_assert": "Use g_set_error() or g_return_val_if_fail() instead", "HIDIOCSFEATURE": "Use fu_hidraw_device_set_feature() instead", "HIDIOCGFEATURE": "Use fu_hidraw_device_get_feature() instead", "memcpy": "Use fu_memcpy_safe or rustgen instead", "g_unsetenv": "This is not thread safe", "~GUINT??_FROM_?E": "Use fu_memread_uintXX_safe() or rustgen instead", "~GUINT??_TO_?E": "Use fu_memwrite_uintXX_safe() or rustgen instead", "ioctl": "Use fu_udev_device_ioctl() instead", }.items(): idx = node.tokens.find_fuzzy([token, "("]) if idx != -1: token = node.tokens[idx] self.add_failure( f"contains blocked token {token.data}: {msg}", linecnt=token.linecnt ) # only enforce FuInputStream/FuMemoryInputStream outside of libfwupd if not self._current_fn or not self._current_fn.startswith("libfwupd/"): for token, msg in { "~g_memory_input_stream_new*": "Use fu_memory_input_stream_new*() instead", "g_file_read": "Use fu_file_input_stream_from_file() instead", "~g_file_input_stream_*": "Use fu_file_input_stream_*() instead", "g_zlib_compressor_new": "Use fu_compressor_stream_new_compress() instead", "g_zlib_decompressor_new": "Use fu_compressor_stream_new_decompress() instead", "g_input_stream_read_all": "Use fu_input_stream_read_all() instead", "g_input_stream_read_bytes": "Use fu_input_stream_read_bytes() instead", "g_input_stream_read": "Use fu_input_stream_read() instead", }.items(): idx = node.tokens.find_fuzzy([token, "("]) if idx != -1: token = node.tokens[idx] self.add_failure( f"contains blocked token {token.data}: {msg}", linecnt=token.linecnt, ) idx = node.tokens.find_fuzzy(["|=", "~1*", "<", "<"]) if idx != -1: token = node.tokens[idx] self.add_failure("Use FU_BIT_SET() instead", linecnt=token.linecnt) idx = node.tokens.find_fuzzy(["|=", "(", "~1*", "<", "<"]) if idx != -1: token = node.tokens[idx] self.add_failure("Use FU_BIT_SET() instead", linecnt=token.linecnt) idx = node.tokens.find_fuzzy(["&", "=", "~", "(", "~1*", "<", "<"]) if idx != -1: token = node.tokens[idx] self.add_failure("Use FU_BIT_CLEAR() instead", linecnt=token.linecnt) idx = node.tokens_pre.find_fuzzy( ["__attribute__", "(", "(", "packed", ")", ")"] ) if idx != -1: token = node.tokens_pre[idx] self.add_failure("use rustgen instead", linecnt=token.linecnt) def _test_blocked_tokens(self, node: Node) -> None: for search, msg in { "__FUNCTION__": "Use G_STRFUNC instead", "__VA_ARGS__": "Use native functions instead", "uint32_t": "Use guint32 instead", "uint16_t": "Use guint16 instead", "uint8_t": "Use guint8 instead", }.items(): for token in node.tokens: if token.data.find(search) != -1: self.add_failure( f"contains blocked token {token.data}: {msg}", linecnt=token.linecnt, ) # only enforce FuInputStream/FuMemoryInputStream outside of libfwupd if not self._current_fn or not self._current_fn.startswith("libfwupd/"): for search, msg in { "GInputStream": "Use FuInputStream instead", "GMemoryInputStream": "Use FuMemoryInputStream instead", "GFileInputStream": "Use FuFileInputStream instead", }.items(): for token in node.tokens: if token.data == search: self.add_failure( f"contains blocked token {token.data}: {msg}", linecnt=token.linecnt, ) def _test_magic_numbers_defined(self, nodes: List[Node]) -> None: cnt: int = 0 limit: int = 15 linecnt: int = 0 for node in nodes: if node.depth != 0: continue # overridden per-file idx = node.tokens_pre.find_fuzzy([f"~*{self._current_nocheck}=*"]) if idx != -1: limit = int(node.tokens_pre[idx].data.split("=")[1].split(" ")[0]) cnt_tmp = node.tokens_pre.count_fuzzy(["#define", "~*", "~0x*"]) if cnt_tmp: linecnt = node.linecnt cnt += cnt_tmp if cnt > limit: self.add_failure( f"file has too many #defined magic values ({cnt}), limit of {limit}", linecnt=linecnt, ) def _test_magic_numbers_inline(self, nodes: List[Node]) -> None: cnt: int = 0 limit: int = 80 linecnt: int = 0 for node in nodes: # overridden per-file idx = node.tokens_pre.find_fuzzy([f"~*{self._current_nocheck}=*"]) if idx != -1: for word in node.tokens_pre[idx].data.split(" "): if word.startswith("nocheck:magic-inlines="): limit = int(word[22:]) break for token in node.tokens: if token.data in ["0x0", "0x00", "0x0000"]: continue if len(token.data) >= 3 and token.data.startswith("0x"): linecnt = node.linecnt cnt += 1 if cnt > limit: self.add_failure( f"file has too many inline magic values ({cnt}), limit of {limit}", linecnt=linecnt, ) def _test_gerror_false_returns(self, nodes: List[Node]) -> None: for node in nodes: if node.depth == 0: continue if not self._should_process_node(node): return idx = node.tokens.find_fuzzy(["~g_set_error*", "("]) if idx != -1: token = node.tokens[idx] idx = node.tokens.find_fuzzy(["return", "~*", ";"], offset=idx) if idx == -1: idx = node.tokens.find_fuzzy(["break", ";"], offset=idx) if idx == -1: self.add_failure( "uses g_set_error() without returning a value", linecnt=token.linecnt, ) break def _test_gerror_not_set(self, nodes: List[Node]) -> None: limit: int = 10 for node in nodes: if node.depth == 0: continue if not self._should_process_node(node): continue idx = node.tokens.find_fuzzy(["~g_prefix_error*", "("]) if idx != -1: linecnt = node.tokens[idx].linecnt if self.verbose: print(f"GError required @{linecnt}") found_linecnt: List[int] = [] # set error inner idx_found = node.tokens.find_fuzzy( ["~error*", ")"], reverse=True, offset=idx ) if idx_found != -1: found_linecnt.append(node.tokens[idx_found].linecnt_end) idx_found = node.tokens.find_fuzzy( ["~g_set_error*", "(", "~error*"], reverse=True, offset=idx ) if idx_found != -1: found_linecnt.append(node.tokens[idx_found].linecnt_end) # set error prior idx_found = node.tokens_pre.find_fuzzy(["~error*", ")"], reverse=True) if idx_found != -1: found_linecnt.append(node.tokens_pre[idx_found].linecnt_end) idx_found = node.tokens_pre.find_fuzzy( ["~g_set_error*", "(", "~error*"], reverse=True ) if idx_found != -1: found_linecnt.append(node.tokens_pre[idx_found].linecnt_end) idx_found = node.tokens_pre.find_fuzzy(["~error*", ","], reverse=True) if idx_found != -1: found_linecnt.append(node.tokens_pre[idx_found].linecnt_end) # find the closest error set linecnt_closest: int = 0 if found_linecnt: linecnt_closest = max(found_linecnt) if self.verbose: print(f"GError set @{linecnt_closest}") linecnt_delta = linecnt - linecnt_closest if linecnt_delta > limit: self.add_failure( "uses g_prefix_error() without setting GError for " f"{linecnt_delta}/{limit} previous lines", linecnt=linecnt, ) break def _test_gerror_domain(self, node: Node) -> None: """must use FUWPD_ERROR domains""" idx = node.tokens.find_fuzzy( ["~g_set_error*", "(", "~*", ",", "~G_*", ",", "~G_*"] ) if idx == -1: return token = node.tokens[idx] self.add_failure( "uses g_set_error() without using FWUPD_ERROR", linecnt=token.linecnt ) def _test_gerror_deref(self, node: Node) -> None: """using (*error)->message""" idx = node.tokens.find_fuzzy(["(", "*", "~*error*", ")", "-", ">"]) if idx == -1: return token = node.tokens[idx] self.add_failure( "dereferences GError; use error_local instead", linecnt=token.linecnt ) def _test_switch(self, nodes: List[Node]) -> None: limit: int = 2 cnt: int = 0 for node in nodes: # restrict only per-top-level=function, not per-file if node.depth == 0: cnt = 0 idx = node.tokens_pre.find_fuzzy(["switch", "(", "~*", ")"]) if idx != -1: cnt += 1 if cnt > limit: self.add_failure( f"has too many switches ({cnt}), limit of {limit}", linecnt=node.linecnt, ) break def _test_null_false_returns(self, nodes: List[Node]) -> None: # allowed values from g_return_val_if_fail() types_rvif = { "*": ["NULL"], "GQuark": ["0"], "GType": ["G_TYPE_INVALID"], "gpointer": ["NULL"], "gboolean": ["TRUE", "FALSE"], "guint32": ["0", "G_MAXUINT32"], "guint64": ["0", "G_MAXUINT64"], "guint16": ["0", "G_MAXUINT16"], "guint8": ["0", "G_MAXUINT8"], "gint64": ["0", "-1", "G_MAXINT64"], "gint32": ["0", "-1", "G_MAXINT32"], "gint16": ["0", "-1", "G_MAXINT16"], "gint8": ["0", "-1", "G_MAXINT8"], "gint": ["0", "-1", "G_MAXINT"], "guint": ["0", "G_MAXUINT"], "gulong": ["0", "G_MAXLONG"], "gsize": ["0", "G_MAXSIZE"], "gssize": ["0", "-1", "G_MAXSSIZE"], } # disallowed values from return types_nret = { "*": ["FALSE"], "GQuark": ["NULL", "TRUE", "FALSE"], "GType": ["NULL", "TRUE", "FALSE"], "gpointer": ["FALSE"], "gboolean": ["NULL", "0"], "guint32": ["NULL", "TRUE", "FALSE"], "guint64": ["NULL", "TRUE", "FALSE"], "guint16": ["NULL", "TRUE", "FALSE"], "guint8": ["NULL", "TRUE", "FALSE"], "gint64": ["NULL", "TRUE", "FALSE"], "gint32": ["NULL", "TRUE", "FALSE"], "gint16": ["NULL", "TRUE", "FALSE"], "gint8": ["NULL", "TRUE", "FALSE"], "gint": ["NULL", "TRUE", "FALSE"], "guint": ["NULL", "TRUE", "FALSE"], "gulong": ["NULL", "TRUE", "FALSE"], "gsize": ["NULL", "TRUE", "FALSE"], "gssize": ["NULL", "TRUE", "FALSE"], } current_type: str = "" for node in nodes: if not self._should_process_node(node): continue # new function if node.depth == 0: idx = node.tokens_pre.find_fuzzy(["~*", "("]) if idx == -1: continue token_type = node.tokens_pre[idx - 1] token_func = node.tokens_pre[idx] if self.verbose: print("TYPE", token_func.data, token_type.data) current_type = token_type.data # look for g_return_val_if_fail idx = node.tokens.find_fuzzy(["g_return_val_if_fail", "("]) if idx != -1: # advance to the return idx = node.tokens.find_fuzzy([",", "~*", ")", ";"], offset=idx) if idx != -1: token = node.tokens[idx + 1] try: rvif = types_rvif[current_type] except KeyError: if self.verbose: print(f"missing type {current_type}") continue if _value_relaxed(token.data) not in rvif: self.add_failure( "g_return_val_if_fail() return type invalid, " f"expected {'|'.join(rvif)} for {current_type}, not {token.data}", linecnt=token.linecnt, ) # look for return values idx = node.tokens.find_fuzzy(["return", "~*", ";"]) if idx == -1: continue token = node.tokens[idx + 1] if token.hint == TokenHint.FUNCTION: continue try: nret = types_nret[current_type] except KeyError: continue if _value_relaxed(token.data) in nret: self.add_failure( f"return type invalid for {current_type}: {token.data}", linecnt=token.linecnt, ) def _test_gerror_void_return(self, node: Node) -> None: """takes GError but returns void""" if node.depth != 0: return idx_start = node.tokens_pre.find_fuzzy(["static", "void", "@FUNCTION"]) if idx_start == -1: return idx = node.tokens_pre.find_fuzzy( ["GError", "*", "*", "error", ")"], offset=idx_start ) if idx == -1: return if idx - idx_start < 10: token = node.tokens_pre[idx] self.add_failure( "void return type not expected for GError", linecnt=token.linecnt, ) def _test_function_length(self, node: Node) -> None: if node.depth != 0: return limit: int = 400 if node.linecnt_end - node.linecnt > limit: self.add_failure( f"function is too long, was {node.linecnt_end - node.linecnt} of {limit}", linecnt=node.linecnt, ) def _test_firmware_convert_version(self, nodes: List[Node]) -> None: # contains fu_firmware_set_version_raw() _set_version_raw: bool = False for node in nodes: idx = node.tokens.find_fuzzy( ["fu_firmware_set_version_raw", "(", "~*", ",", "~*", ")", ";"] ) if idx != -1: _set_version_raw = True if not _set_version_raw: return # also contains fu_firmware_set_version() for node in nodes: if not self._should_process_node(node): continue idx = node.tokens.find_fuzzy( ["fu_firmware_set_version", "(", "~*", ",", "~*", ")", ";"] ) if idx != -1: token = node.tokens[idx] self.add_failure( "Use FuFirmwareClass->convert_version rather than fu_firmware_set_version()", linecnt=token.linecnt, ) def _test_device_convert_version(self, nodes: List[Node]) -> None: if self._current_fn and os.path.basename(self._current_fn) in [ "fu-engine-test.c", ]: return # contains fu_device_set_version_raw() _set_version_raw: bool = False for node in nodes: idx = node.tokens.find_fuzzy( ["fu_device_set_version_raw", "(", "~*", ",", "~*", ")", ";"] ) if idx != -1: _set_version_raw = True if not _set_version_raw: return # also contains fu_device_set_version() for node in nodes: if not self._should_process_node(node): continue idx = node.tokens.find_fuzzy( ["fu_device_set_version", "(", "~*", ",", "~*", ")", ";"] ) if idx != -1: token = node.tokens[idx] self.add_failure( "Use FuDeviceClass->convert_version rather than fu_device_set_version()", linecnt=token.linecnt, ) def _test_small_conditionals_with_braces_node(self, node: Node) -> None: # has previous conditional idx = node.tokens_pre.find_fuzzy(["else", "if", "("]) if idx != -1: return # has conditional idx = node.tokens_pre.find_fuzzy(["if", "("]) if idx == -1: return # multiline conditional can have braces if node.linecnt_end - node.tokens_pre[idx].linecnt >= 3: return # inline struct idx = node.tokens_pre.find_fuzzy(["@STRUCT"]) if idx != -1: return # nested idx = node.tokens.find_fuzzy(["if", "("]) if idx != -1: return # not one line block if len(node.tokens) < 2: return if node.tokens[1].linecnt != node.tokens[-1].linecnt: return limit: int = 70 if len("".join([token.data for token in node.tokens])) < limit: token = node.tokens[1] self.add_failure( "no {} required for small single-line conditional", linecnt=token.linecnt, ) def _test_small_conditionals_with_braces(self, nodes: List[Node]) -> None: # we need to parse the nodes in order for idx, node in enumerate(nodes): next_node_depth: int = 0 try: next_node_depth = nodes[idx + 1].depth except IndexError: pass # followed by else try: idx = nodes[idx + 1].tokens_pre.find_fuzzy(["else"]) if idx != -1: continue except IndexError: pass # ignore if is nested deeper if next_node_depth > node.depth: continue # check node self._test_small_conditionals_with_braces_node(node) def _test_nesting_depth(self, node: Node) -> None: limit: int = 5 if node.depth >= limit: self.add_failure( f"is nested too deep {node.depth}/{limit}", linecnt=node.linecnt ) def _test_strsafe_offset(self, node: Node) -> None: idx = node.tokens.find_fuzzy( [ "fu_strsafe@FUNCTION", "(", "(", "const", "~*char", "*", ")", "~*", "+", ] ) if idx == -1: idx = node.tokens.find_fuzzy( [ "fu_strsafe@FUNCTION", "(", "(", "gchar", "*", ")", "~*", "+", ] ) if idx == -1: idx = node.tokens.find_fuzzy( [ "fu_strsafe@FUNCTION", "(", "(", "const", "gchar", "*", ")", "~*", "-", ">", "data", "+", ] ) if idx != -1: token = node.tokens[idx] self.add_failure( "use fu_memstrsafe() rather than fu_strsafe() if reading with an offset", linecnt=token.linecnt, ) def _test_memread(self, node: Node) -> None: limit: int = 7 cnt = node.tokens.count_fuzzy(["~fu_memread_uint*"]) if cnt >= limit: self.add_failure( f"too many calls to fu_memread_uintXX() ({cnt}/{limit}), use rustgen", linecnt=node.linecnt, ) cnt = node.tokens.count_fuzzy(["~fu_memwrite_uint*"]) if cnt >= limit: self.add_failure( f"too many calls to fu_memwrite_uintXX() ({cnt}/{limit}), use rustgen", linecnt=node.linecnt, ) def _test_gobject_parents(self, nodes: List[Node]) -> None: gtype: str = "" gtypeparent: str = "" for node in nodes: if node.depth != 0: continue if not self._should_process_node(node): return # .h idx = node.tokens_pre.find_fuzzy(["~G_DECLARE_*_TYPE", "("]) if idx != -1: gtype = node.tokens_pre[idx + 2].data gtypeparent = node.tokens_pre[idx + 10].data self._gtype_parents[gtype] = gtypeparent # check the class def is correct if gtype: idx = node.tokens.find_fuzzy(["~*Class", "parent_class"]) if idx != -1: gtypeparentclass_found = node.tokens[idx].data gtypeparentclass_expected = f"{gtypeparent}Class" if gtypeparentclass_found != gtypeparentclass_expected: self.add_failure( f"wrong parent_class for {gtype}, " f"got {gtypeparentclass_found} and " f"expected {gtypeparentclass_expected}", linecnt=node.linecnt, ) # .c idx = node.tokens_pre.find_fuzzy(["~G_DEFINE_TYPE*", "("]) if idx != -1: gtype = node.tokens_pre[idx + 2].data if self.verbose: print("GTYPE", gtype, self._gtype_parents) if gtype not in self._gtype_parents: continue gtypeparent_found: str = node.tokens_pre[idx + 6].data gtype_snake = _camel_to_snake(self._gtype_parents[gtype]).split( "_", maxsplit=1 ) gtypeparent_expected = f"{gtype_snake[0]}_type_{gtype_snake[1]}".upper() if gtypeparent_found != gtypeparent_expected: self.add_failure( f"wrong parent GType for {gtype}, " f"got {gtypeparent_found} and " f"expected {gtypeparent_expected}", linecnt=node.linecnt, ) def _test_nodes(self, nodes: List[Node]) -> None: # preroll self._klass_funcs.clear() for node in nodes: self._discover_klass_functions(node) if self.verbose: print("KLASS FUNCS", self._klass_funcs) # tests we can do node by node for node in nodes: # test for missing ->finalize() self._current_nocheck = "nocheck:finalize" if self._should_process_node(node): self._test_gobject_finalize(node) self._test_gobject_constructed(node) # test for blocked functions self._current_nocheck = "nocheck:blocked" if self._should_process_node(node): self._test_blocked_funcs(node) self._test_blocked_tokens(node) self._test_blocked_goto(node) self._test_device_display(node) self._test_equals_true(node) # test for debug lines self._current_nocheck = "nocheck:print" if self._should_process_node(node): self._test_debug_fns(node) self._test_debug_newlines(node) self._test_debug_fullstops(node) self._test_debug_sentence_case(node) self._test_comment_lower_case(node) self._test_comment_cpp(node) self._test_comment_lines(node) # not nesting too deep self._current_nocheck = "nocheck:depth" if self._should_process_node(node): self._test_nesting_depth(node) # test for function names self._current_nocheck = "nocheck:name" if self._should_process_node(node): self._test_function_names_prefix(node) self._test_function_names_ensure(node) self._test_function_name_suffix(node) self._test_param_self_device(node) self._test_param_self_firmware(node) self._test_param_self_native_device(node) self._test_param_self_native_firmware(node) self._test_variable_case(node) self._test_variable_buffer_index(node) self._test_struct_member_case(node) # test for invalid struct and enum names self._current_nocheck = "nocheck:prefix" if self._should_process_node(node): self._test_struct(node) self._test_enums(node) # test for static variables self._current_nocheck = "nocheck:static" if self._should_process_node(node): self._test_static_vars(node) # test for magic numbers self._current_nocheck = "nocheck:magic" if self._should_process_node(node): self._test_magic_numbers_buffer(node) # test for rustgen variables self._current_nocheck = "nocheck:rustgen" if self._should_process_node(node): self._test_rustgen_vars(node) # test for endian safety self._current_nocheck = "nocheck:endian" if self._should_process_node(node): self._test_rustgen_bitshifts(node) # GError self._current_nocheck = "nocheck:error" if self._should_process_node(node): self._test_gerror_domain(node) self._test_gerror_void_return(node) self._test_extra_error_suffixes(node) self._test_missing_error_suffixes(node) self._test_missing_literal(node) self._test_gerror_deref(node) # using too many memory reads/writes self._current_nocheck = "nocheck:memread" if self._should_process_node(node): self._test_memread(node) self._test_strsafe_offset(node) # test for non-zero'd init self._current_nocheck = "nocheck:zero-init" if self._should_process_node(node): self._test_zero_init(node) # functions too long self._current_nocheck = "nocheck:lines" if self._should_process_node(node): self._test_function_length(node) # NULL != FALSE self._current_nocheck = "nocheck:lines" self._test_null_false_returns(nodes) self._test_small_conditionals_with_braces(nodes) # using too many switch() self._current_nocheck = None self._test_switch(nodes) # should use FuFirmwareClass->convert_version or FuDeviceClass->convert_version self._current_nocheck = "nocheck:set-version" self._test_firmware_convert_version(nodes) self._test_device_convert_version(nodes) # using too many hardcoded constants self._current_nocheck = "nocheck:magic-defines" self._test_magic_numbers_defined(nodes) self._current_nocheck = "nocheck:magic-inlines" self._test_magic_numbers_inline(nodes) # setting GError, not returning self._current_nocheck = "nocheck:error-false-return" self._test_gerror_false_returns(nodes) # prefix with no set self._current_nocheck = "nocheck:error" self._test_gerror_not_set(nodes) # test GObject parent types self._current_nocheck = "nocheck:name" self._test_gobject_parents(nodes) def test_file(self, fn: str) -> None: self._current_fn = fn self._current_nocheck = None with open(fn, "rb") as f: try: data = f.read().decode() except UnicodeDecodeError as e: print(f"failed to read {fn}: {e}") if data.find("Copyright") == -1: self.add_failure("does not have copyright assigned") if data.find("SPDX-License-Identifier") == -1: self.add_failure("does not have a SPDX-License-Identifier") tokenizer = Tokenizer(data) nodes = tokenizer.nodes if self.verbose: print(nodes) self._test_nodes(nodes) def _build_gtype_parents(fns: List[str]) -> Dict[str, str]: """Pre-scan header files to build the GType parent map.""" gtype_parents: Dict[str, str] = {} for fn in fns: if not fn.endswith(".h"): continue with open(fn, "rb") as f: try: data = f.read().decode() except UnicodeDecodeError as e: print(f"failed to read {fn}: {e}", file=sys.stderr) continue tokenizer = Tokenizer(data) for node in tokenizer.nodes: if node.depth != 0: continue idx = node.tokens_pre.find_fuzzy(["~G_DECLARE_*_TYPE", "("]) if idx != -1: gtype = node.tokens_pre[idx + 2].data gtypeparent = node.tokens_pre[idx + 10].data gtype_parents[gtype] = gtypeparent return gtype_parents def _check_file( args: Tuple[str, Dict[str, str], bool], ) -> List[SourceFailure]: """Check a single file -- top-level function for multiprocessing.""" fn, gtype_parents, verbose = args print(f"checking {fn}…", file=sys.stderr) checker = Checker(verbose=verbose, _gtype_parents=gtype_parents) checker.test_file(fn) return checker.failures def test_files(fns_optional: List[str], verbose: bool = False) -> int: # test all C and H files # use any file specified in argv, falling back to scanning the entire tree fns: List[str] = [] if fns_optional: for fn in fns_optional: if fn.startswith("contrib/ci/tests"): continue try: ext: str = fn.rsplit(".", maxsplit=1)[1] except IndexError: continue if ext in ["c", "h"]: fns.append(fn) else: fns.extend(glob.glob("libfwupd/*.[c|h]")) fns.extend(glob.glob("libfwupdplugin/*.[c|h]")) fns.extend(glob.glob("plugins/*/*.[c|h]")) fns.extend(glob.glob("src/*.[c|h]")) fns = sorted(fns, reverse=True) fns = [fn for fn in fns if os.path.basename(fn) != "check-source.py"] # pre-scan .h files to build the GType parent map gtype_parents = _build_gtype_parents(fns) # check files in parallel (fall back to serial for verbose or small batches) failures: List[SourceFailure] = [] if verbose or len(fns) < 4: checker = Checker(verbose=verbose, _gtype_parents=gtype_parents) for fn in fns: print(f"checking {fn}…", file=sys.stderr) checker.test_file(fn) failures = checker.failures else: work = [(fn, gtype_parents, False) for fn in fns] nprocs = min(os.cpu_count() or 1, len(fns)) with multiprocessing.Pool(processes=nprocs) as pool: for file_failures in pool.imap_unordered(_check_file, work): failures.extend(file_failures) # show issues failures.sort(key=lambda f: (f.fn or "", f.linecnt or 0)) for failure in failures: line: str = "" if failure.fn: line += failure.fn if failure.linecnt: line += f":{failure.linecnt}" line += f": {failure.message}" if failure.nocheck: line += f" -- use a {failure.nocheck} comment to ignore" print(line) if failures: print(f"{len(failures)} failures!") return 1 if failures else 0 def unit_test(fn: str, verbose: bool = False) -> int: # load test file with any expected failures rc: int = 0 checker = Checker(verbose=verbose) with open(fn, "rb") as f: lines = f.read().decode().split("\n") for line in lines: if line.startswith(" * nocheck:expect:"): checker.add_expected_failure(line[19:]) checker.test_file(fn) # any unexpected failures for failure in checker.failures: if not failure.expected: print(f"{failure.fn} did not expect to see: {failure.message}") rc += 1 # check we got the ones we wanted for expect in checker._expected_failure_prefixes: if not checker.has_failure(expect): print(f"{fn} expected to see: {expect}") rc += 1 # print what we did get if rc: for failure in checker.failures: line_tmp: str = "" if failure.fn: line_tmp += failure.fn if failure.linecnt: line_tmp += f":{failure.linecnt}" line_tmp += f": {failure.message}" print(line_tmp) return rc if __name__ == "__main__": parser = argparse.ArgumentParser(description="Check source files") parser.add_argument("--test", type=str, help="Run self tests") parser.add_argument("--verbose", action="store_true", help="Run in verbose mode") args, argv = parser.parse_known_args() if args.test: _rc: int = 0 for _fn in glob.glob(f"{args.test}/*.[c|h]"): _rc += unit_test(_fn, args.verbose) sys.exit(_rc) # all done! sys.exit(test_files(argv, args.verbose))