Hi, everyone

I've been working on a large application project, and using Kix to prototype the code / logic. I've created a UDF that scans the KiX code and locates things like variable names defined as both Global and Local, undeclared variable names, and variable names declared but never referenced. It also does some basic tests of matched quotes and parens (but not across multiple lines).

I've circulated it offline for a bit, and am still in early coding stages, so will post it here instead of the UDF section for now. I'd like to get some feedback, suggestions, feature requests, etc, before I do a final revision and post it. This release is working fairly well, and has proven useful in validating my declarations and other basic syntax.

Just pass the UDF the name of a kix source file, or a string containing the script source (and set Mode to 1)

Glenn

NOTE: After editing this to post the updated version, the code became truncated. It's available for download on my web site. It will be embedded in a small demo script.


Code:
;; 
;;======================================================================
;;
;;FUNCTION Sanity()
;;
;;ACTION Performs a "sanity check" on KiX scripts
;;
;;AUTHOR Glenn Barnas
;;
;;VERSION 0.95 - PreRelease
;; Works with files or tlobs
;;
;;SYNTAX Sanity( Script [, Mode] )
;;
;;PARAMETERS Script - REQUIRED, script to review *
;; If a string, the name of the file to open
;; If an array, the file data
;;
;; Mode - OPTIONAL, Defines data format, Mode 0 is default
;; MODE=0 - Script is the name of the file to open
;; MODE=1 - Script is a string containing the file data
;;
;;REMARKS Developed as part of KGen, the Kix Script Generator, this
;; UDF performs basic sanity checks and reports the results.
;; * Generate list of Declared VarNames - Global, Main, & per UDF
;; * Identify VarNames declared by both DIM and GLOBAL
;; * Identify VarNames that were referenced but not declared
;; * Identify VarNamed Declared but not referenced
;; * Identify suspected use of Vars or Macros in strings
;; * Identify suspected mismatched single & double quotes
;; * Identify suspected mismatched parens
;;
;; Quote and Paren matching DOES NOT cross line boundaries!
;; This is intentional at this time, since KiX does not employ
;; a line-continuation char, no assumption of when a closing char
;; is found will be made.
;;
;; The goal is not to have "Zero Warnings", but to be aware
;; that the warnings should be reviewed and understood.
;; YOU are the ultimate authority of what you've coded!
;;
;;RETURNS Array of warning messages in CSV format
;; FuncName, Line, VarName, Warning_ID, Extra_Info, Message
;; Warning IDs:
;; 1 - Recursive Function Definition
;; 2 - Variable declared as local & global (Extra=Global Def Line)
;; 3 - Undeclared variable
;; 4 - Variable inside string
;; 5 - Unterminated single quote
;; 6 - Unterminated double quote
;; 7 - Mismatched parenthesis
;; 8 - Variable declared but not referenced
;;
;;
;;DEPENDENCIES none
;;
;;TESTED WITH W2K, WXP, W2K3
;;
;;EXAMPLES Sanity('MyScript_or_UDF', 0)
;
Function Sanity($_File, OPTIONAL $_Mode)


Dim $_, $_Line, $_Ctr, $_SQF, $_DQF, $_VF, $_FF, $_PF, $_DT, $_Fn, $_Vt
Dim $_C, $_P, $_E, $_G, $_L, $_ECt, $_SectList, $_KeyList, $_Var, $_Vars


; ==========================
Dim $_MaxLines
$_MaxLines = 10000 ; set maximum input file buffer size, in lines
Dim $_FData[$_MaxLines]
; ==========================


Dim $_Warning[1000] ; Warning array


$_Ctr = 0 ; Line Counter - 0-based, post-incremented
$_SQF = 0 ; Single Quote Flag: 0=inactive; 1=active; -1=active,embedded in other string
$_DQF = 0 ; Double Quote Flag: 0=inactive; 1=active; -1=active,embedded in other string
$_VF = 0 ; Variable Flag: true while assembling a var name
$_FF = 0 ; Function Flag: true while a UDF is active
$_PF = 0 ; Paren Flag: count of parens; "(" increments, ")" decrements
$_ECt = 0 ; count of errors/warnings
$_Fn = 'Main' ; name of active function
$_DT = 0 ; Declaration type


; $_Vt defines characters not allowed in a variable name (denoting the end of a var name!)
$_Vt = "-+*/\,.?&=@@()[]<>"+Chr(9)+Chr(32)+Chr(34)+Chr(36)+Chr(39)


