Skip to content

Commit

Permalink
add sqf validator
Browse files Browse the repository at this point in the history
  • Loading branch information
OverlordZorn committed Sep 22, 2023
1 parent dcb04e2 commit defa199
Show file tree
Hide file tree
Showing 3 changed files with 205 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# These are supported funding model platforms

github: [PulsarNeutronStar] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: [24cvo] # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
custom: # https://www.paypal.me/Vdauphin # Replace with a single custom sponsorship URL
18 changes: 18 additions & 0 deletions .github/workflows/sqf-validator.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: SQF Validator

on:
push:
branches:
- master_daily
- master

jobs:
run_python:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.9'
architecture: 'x64'
- run: python tools/sqf_validator.py
178 changes: 178 additions & 0 deletions tools/sqf_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
#!/usr/bin/env python3

import fnmatch
import os
import re
import ntpath
import sys
import argparse

def validKeyWordAfterCode(content, index):
keyWords = ["for", "do", "count", "each", "forEach", "else", "and", "not", "isEqualTo", "in", "call", "spawn", "execVM", "catch", "param", "select", "apply", "findIf", "remoteExec"];
for word in keyWords:
try:
subWord = content.index(word, index, index+len(word))
return True;
except:
pass
return False

def check_sqf_syntax(filepath):
bad_count_file = 0
def pushClosing(t):
closingStack.append(closing.expr)
closing << Literal( closingFor[t[0]] )

def popClosing():
closing << closingStack.pop()

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_list = []

# To check if we are in a comment block
isInCommentBlock = False
checkIfInComment = False
# Used in case we are in a line comment (//)
ignoreTillEndOfLine = 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.
checkIfNextIsClosingBlock = False

# We ignore everything inside a string
isInString = False
# Used to store the starting type of a string, so we can match that to the end of a string
inStringType = '';

lastIsCurlyBrace = False
checkForSemiColon = False

# Extra information so we know what line we find errors at
lineNumber = 0

indexOfCharacter = 0
# Parse all characters in the content of this file to search for potential errors
for c in content:
if (lastIsCurlyBrace):
lastIsCurlyBrace = False
# Test generates false positives with binary commands that take CODE as 2nd arg (e.g. findIf)
checkForSemiColon = not re.search('findIf', content, re.IGNORECASE)

if c == '\n': # Keeping track of our line numbers
lineNumber += 1 # so we can print accurate line number information when we detect a possible error
if (isInString): # while we are in a string, we can ignore everything else, except the end of the string
if (c == inStringType):
isInString = False
# if we are not in a comment block, we will check if we are at the start of one or count the () {} and []
elif (isInCommentBlock == False):

# This means we have encountered a /, so we are now checking if this is an inline comment or a comment block
if (checkIfInComment):
checkIfInComment = False
if c == '*': # if the next character after / is a *, we are at the start of a comment block
isInCommentBlock = True
elif (c == '/'): # Otherwise, will check if we are in an line comment
ignoreTillEndOfLine = True # and an line comment is a / followed by another / (//) We won't care about anything that comes after it

if (isInCommentBlock == False):
if (ignoreTillEndOfLine): # we are in a line comment, just continue going through the characters until we find an end of line
if (c == '\n'):
ignoreTillEndOfLine = False
else: # validate brackets
if (c == '"' or c == "'"):
isInString = True
inStringType = c
elif (c == '#'):
ignoreTillEndOfLine = True
elif (c == '/'):
checkIfInComment = True
elif (c == '('):
brackets_list.append('(')
elif (c == ')'):
if (brackets_list[-1] in ['{', '[']):
print("ERROR: Possible missing round bracket ')' detected at {0} Line number: {1}".format(filepath,lineNumber))
bad_count_file += 1
brackets_list.append(')')
elif (c == '['):
brackets_list.append('[')
elif (c == ']'):
if (brackets_list[-1] in ['{', '(']):
print("ERROR: Possible missing square bracket ']' detected at {0} Line number: {1}".format(filepath,lineNumber))
bad_count_file += 1
brackets_list.append(']')
elif (c == '{'):
brackets_list.append('{')
elif (c == '}'):
lastIsCurlyBrace = True
if (brackets_list[-1] in ['(', '[']):
print("ERROR: Possible missing curly brace '}}' detected at {0} Line number: {1}".format(filepath,lineNumber))
bad_count_file += 1
brackets_list.append('}')
elif (c== '\t'):
print("ERROR: Tab detected at {0} Line number: {1}".format(filepath,lineNumber))
bad_count_file += 1

if (checkForSemiColon):
if (c not in [' ', '\t', '\n', '/']): # keep reading until no white space or comments
checkForSemiColon = False
if (c not in [']', ')', '}', ';', ',', '&', '!', '|', '='] and not validKeyWordAfterCode(content, indexOfCharacter)): # , 'f', 'd', 'c', 'e', 'a', 'n', 'i']):
print("ERROR: Possible missing semicolon ';' detected at {0} Line number: {1}".format(filepath,lineNumber))
bad_count_file += 1

else: # Look for the end of our comment block
if (c == '*'):
checkIfNextIsClosingBlock = True;
elif (checkIfNextIsClosingBlock):
if (c == '/'):
isInCommentBlock = False
elif (c != '*'):
checkIfNextIsClosingBlock = False
indexOfCharacter += 1

if brackets_list.count('[') != brackets_list.count(']'):
print("ERROR: A possible missing square bracket [ or ] in file {0} [ = {1} ] = {2}".format(filepath,brackets_list.count('['),brackets_list.count(']')))
bad_count_file += 1
if brackets_list.count('(') != brackets_list.count(')'):
print("ERROR: A possible missing round bracket ( or ) in file {0} ( = {1} ) = {2}".format(filepath,brackets_list.count('('),brackets_list.count(')')))
bad_count_file += 1
if brackets_list.count('{') != brackets_list.count('}'):
print("ERROR: A possible missing curly brace {{ or }} in file {0} {{ = {1} }} = {2}".format(filepath,brackets_list.count('{'),brackets_list.count('}')))
bad_count_file += 1
return bad_count_file

def main():

print("Validating SQF")

sqf_list = []
bad_count = 0

parser = argparse.ArgumentParser()
parser.add_argument('-m','--module', help='only search specified module addon folder', required=False, default="")
args = parser.parse_args()

# Allow running from root directory as well as from inside the tools directory
rootDir = "./=BTC=co@30_Hearts_and_Minds.Altis"
if (os.path.exists("addons")):
rootDir = "addons"

for root, dirnames, filenames in os.walk(rootDir + '/' + args.module):
for filename in fnmatch.filter(filenames, '*.sqf'):
sqf_list.append(os.path.join(root, filename))

for filename in sqf_list:
bad_count = bad_count + check_sqf_syntax(filename)


print("------\nChecked {0} files\nErrors detected: {1}".format(len(sqf_list), bad_count))
if (bad_count == 0):
print("SQF validation PASSED")
else:
print("SQF validation FAILED")

return bad_count

if __name__ == "__main__":
sys.exit(main())

0 comments on commit defa199

Please sign in to comment.