Files
CHOMPStation2/tools/ticked_file_enforcement/ticked_file_enforcement.py
2025-08-12 08:46:46 +02:00

157 lines
4.7 KiB
Python

import codecs
import fnmatch
import functools
import glob
import json
import os
import sys
# simple way to check if we're running on github actions, or on a local machine
on_github = os.getenv("GITHUB_ACTIONS") == "true"
def green(text):
return "\033[32m" + str(text) + "\033[0m"
def red(text):
return "\033[31m" + str(text) + "\033[0m"
def blue(text):
return "\033[34m" + str(text) + "\033[0m"
schema = json.load(sys.stdin)
file_reference = schema["file"]
file_reference_basename = os.path.basename(file_reference)
scannable_directory = schema["scannable_directory"]
subdirectories = schema["subdirectories"]
FORBIDDEN_INCLUDES = schema["forbidden_includes"]
excluded_files = schema["excluded_files"]
def post_error(string):
print(red(f"Ticked File Enforcement [{file_reference}]: " + string))
if on_github:
print(f"::error file={file_reference},line=1,title=Ticked File Enforcement::{string}")
for excluded_file in excluded_files:
full_file_path = scannable_directory + excluded_file
if not os.path.isfile(full_file_path):
post_error(f"Excluded file {full_file_path} does not exist, please remove it!")
sys.exit(1)
file_extensions = ("dm", "dmf")
reading = False
lines = []
total = 0
with open(file_reference, 'r') as file:
for line in file:
total += 1
line = line.strip()
if line == "// BEGIN_INCLUDE":
reading = True
continue
elif line == "// END_INCLUDE":
break
elif not reading:
continue
lines.append(line)
offset = total - len(lines)
print(blue(f"Ticked File Enforcement: {offset} lines were ignored in output for [{file_reference}]."))
fail_no_include = False
scannable_files = []
for file_extension in file_extensions:
compiled_directory = f"{scannable_directory}/**/*.{file_extension}"
scannable_files += glob.glob(compiled_directory, recursive=True)
if len(scannable_files) == 0:
post_error(f"No files were found in {scannable_directory}. Ticked File Enforcement has failed!")
sys.exit(1)
for code_file in scannable_files:
dm_path = ""
if subdirectories is True:
dm_path = code_file.replace('/', '\\')
else:
dm_path = os.path.basename(code_file)
included = f"#include \"{dm_path}\"" in lines
forbid_include = False
for forbidable in FORBIDDEN_INCLUDES:
if not fnmatch.fnmatch(code_file, forbidable):
continue
forbid_include = True
if included:
post_error(f"{dm_path} should NOT be included.")
fail_no_include = True
if forbid_include:
continue
if not included:
if(dm_path == file_reference_basename):
continue
if(dm_path in excluded_files):
continue
post_error(f"Missing include for {dm_path}.")
fail_no_include = True
if fail_no_include:
sys.exit(1)
def compare_lines(a, b):
# Remove initial include as well as the final quotation mark
a = a[len("#include \""):-1].lower()
b = b[len("#include \""):-1].lower()
split_by_period = a.split('.')
a_suffix = ""
if len(split_by_period) >= 2:
a_suffix = split_by_period[len(split_by_period) - 1]
split_by_period = b.split('.')
b_suffix = ""
if len(split_by_period) >= 2:
b_suffix = split_by_period[len(split_by_period) - 1]
a_segments = a.split('\\')
b_segments = b.split('\\')
for (a_segment, b_segment) in zip(a_segments, b_segments):
a_is_file = a_segment.endswith(file_extensions)
b_is_file = b_segment.endswith(file_extensions)
# code\something.dm will ALWAYS come before code\directory\something.dm
if a_is_file and not b_is_file:
return -1
if b_is_file and not a_is_file:
return 1
# interface\something.dm will ALWAYS come after code\something.dm
if a_segment != b_segment:
# if we're at the end of a compare, then this is about the file name
# files with longer suffixes come after ones with shorter ones
if a_suffix != b_suffix:
return (a_suffix > b_suffix) - (a_suffix < b_suffix)
return (a_segment > b_segment) - (a_segment < b_segment)
print(f"Two lines were exactly the same ({a} vs. {b})")
sys.exit(1)
sorted_lines = sorted(lines, key = functools.cmp_to_key(compare_lines))
for (index, line) in enumerate(lines):
if sorted_lines[index] != line:
post_error(f"The include at line {index + offset} is out of order ({line}, expected {sorted_lines[index]})")
sys.exit(1)
print(green(f"Ticked File Enforcement: [{file_reference}] All includes (for {len(scannable_files)} scanned files) are in order!"))