; Variable Info:
; $_ Temp var, holds anything, lifespan is no more than 4 lines!
; $_G, $_L Hold results of varname queries for Global or Local vars.
; $_C Current character when parsing line.
; $_P, $_E Current and ending char position when parsing line.
; $_Var Name of active variable .
; $_Vars List of variables found.
; $_Line Currently active source line - may be trimmed or otherwise modified.
; $_FData[] Array of original source lines, 10,000 line max as currently defined
; This array will exactly represent the original file data, except that
; Multi-line declarations will be combined onto a single line, and
; leading / trailing spaces are trimmed.
; $_SectList List of sections of the INI file that are being enumerated.
; $_KeyList List of keys in one section of the INI file that are being enumerated.


; Start with a fresh index file
Del '.\VarInfo.ini'


;=============================================================================================
; Open & load the source file into an array
;=============================================================================================
If $_Mode = 0
If Open(5, $_File, 2) = 0
While Not @ERROR And $_Ctr < $_MaxLines
$_FData[$_Ctr] = Trim(ReadLine(5)) ; read the line, trimming spaces
$_Ctr = $_Ctr + 1
Loop
ReDim Preserve $_FData[$_Ctr - 1]
$_ = Close(5)
EndIf
Else
$_FData = Split($_FILE, @CRLF)
EndIf




;=============================================================================================
; PASS 1
; Parse the file data, looking for all function and variable declaration statements.
; Variable declaration statements can span multiple lines (which are consolidated in
; the array for further processing). This pass only prepares the raw data for further
; processing. Note that when a reference is made to a consolidated line, it is made
; to the first line of reference (Declaration Line), and not the actual line in the
; source file.
;=============================================================================================


$_Ctr = 0 ; init array/line pointer


While $_Ctr <= UBound($_FData)
$_Line = $_FData[$_Ctr]
$_Ctr = $_Ctr + 1 ; data is 0 indexed, references are 1 indexed


$_DT = 0 ; Start with _DT undefined
$_E = 0


If $_Line <> '' ; skip blank lines


; Check for "dim $" and "global $" inside other code, and convert line to place the declaration
; on the left edge of the line so the processing that follows will find it properly.
$_L = InStr($_Line, ' Dim ' + Chr(36))
$_G = InStr($_Line, ' Global ' + Chr(36))
If $_L > 1 Or $_G > 1 ; only 1 will be found
$_SQF = 0 $_DQF = 0
; read from beginning of line to declaration, setting quote flags and checking for comments
; Ignore the declaration if it exists within quotes, or in a comment
For $_P = 1 to $_L + $_G ; combine
$_C = SubStr($_Line, $_P, 1)
Select
; set the quote flags: 0=not active, 1=active alone, -1=active inside other quote
Case $_C = Chr(34) And Not $_DQF ; opening double quote
If $_SQF $_DQF = -1 Else $_DQF = 1 EndIF
Case $_C = Chr(34) And $_DQF ; closing double quote
$_DQF = 0
If $_SQF = -1 $_SQF = 0 EndIf


Case $_C = Chr(39) And Not $_SQF ; opening single quote
If $_DQF <> 0 $_SQF = -1 Else $_SQF = 1 EndIF
Case $_C = Chr(39) And $_SQF ; closing single quote
$_SQF = 0
If $_DQF = -1 $_DQF = 0 EndIf


Case $_C = ';' And $_SQF = 0 And $_DQF = 0 ; comment - ignore rest of line
$_E = 1
EndSelect
Next


; If one quote (any type) or a comment tag exist before the declaration, it isn't valid
; process the line only if none of the flags are active
If $_DQF = 0 And $_SQF = 0 And $_E = 0 ; Declaration is valid
$_ = Split(SubStr($_Line, $_L + $_G + 1), ' ') ; break into words, first is "Global"
$_Line = $_[0] + ' '
For $_P = 1 to UBound($_)
If Left($_[$_P], 1) = Chr(36) ; If first char is $, add to line
$_Line = $_Line + $_[$_P]
EndIF
Next
EndIF ; active flag


EndIf ; Dim or Global in code


; Line is ready for evaluation - find function, dim, and global declarations


Select


; ===============================================================================
; Check for function Start
Case InStr($_Line, 'Function ') = 1
$_Fn = Trim(Split(SubStr($_Line, 10), '(')[0])
; warn if defining a function without terminating the prior definition
If $_FF
$_ECt = $_ECt + 1
$_Warning[$_Ect] = $_Fn + ',' + $_Ctr + ',' + $_Var + ',' + 1 + ',' + '' + ',' + 'Recursive function definition, Prior function definition may not be properly terminated.', '', $_Fn, $_Ctr
EndIF


