|
| 1 | +#!/usr/bin/env python3 |
| 2 | + |
| 3 | +#credits to ACE3: https://github.com/acemod/ACE3/blob/master/tools/sqf_validator.py |
| 4 | + |
| 5 | +import fnmatch |
| 6 | +import os |
| 7 | +import re |
| 8 | +import ntpath |
| 9 | +import sys |
| 10 | +import argparse |
| 11 | + |
| 12 | +def validKeyWordAfterCode(content, index): |
| 13 | + keyWords = ["for", "do", "count", "each", "forEach", "else", "and", "not", "isEqualTo", "in", "call", "spawn", "execVM", "catch", "param", "select", "apply", "findIf", "remoteExec"]; |
| 14 | + for word in keyWords: |
| 15 | + try: |
| 16 | + subWord = content.index(word, index, index+len(word)) |
| 17 | + return True; |
| 18 | + except: |
| 19 | + pass |
| 20 | + return False |
| 21 | + |
| 22 | +def check_sqf_syntax(filepath): |
| 23 | + bad_count_file = 0 |
| 24 | + def pushClosing(t): |
| 25 | + closingStack.append(closing.expr) |
| 26 | + closing << Literal( closingFor[t[0]] ) |
| 27 | + |
| 28 | + def popClosing(): |
| 29 | + closing << closingStack.pop() |
| 30 | + |
| 31 | + with open(filepath, 'r', encoding='utf-8', errors='ignore') as file: |
| 32 | + content = file.read() |
| 33 | + |
| 34 | + # Store all brackets we find in this file, so we can validate everything on the end |
| 35 | + brackets_list = [] |
| 36 | + |
| 37 | + # To check if we are in a comment block |
| 38 | + isInCommentBlock = False |
| 39 | + checkIfInComment = False |
| 40 | + # Used in case we are in a line comment (//) |
| 41 | + ignoreTillEndOfLine = False |
| 42 | + # Used in case we are in a comment block (/* */). This is true if we detect a * inside a comment block. |
| 43 | + # If the next character is a /, it means we end our comment block. |
| 44 | + checkIfNextIsClosingBlock = False |
| 45 | + |
| 46 | + # We ignore everything inside a string |
| 47 | + isInString = False |
| 48 | + # Used to store the starting type of a string, so we can match that to the end of a string |
| 49 | + inStringType = ''; |
| 50 | + |
| 51 | + lastIsCurlyBrace = False |
| 52 | + checkForSemicolon = False |
| 53 | + onlyWhitespace = True |
| 54 | + |
| 55 | + # Extra information so we know what line we find errors at |
| 56 | + lineNumber = 1 |
| 57 | + |
| 58 | + indexOfCharacter = 0 |
| 59 | + # Parse all characters in the content of this file to search for potential errors |
| 60 | + for c in content: |
| 61 | + if (lastIsCurlyBrace): |
| 62 | + lastIsCurlyBrace = False |
| 63 | + # Test generates false positives with binary commands that take CODE as 2nd arg (e.g. findIf) |
| 64 | + checkForSemicolon = not re.search('findIf', content, re.IGNORECASE) |
| 65 | + |
| 66 | + if c == '\n': # Keeping track of our line numbers |
| 67 | + onlyWhitespace = True # reset so we can see if # is for a preprocessor command |
| 68 | + lineNumber += 1 # so we can print accurate line number information when we detect a possible error |
| 69 | + if (isInString): # while we are in a string, we can ignore everything else, except the end of the string |
| 70 | + if (c == inStringType): |
| 71 | + isInString = False |
| 72 | + # if we are not in a comment block, we will check if we are at the start of one or count the () {} and [] |
| 73 | + elif (isInCommentBlock == False): |
| 74 | + |
| 75 | + # This means we have encountered a /, so we are now checking if this is an inline comment or a comment block |
| 76 | + if (checkIfInComment): |
| 77 | + checkIfInComment = False |
| 78 | + if c == '*': # if the next character after / is a *, we are at the start of a comment block |
| 79 | + isInCommentBlock = True |
| 80 | + elif (c == '/'): # Otherwise, will check if we are in an line comment |
| 81 | + ignoreTillEndOfLine = True # and an line comment is a / followed by another / (//) We won't care about anything that comes after it |
| 82 | + |
| 83 | + if (isInCommentBlock == False): |
| 84 | + if (ignoreTillEndOfLine): # we are in a line comment, just continue going through the characters until we find an end of line |
| 85 | + if (c == '\n'): |
| 86 | + ignoreTillEndOfLine = False |
| 87 | + else: # validate brackets |
| 88 | + if (c == '"' or c == "'"): |
| 89 | + isInString = True |
| 90 | + inStringType = c |
| 91 | + elif (c == '#' and onlyWhitespace): |
| 92 | + ignoreTillEndOfLine = True |
| 93 | + elif (c == '/'): |
| 94 | + checkIfInComment = True |
| 95 | + elif (c == '('): |
| 96 | + brackets_list.append('(') |
| 97 | + elif (c == ')'): |
| 98 | + if (brackets_list[-1] in ['{', '[']): |
| 99 | + print("ERROR: Possible missing round bracket ')' detected at {0} Line number: {1}".format(filepath,lineNumber)) |
| 100 | + bad_count_file += 1 |
| 101 | + brackets_list.append(')') |
| 102 | + elif (c == '['): |
| 103 | + brackets_list.append('[') |
| 104 | + elif (c == ']'): |
| 105 | + if (brackets_list[-1] in ['{', '(']): |
| 106 | + print("ERROR: Possible missing square bracket ']' detected at {0} Line number: {1}".format(filepath,lineNumber)) |
| 107 | + bad_count_file += 1 |
| 108 | + brackets_list.append(']') |
| 109 | + elif (c == '{'): |
| 110 | + brackets_list.append('{') |
| 111 | + elif (c == '}'): |
| 112 | + lastIsCurlyBrace = True |
| 113 | + if (brackets_list[-1] in ['(', '[']): |
| 114 | + print("ERROR: Possible missing curly brace '}}' detected at {0} Line number: {1}".format(filepath,lineNumber)) |
| 115 | + bad_count_file += 1 |
| 116 | + brackets_list.append('}') |
| 117 | + elif (c== '\t'): |
| 118 | + print("ERROR: Tab detected at {0} Line number: {1}".format(filepath,lineNumber)) |
| 119 | + bad_count_file += 1 |
| 120 | + |
| 121 | + if (c not in [' ', '\t', '\n']): |
| 122 | + onlyWhitespace = False |
| 123 | + |
| 124 | + if (checkForSemicolon): |
| 125 | + if (c not in [' ', '\t', '\n', '/']): # keep reading until no white space or comments |
| 126 | + checkForSemicolon = False |
| 127 | + if (c not in [']', ')', '}', ';', ',', '&', '!', '|', '='] and not validKeyWordAfterCode(content, indexOfCharacter)): # , 'f', 'd', 'c', 'e', 'a', 'n', 'i']): |
| 128 | + print("ERROR: Possible missing semicolon ';' detected at {0} Line number: {1}".format(filepath,lineNumber)) |
| 129 | + bad_count_file += 1 |
| 130 | + |
| 131 | + else: # Look for the end of our comment block |
| 132 | + if (c == '*'): |
| 133 | + checkIfNextIsClosingBlock = True; |
| 134 | + elif (checkIfNextIsClosingBlock): |
| 135 | + if (c == '/'): |
| 136 | + isInCommentBlock = False |
| 137 | + elif (c != '*'): |
| 138 | + checkIfNextIsClosingBlock = False |
| 139 | + indexOfCharacter += 1 |
| 140 | + |
| 141 | + if brackets_list.count('[') != brackets_list.count(']'): |
| 142 | + print("ERROR: A possible missing square bracket [ or ] in file {0} [ = {1} ] = {2}".format(filepath,brackets_list.count('['),brackets_list.count(']'))) |
| 143 | + bad_count_file += 1 |
| 144 | + if brackets_list.count('(') != brackets_list.count(')'): |
| 145 | + print("ERROR: A possible missing round bracket ( or ) in file {0} ( = {1} ) = {2}".format(filepath,brackets_list.count('('),brackets_list.count(')'))) |
| 146 | + bad_count_file += 1 |
| 147 | + if brackets_list.count('{') != brackets_list.count('}'): |
| 148 | + print("ERROR: A possible missing curly brace {{ or }} in file {0} {{ = {1} }} = {2}".format(filepath,brackets_list.count('{'),brackets_list.count('}'))) |
| 149 | + bad_count_file += 1 |
| 150 | + pattern = re.compile('\s*(/\*[\s\S]+?\*/)\s*#include') |
| 151 | + if pattern.match(content): |
| 152 | + print("ERROR: A found #include after block comment in file {0}".format(filepath)) |
| 153 | + bad_count_file += 1 |
| 154 | + |
| 155 | + |
| 156 | + |
| 157 | + return bad_count_file |
| 158 | + |
| 159 | +def main(): |
| 160 | + |
| 161 | + print("Validating SQF") |
| 162 | + |
| 163 | + sqf_list = [] |
| 164 | + bad_count = 0 |
| 165 | + |
| 166 | + parser = argparse.ArgumentParser() |
| 167 | + parser.add_argument('-m','--module', help='only search specified module addon folder', required=False, default="") |
| 168 | + args = parser.parse_args() |
| 169 | + |
| 170 | + # Allow running from root directory as well as from inside the tools directory |
| 171 | + rootDir = "../addons" |
| 172 | + if (os.path.exists("addons")): |
| 173 | + rootDir = "addons" |
| 174 | + |
| 175 | + for root, dirnames, filenames in os.walk(rootDir + '/' + args.module): |
| 176 | + for filename in fnmatch.filter(filenames, '*.sqf'): |
| 177 | + sqf_list.append(os.path.join(root, filename)) |
| 178 | + |
| 179 | + for filename in sqf_list: |
| 180 | + bad_count = bad_count + check_sqf_syntax(filename) |
| 181 | + |
| 182 | + |
| 183 | + print("------\nChecked {0} files\nErrors detected: {1}".format(len(sqf_list), bad_count)) |
| 184 | + if (bad_count == 0): |
| 185 | + print("SQF validation PASSED") |
| 186 | + else: |
| 187 | + print("SQF validation FAILED") |
| 188 | + |
| 189 | + return bad_count |
| 190 | + |
| 191 | +if __name__ == "__main__": |
| 192 | + sys.exit(main()) |
0 commit comments