mirror of
https://github.com/ParadiseSS13/Paradise.git
synced 2026-01-01 05:02:33 +00:00
262 lines
12 KiB
Python
262 lines
12 KiB
Python
import glob
|
|
import re
|
|
import os
|
|
import sys
|
|
import time
|
|
from collections import namedtuple
|
|
from concurrent.futures import ProcessPoolExecutor
|
|
Failure = namedtuple("Failure", ["filename", "lineno", "message"])
|
|
|
|
RED = "\033[0;31m"
|
|
GREEN = "\033[0;32m"
|
|
BLUE = "\033[0;34m"
|
|
NC = "\033[0m" # No Color
|
|
|
|
def print_error(message: str, filename: str, line_number: int):
|
|
if os.getenv("GITHUB_ACTIONS") == "true": # We're on github, output in a special format.
|
|
print(f"::error file={filename},line={line_number},title=Check Grep::{filename}:{line_number}: {RED}{message}{NC}")
|
|
else:
|
|
print(f"{filename}:{line_number}: {RED}{message}{NC}")
|
|
|
|
IGNORE_515_PROC_MARKER_FILENAME = "__proc_refs.dm"
|
|
CHECK_515_PROC_MARKER_RE = re.compile(r"\.proc/")
|
|
def check_515_proc_syntax(idx, line):
|
|
if CHECK_515_PROC_MARKER_RE.search(line):
|
|
return [(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(idx, line):
|
|
"""
|
|
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
|
|
"""
|
|
if CHECK_SPACE_INDENTATION_RE.match(line):
|
|
return [(idx + 1, "Space indentation detected, please use tab indentation.")]
|
|
|
|
|
|
CHECK_MIXED_INDENTATION_RE = re.compile(r"^(\t+ | +\t)\s*[^\s\*]")
|
|
def check_mixed_indentation(idx, line):
|
|
"""
|
|
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
|
|
"""
|
|
if CHECK_MIXED_INDENTATION_RE.match(line):
|
|
return [(idx + 1, "Mixed <tab><space> indentation detected, please stick to tab indentation.")]
|
|
|
|
|
|
GLOBAL_VARS_RE = re.compile(r"^/*var/")
|
|
def check_global_vars(idx, line):
|
|
if GLOBAL_VARS_RE.match(line):
|
|
return [(idx + 1, "Unmanaged global var use detected in code, please use the helpers.")]
|
|
|
|
|
|
TOPLEVEL_VARDECLS_RE = re.compile(r"^(/[^\*(\/\/)].*/var/(list/)?\w+)")
|
|
def check_toplevel_vardecls(idx, line):
|
|
if match := TOPLEVEL_VARDECLS_RE.match(line):
|
|
return [(idx + 1, f"Top-level var {match.group(0)} found, please move to type declaration.")]
|
|
|
|
|
|
PROC_ARGS_WITH_VAR_PREFIX_RE = re.compile(r"^/[\w/]\S+\(.*(var/|, ?var/.*).*\)")
|
|
def check_proc_args_with_var_prefix(idx, line):
|
|
if PROC_ARGS_WITH_VAR_PREFIX_RE.match(line):
|
|
return [(idx + 1, "Changed files contains a proc argument starting with 'var'.")]
|
|
|
|
NANOTRASEN_CAMEL_CASE = re.compile(r"NanoTrasen")
|
|
def check_for_nanotrasen_camel_case(idx, line):
|
|
if NANOTRASEN_CAMEL_CASE.search(line):
|
|
return [(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(idx, line):
|
|
if TO_CHAT_WITH_NO_USER_ARG_RE.search(line):
|
|
return [(idx + 1, "Changed files contains a to_chat() procedure without a user argument.")]
|
|
|
|
CONDITIONAL_LEADING_SPACE = re.compile(r"(if|for|while|switch)\s+(\(.*?\)?)") # checks for "if (thing)", replace with $1$2
|
|
CONDITIONAL_BEGINNING_SPACE = re.compile(r"(if|for|while|switch)\((!?) (.+\)?)") # checks for "if( thing)", replace with $1($2$3
|
|
CONDITIONAL_ENDING_SPACE = re.compile(r"(if|for|while|switch)(\(.+) \)") # checks for "if(thing )", replace with $1$2)
|
|
CONDITIONAL_DOUBLE_PARENTHESIS = re.compile(r"(if)\((\([^)]+\))\)$") # checks for if((thing)), replace with $1$2
|
|
# To fix any of these, run them as regex in VSCode, with the appropriate replacement
|
|
# It may be a good idea to turn the replacement into a script someday
|
|
def check_conditional_spacing(idx, line):
|
|
failures = []
|
|
if CONDITIONAL_LEADING_SPACE.search(line):
|
|
failures.append((idx + 1, "Found a conditional statement matching the format \"if (thing)\" (irregular spacing), please use \"if(thing)\" instead."))
|
|
if CONDITIONAL_BEGINNING_SPACE.search(line):
|
|
failures.append((idx + 1, "Found a conditional statement matching the format \"if( thing)\" (irregular spacing), please use \"if(thing)\" instead."))
|
|
if CONDITIONAL_ENDING_SPACE.search(line):
|
|
failures.append((idx + 1, "Found a conditional statement matching the format \"if(thing )\" (irregular spacing), please use \"if(thing)\" instead."))
|
|
if CONDITIONAL_DOUBLE_PARENTHESIS.search(line):
|
|
failures.append((idx + 1, "Found a conditional statement matching the format \"if((thing))\" (unnecessary outer parentheses), please use \"if(thing)\" instead."))
|
|
|
|
return failures
|
|
|
|
# makes sure that no global list inits have an empty list in them without using the helper
|
|
GLOBAL_LIST_EMPTY = re.compile(r"(?<!#define GLOBAL_LIST_EMPTY\(X\) )GLOBAL_LIST_INIT([^,]+),.{0,5}list\(\)")
|
|
# This uses a negative look behind to make sure its not the global list definition
|
|
# An easy regex replacement for this is GLOBAL_LIST_EMPTY$1
|
|
def check_global_list_empty(idx, line):
|
|
if GLOBAL_LIST_EMPTY.search(line):
|
|
failures.append((idx + 1, "Found a GLOBAL_LIST_INIT(_, list()), please use GLOBAL_LIST_EMPTY(_) instead."))
|
|
|
|
# makes sure arguments contained within "ui = new" are valid
|
|
TGUI_UI_NEW = re.compile(r"ui = new\(((?:(?!,\s*).)+,\s*){1,3}(?:(?!,\s*).)+\)")
|
|
def check_tgui_ui_new_argument(idx, line):
|
|
if "\tui = new" in line and not TGUI_UI_NEW.search(line):
|
|
return [(idx + 1, "Invalid argument within constructor, please make sure window sizing is in corresponding JavaScript file.")]
|
|
|
|
# checks for any (for var/type/x) loops that are not looping over bare datums: enforcing that the only case we will see this is if you genuinely want to loop over all datums in memory
|
|
FOR_ALL_DATUMS = re.compile(r"for\s*\(\s*var\/((\w+)(?:(?:\/\w+){2,})?)\)")
|
|
# double-checks that we don't have any attempts at looping like for(var/atom/a)
|
|
FOR_ALL_NOT_DATUMS = re.compile(r"for\s*\(\s*var\/((?:atom|area|turf|obj|mob)(?:\/\w+))\)")
|
|
def check_datum_loops(idx, line):
|
|
if FOR_ALL_DATUMS.search(line) or FOR_ALL_NOT_DATUMS.search(line):
|
|
return [(
|
|
idx + 1,
|
|
# yes this will concatenate the strings, don't look too hard
|
|
"Found a for loop without explicit contents. If you're trying to loop over everything in the world, first double check that you truly need to, and if so specify \'in world\'.\n"
|
|
"If you're trying to check bare datums, please ensure that your value is only cast to /datum, and please make sure you use \'as anything\', or use a global list instead."
|
|
)]
|
|
|
|
HREF_OLD_STYLE = re.compile(r"href[\s='\"\\]*\?")
|
|
def check_href_styles(idx, line):
|
|
if HREF_OLD_STYLE.search(line):
|
|
return [(idx + 1, "BYOND requires internal href links to begin with \"byond://\"")]
|
|
|
|
INITIALIZE_MISSING_MAPLOAD = re.compile(
|
|
r"^/(obj|mob|turf|area|atom)/.+/Initialize\((?!mapload).*\)"
|
|
)
|
|
def check_initialize_missing_mapload(idx, line):
|
|
if INITIALIZE_MISSING_MAPLOAD.search(line):
|
|
return [(idx + 1, "Initialize override without 'mapload' argument.")]
|
|
|
|
# TODO: This finds most cases except for e.g. `list(1, 2, 3 )`
|
|
# Find a way to include this without breaking macro/tab-aligned versions such as `list( \`
|
|
# Maybe even make sure it doesn't include comments, idk
|
|
EMPTY_LIST_WHITESPACE = re.compile(r"list\([^\S\n\r\f]+.*?[^\\]\n")
|
|
def check_empty_list_whitespace(idx, line):
|
|
if EMPTY_LIST_WHITESPACE.search(line):
|
|
return [(idx + 1, "Empty list declarations should not have any whitespace within their parentheses.")]
|
|
|
|
IGNORE_ATOM_ICON_FILE = "atoms.dm"
|
|
NO_MANUAL_ICON_UPDATES = re.compile(r"([\s.])(update_icon_state|update_desc|update_overlays|update_name)\(.*\)")
|
|
def check_manual_icon_updates(idx, line):
|
|
if result := NO_MANUAL_ICON_UPDATES.search(line):
|
|
proc_result = result.group(2)
|
|
target = "update_icon"
|
|
if(proc_result == "update_name" or proc_result == "update_desc"):
|
|
target = "update_appearance"
|
|
return [(idx + 1, f"{proc_result}() should not be called manually. Use {target}({proc_result.upper()}) instead.")]
|
|
|
|
CONDITIONAL_ISTYPE_SRC = re.compile(r"if.+istype\(src,\s?\/[^turf]")
|
|
def check_istype_src(idx, line):
|
|
if CONDITIONAL_ISTYPE_SRC.search(line):
|
|
return [(idx + 1, "Our coding requirements prohibit use of istype(src, /any_type). Consider making the behavior dependent on a variable and/or overriding a proc instead.")]
|
|
|
|
CAMEL_CASE_TYPE_NAMES = re.compile(r"^/[\w]\S+/{1}([a-zA-Z]+([A-Z][a-z]+)+|([A-Z]+[a-z]+))$")
|
|
def check_camel_case_type_names(idx, line):
|
|
if result := CAMEL_CASE_TYPE_NAMES.search(line):
|
|
type_result = result.group(0)
|
|
return [(idx + 1, f"name of type {type_result} is not in snake_case format.")]
|
|
|
|
UID_WITH_PARAMETER = re.compile(r"\bUID\(\w+\)")
|
|
def check_uid_parameters(idx, line):
|
|
if result := UID_WITH_PARAMETER.search(line):
|
|
return [(idx + 1, "UID() does not take arguments. Use UID() instead of UID(src) and datum.UID() instead of UID(datum).")]
|
|
|
|
CODE_CHECKS = [
|
|
check_space_indentation,
|
|
check_mixed_indentation,
|
|
check_global_vars,
|
|
check_toplevel_vardecls,
|
|
check_proc_args_with_var_prefix,
|
|
check_for_nanotrasen_camel_case,
|
|
check_to_chats_have_a_user_arguement,
|
|
check_conditional_spacing,
|
|
check_global_list_empty,
|
|
check_tgui_ui_new_argument,
|
|
check_datum_loops,
|
|
check_href_styles,
|
|
check_initialize_missing_mapload,
|
|
check_empty_list_whitespace,
|
|
check_istype_src,
|
|
check_camel_case_type_names,
|
|
check_uid_parameters,
|
|
]
|
|
|
|
def lint_file(code_filepath: str) -> list[Failure]:
|
|
all_failures = []
|
|
with open(code_filepath, encoding="UTF-8") as code:
|
|
filename = code_filepath.split(os.path.sep)[-1]
|
|
|
|
extra_checks = []
|
|
if filename != IGNORE_515_PROC_MARKER_FILENAME:
|
|
extra_checks.append(check_515_proc_syntax)
|
|
if filename != IGNORE_ATOM_ICON_FILE:
|
|
extra_checks.append(check_manual_icon_updates)
|
|
|
|
last_line = None
|
|
for idx, line in enumerate(code):
|
|
for check in CODE_CHECKS + extra_checks:
|
|
if failures := check(idx, line):
|
|
all_failures += [Failure(code_filepath, lineno, message) for lineno, message in failures]
|
|
last_line = line
|
|
|
|
if last_line and last_line[-1] != '\n':
|
|
all_failures.append(Failure(code_filepath, idx + 1, "Missing a trailing newline"))
|
|
return all_failures
|
|
|
|
if __name__ == "__main__":
|
|
print("check_grep2 started")
|
|
|
|
exit_code = 0
|
|
start = time.time()
|
|
|
|
dm_files = glob.glob("**/*.dm", recursive=True)
|
|
|
|
if len(sys.argv) > 1:
|
|
dm_files = [sys.argv[1]]
|
|
|
|
all_failures = []
|
|
with ProcessPoolExecutor() as executor:
|
|
for failures in executor.map(lint_file, dm_files):
|
|
all_failures += failures
|
|
|
|
if all_failures:
|
|
exit_code = 1
|
|
for failure in all_failures:
|
|
print_error(failure.message, failure.filename, failure.lineno)
|
|
|
|
end = time.time()
|
|
print(f"\ncheck_grep2 tests completed in {end - start:.2f}s\n")
|
|
|
|
sys.exit(exit_code)
|