; define the reference to the function, and turn on the active Function Flag
$_FF = 1
$_ = WriteProfileString('.\VarInfo.ini', $_Fn, 'DefinedOnLine', $_Ctr)


; include the Fn name as a declared var
$_ = WriteProfileString('.\VarInfo.ini', $_Fn, cHR(36) + $_Fn, '1,' + CStr($_Ctr))


; mark any passed arguments as defined variables - find text between parens & remove spaces
$_Vars = Join(Split(Split(Split($_Line, Chr(40))[1], Chr(41))[0], ' '), '')
If $_Vars <> ''
For Each $_Var in Split($_Vars, ',')
If Left($_Var, 9) = 'optional' + Chr(36) ; check for "Optional$VarName" & trim it
$_Var = SubStr($_Var, 9)
EndIf
If Trim($_Var) <> 'Optional' ; exclude "optional" tags
$_ = WriteProfileString('.\VarInfo.ini', $_Fn, Trim($_Var), '1,' + CStr($_Ctr))
EndIf
Next
EndIF
; Function ?


; Check for Function End
Case InStr($_Line, 'EndFunction') = 1
$_FF = 0
$_Fn = 'Main'
; EndFunction ?



; ===============================================================================
; Check for Variable Declaration statements - can be one of:
;
; Declare Varname, varname... (type 1/2)
;
; Declare Varname, varname... , (type 3/4)
; more_var_names..., last_var_name
;
; Declare (type 5/6)
; Varname, varname... ,
; more_var_names..., last_var_name
;
; "Declare" can be "Global" or "Dim"


Case InStr($_Line, 'Global') = 1 ; found a GLOBAL declaration
$_Line = Trim(Split($_Line, ';')[0]) ; remove comment portion
Select
Case $_Line = 'Global' ; type 5
$_DT = 5
$_Line = $_Line + ' '
Case Right($_Line, 1) = ',' ; type 3
$_DT = 3
Case 1 ; type 1
$_DT = 1
EndSelect


Case InStr($_Line, 'Dim') = 1
$_Line = Trim(Split($_Line, ';')[0]) ; remove comment portion
Select
Case $_Line = 'Dim' ; type 6
$_DT = 6
$_Line = $_Line + ' '
Case Right($_Line, 1) = ',' ; type 4
$_DT = 4
Case 1 ; type 2
$_DT = 2
EndSelect


EndSelect




; If $_DT > 2, source lines are split.
; collect & combine the following lines until none end in ',' before continuing
; The data in the array source is combined - this is the
If $_DT > 2
$_ = $_Ctr - 1 ; remember where the declaration started
Do
$_Line = $_Line + $_FData[$_Ctr]
$_FData[$_Ctr] = '' ; clear the extended source line
$_Ctr = $_Ctr + 1
Until Right($_Line, 1) <> ','
$_FData[$_] = $_Line ; write the combined source line
EndIf


; Lines are combined, process either basic DIM or GLOBAL statements
If $_DT = 2 Or $_DT = 4 Or $_DT = 6 ; DIM
; $_Line = Split($_Line, ';')[0] ; remove comment portion
$_E = Len($_Line)
For $_P = 5 to $_E
$_C = SubStr($_Line, $_P, 1)


Select
Case $_C = Chr(36) And $_VF = 0 ; found "$"
$_Var = $_C
If $_P = $_E ; trap for "Dim $" by itself
$_VF = 0
$_ = WriteProfileString('.\VarInfo.ini', $_Fn, Trim($_Var), '1,' + CStr($_Ctr))
Else
$_VF = 1
EndIf


Case $_VF = 1 And (InStr($_Vt, $_C) Or $_P = $_E) ; terminating char
$_VF = 0
If $_P = $_E And Not InStr($_Vt, $_C) ; EOL - add last char if needed
$_Var = $_Var + $_C
EndIf
$_ = WriteProfileString('.\VarInfo.ini', $_Fn, Trim($_Var), '1,' + CStr($_Ctr))


If $_C = '[' $_VF = -1 EndIf ; handle arrays


Case $_VF = -1 ; skip chars betweet [ and ]
If $_C = ']' $_VF = 0 EndIf


Case $_VF = 1 And Not InStr($_Vt, $_C) ; build var name
$_Var = $_Var + $_C

