import glob import re import os import sys import time from collections import namedtuple Failure = namedtuple("Failure", ["lineno", "message"]) RED = "\033[0;31m" GREEN = "\033[0;32m" BLUE = "\033[0;34m" NC = "\033[0m" # No Color IGNORE_515_PROC_MARKER_FILENAME = "__byond_version_compat.dm" CHECK_515_PROC_MARKER_RE = re.compile(r"\.proc/") def check_515_proc_syntax(lines): for idx, line in enumerate(lines): if CHECK_515_PROC_MARKER_RE.search(line): return Failure(idx + 1, "Outdated proc reference use detected in code. Please use proc reference helpers.") CHECK_SPACE_INDENTATION_RE = re.compile(r"^ {2,}[^\*]") def check_space_indentation(lines): """ Check specifically for space-significant indentation. Excludes dmdoc block comment lines so long as there is an asterisk immediately after the leading spaces. >>> bool(check_space_indentation([" foo"])) True >>> bool(check_space_indentation(["\\tfoo"])) False >>> bool(check_space_indentation([" * foo"])) False """ for idx, line in enumerate(lines): if CHECK_SPACE_INDENTATION_RE.match(line): return Failure(idx + 1, "Space indentation detected, please use tab indentation.") CHECK_MIXED_INDENTATION_RE = re.compile(r"^(\t+ | +\t)\s*[^\s\*]") def check_mixed_indentation(lines): """ Check specifically for leading whitespace which contains a mix of tab and space characters. Excludes dmdoc block comment lines so long as there is an asterisk immediately after the leading whitespace. >>> bool(check_mixed_indentation(["\\t\\t foo"])) True >>> bool(check_mixed_indentation(["\\t \\t foo"])) True >>> bool(check_mixed_indentation(["\\t // foo"])) True >>> bool(check_mixed_indentation([" \\tfoo"])) True >>> bool(check_mixed_indentation([" \\t foo"])) True >>> bool(check_mixed_indentation(["\\t * foo"])) False >>> bool(check_mixed_indentation(["\\t\\t* foo"])) False >>> bool(check_mixed_indentation(["\\t \\t * foo"])) False """ for idx, line in enumerate(lines): if CHECK_MIXED_INDENTATION_RE.match(line): return Failure(idx + 1, "Mixed indentation detected, please stick to tab indentation.") def check_trailing_newlines(lines): lines = [x for x in lines] if lines and not lines[-1].endswith("\n"): return Failure(len(lines), "Missing a trailing newline") GLOBAL_VARS_RE = re.compile(r"^/*var/") def check_global_vars(lines): for idx, line in enumerate(lines): if GLOBAL_VARS_RE.match(line): return Failure(idx + 1, "Unmanaged global var use detected in code, please use the helpers.") PROC_ARGS_WITH_VAR_PREFIX_RE = re.compile(r"^/[\w/]\S+\(.*(var/|, ?var/.*).*\)") def check_proc_args_with_var_prefix(lines): for idx, line in enumerate(lines): if PROC_ARGS_WITH_VAR_PREFIX_RE.match(line): return Failure(idx + 1, "Changed files contains a proc argument starting with 'var'.") NANOTRASEN_CAMEL_CASE = re.compile(r"NanoTrasen") def check_for_nanotrasen_camel_case(lines): for idx, line in enumerate(lines): if NANOTRASEN_CAMEL_CASE.search(line): return Failure(idx + 1, "Nanotrasen should not be spelled in the camel case form.") TO_CHAT_WITH_NO_USER_ARG_RE = re.compile(r"to_chat\(\"") def check_to_chats_have_a_user_arguement(lines): for idx, line in enumerate(lines): if TO_CHAT_WITH_NO_USER_ARG_RE.search(line): return Failure(idx + 1, "Changed files contains a to_chat() procedure without a user argument.") CODE_CHECKS = [ check_space_indentation, check_mixed_indentation, check_trailing_newlines, check_global_vars, check_proc_args_with_var_prefix, check_for_nanotrasen_camel_case, check_to_chats_have_a_user_arguement, ] if __name__ == "__main__": print("check_grep2 started") exit_code = 0 start = time.time() for code_filepath in glob.glob("**/*.dm", recursive=True): with open(code_filepath, encoding="UTF-8") as code: filename = code_filepath.split(os.path.sep)[-1] # 515 proc syntax check is unique in running on all files but one, # but I'm not going to make some disproportionately generic "check" # that also validates that the test should be run, so it just goes # here. if filename != IGNORE_515_PROC_MARKER_FILENAME: if failure := check_515_proc_syntax(code): exit_code = 1 print(f"{code_filepath}:{failure.lineno}: {RED}{failure.message}{NC}") for check in CODE_CHECKS: code.seek(0) if failure := check(code): exit_code = 1 print(f"{code_filepath}:{failure.lineno}: {RED}{failure.message}{NC}") end = time.time() print(f"\ncheck_grep2 tests completed in {end - start:.2f}s\n") sys.exit(exit_code)