#!/usr/bin/env python3 import fnmatch import os import re import sys def valid_keyword_after_code(content, index): for word in ["for", "do", "count", "each", "forEach", "else", "and", "not", "isEqualTo", "isNotEqualTo", "in", "call", "spawn", "execVM", "catch", "param", "select", "apply", "findIf", "remoteExec"]: if content.find(word, index, index + len(word)) != -1: return True return False def check_sqf(filepath): errors = [] with open(filepath, "r", encoding = "utf-8", errors = "ignore") as file: content = file.read() # Store all brackets we find in this file, so we can validate everything on the end brackets = [] # Used in case we are in a line comment (//) ignore_till_eol = False # To check if we are in a comment block in_comment_block = False check_if_comment = False # Used in case we are in a comment block (/* */) # This is true if we detect a * inside a comment block # If the next character is a /, it means we end our comment block check_if_closing = False # We ignore everything inside a string in_string = False # Used to store the starting type of a string, so we can match that to the end of a string string_type = "" # Used to check for semicolon after code blocks last_is_curly_brace = False check_for_semicolon = False # Extra information so we know what line we find errors at line_number = 1 char_index = 0 for c in content: if last_is_curly_brace: last_is_curly_brace = False # Test generates false positives with binary commands that take CODE as 2nd arg (e.g. findIf) check_for_semicolon = not re.search("findIf", content, re.IGNORECASE) # Keep track of current line number if c == "\n": line_number += 1 # While we are in a string, we can ignore everything else, except the end of the string if in_string: if c == string_type: in_string = False # Look for the end of this comment block elif in_comment_block: if c == "*": check_if_closing = True elif check_if_closing: if c == "/": in_comment_block = False elif c != "*": check_if_closing = False # If we are not in a comment block, we will check if we are at the start of one or count the () {} and [] else: # This means we have encountered a /, so we are now checking if this is an inline comment or a comment block if check_if_comment: check_if_comment = False # If the next character after / is a *, we are at the start of a comment block if c == "*": in_comment_block = True # Otherwise, check if we are in an line comment, / followed by another / (//) elif c == "/": ignore_till_eol = True if not in_comment_block: if ignore_till_eol: # We are in a line comment, just continue going through the characters until we find an end of line if c == "\n": ignore_till_eol = False else: if c == '"' or c == "'": in_string = True string_type = c elif c == "/": check_if_comment = True elif c == "\t": errors.append(" ERROR: Found a tab on line {}.".format(line_number)) elif c in ["(", "[", "{"]: brackets.append(c) elif c == ")": if not brackets or brackets[-1] in ["[", "{"]: errors.append(" ERROR: Missing parenthesis '(' on line {}.".format(line_number)) brackets.append(c) elif c == "]": if not brackets or brackets[-1] in ["(", "{"]: errors.append(" ERROR: Missing square bracket '[' on line {}.".format(line_number)) brackets.append(c) elif c == "}": last_is_curly_brace = True if not brackets or brackets[-1] in ["(", "["]: errors.append(" ERROR: Missing curly brace '{{' on line {}.".format(line_number)) brackets.append(c) if check_for_semicolon: # Keep reading until no white space or comments if c not in [" ", "\t", "\n", "/"]: check_for_semicolon = False if c not in ["]", ")", "}", ";", ",", "&", "!", "|", "="] and not valid_keyword_after_code(content, char_index): errors.append(" ERROR: Possible missing semicolon ';' on line {}.".format(line_number)) char_index += 1 # Compare opening and closing bracket counts if brackets.count("(") != brackets.count(")"): errors.append(" ERROR: Unequal number of parentheses, '(' = {}, ')' = {}.".format(brackets.count("("), brackets.count(")"))) if brackets.count("[") != brackets.count("]"): errors.append(" ERROR: Unequal number of square brackets, '[' = {}, ']' = {}.".format(brackets.count("["), brackets.count("]"))) if brackets.count("{") != brackets.count("}"): errors.append(" ERROR: Unequal number of curly braces, '{{' = {}, '}}' = {}.".format(brackets.count("{"), brackets.count("}"))) # Ensure includes are before block comments if re.compile('\s*(/\*[\s\S]+?\*/)\s*#include').match(content): errors.append(" ERROR: Found an #include after a block comment.") return errors def main(): print("Validating SQF") print("--------------") # Allow running from root directory and tools directory root_dir = ".." if os.path.exists("addons"): root_dir = "." # Check all SQF files in the project directory sqf_files = [] for root, _, files in os.walk(root_dir): for file in fnmatch.filter(files, "*.sqf"): sqf_files.append(os.path.join(root, file)) sqf_files.sort() bad_count = 0 for filepath in sqf_files: errors = check_sqf(filepath) if errors: print("\nFound {} error(s) in {}:".format(len(errors), os.path.relpath(filepath, root_dir))) for error in errors: print(error) bad_count += 1 print("\nChecked {} files, found errors in {}.".format(len(sqf_files), bad_count)) if bad_count == 0: print("SQF Validation PASSED") else: print("SQF Validation FAILED") return bad_count if __name__ == "__main__": sys.exit(main())