EndSelect
Next
EndIf


If $_DT = 1 Or $_DT = 3 Or $_DT = 5 ; GLOBAL
; $_Line = Split($_Line, ';')[0] ; remove comment portion
$_E = Len($_Line)
For $_P = 8 to $_E
$_C = SubStr($_Line, $_P, 1)


Select
Case $_C = Chr(36) And $_VF = 0 ; found "$"
$_Var = $_C
If $_P = $_E ; trap for "Global $" by itself (rare)
$_VF = 0
$_ = WriteProfileString('.\VarInfo.ini', 'Global', Trim($_Var), '1,' + CStr($_Ctr))
Else
$_VF = 1
EndIf


Case $_VF = 1 And (InStr($_Vt, $_C) Or $_P = $_E) ; terminating char
$_VF = 0
If $_P = $_E And Not InStr($_Vt, $_C) ; EOL - add last char if needed
$_Var = $_Var + $_C
EndIf
$_ = WriteProfileString('.\VarInfo.ini', 'Global', Trim($_Var), '1,' + CStr($_Ctr))
If $_C = '[' ; handle arrays
$_VF = -1
EndIf


Case $_VF = -1 ; skip chars betweet [ and ]
If $_C = ']'
$_VF = 0
EndIF


Case $_VF = 1 And Not InStr($_Vt, $_C) ; build var name
$_Var = $_Var + $_C

EndSelect
Next
EndIf




EndIf ; $_Line <> ''


Loop






;=============================================================================================
; PASS 2
; Enumerate the INI file and check for vars that have been declared both as local and global
;=============================================================================================


$_SectList = ReadProfileString('.\VarInfo.ini', '', '') ; enum sections
$_SectList = Split(Left($_SectList,len($_SectList)-1), Chr(10))
For Each $_Fn in $_SectList
If $_Fn <> 'Global' ; ignore the global sect
$_KeyList = ReadProfileString('.\VarInfo.ini', $_Fn, '')
$_KeyList = Split(Left($_KeyList ,len($_KeyList)-1), Chr(10)) ; enum keys in section
For Each $_Var in $_KeyList
$_ = ReadProfileString('.\VarInfo.ini', 'Global', $_Var) ; is it also a Global?
If $_ <> ''
$_P = Split(ReadProfileString('.\VarInfo.ini', $_Fn, $_Var), ',')[1]
$_ = Split(ReadProfileString('.\VarInfo.ini', 'Global', $_Var), ',')[1]
$_ECt = $_ECt + 1
$_Warning[$_Ect] = $_Fn + ',' + $_P + ',' + $_Var + ',' + 2 + ',' + $_ + ',' + 'Global variable declared as Local.'
EndIf
Next
EndIf
Next






;=============================================================================================
; PASS 3
; Scan the data and identify the variables that are used, verify that they are declared, and
; check for variable and macro usage inside of strings
;=============================================================================================


; reset the line counter & flags for the third pass
$_Ctr = 0
$_Fn = 'Main'
$_VF = 0 ; Variable Flag
$_FF = 0 ; Function Flag






While $_Ctr <= UBound($_FData)
$_Line = $_FData[$_Ctr] ; get working line
$_Ctr = $_Ctr + 1 ; increment counter


If $_Line <> ''


$_SQF = 0 ; init per-line flags
$_DQF = 0
$_PF = 0


; ===============================================================================
; filter out lines we don't want to process char by char


Select
; Check for function Start / End to get name to reference
Case InStr($_Line, 'Function') = 1
$_Fn = Trim(Split(SubStr($_Line, 10), Chr(40))[0]) ; set active function name


Case InStr($_Line, 'EndFunction') = 1
$_Fn = 'Main' ; default active function to "main"


Case InStr($_Line, 'IsDeclared') ; do nothing - was processed earlier

Case 1 ; valid line to parse


; ===============================================================================
; parse the line, char by char, looking for varnames, quotes, parens, and comments


$_VF = 0 ; clear the var active flag


$_E = Len($_Line) ; line end pointer
For $_P = 1 to $_E ; char pointer
$_C = SubStr($_Line, $_P, 1) ; get the active char


; ===============================================================================
; set the quote flags: 0=not active, 1=active alone, -1=active inside other quote
If $_C = Chr(34) Or $_C = Chr(39)
Select
Case $_C = Chr(34) And $_DQF = 0 ; opening double quote
$_DQF = IIf($_S