Sorcerer's IsleCode QueryParam Scanner / diff

865282a Assorted cleanup and minor performance improvements.

 Application.cfc    |   3 +-
 cfcs/jre-utils.cfc | 227 +++++++++++++++++---
 cfcs/qpscanner.cfc |  70 +++---
 3 files changed, 224 insertions(+), 76 deletions(-)
diff --git a/Application.cfc b/Application.cfc
index 857917f..3e8755a 100644
--- a/Application.cfc
+++ b/Application.cfc (view file)
@@ -23,8 +23,7 @@
 	<cffunction name="onRequestStart" returntype="Boolean" output="false">
 		<cfset var Result = True/>
 
-		<!--- TODO: FIX: Implement as URL check once CFCs are stable. --->
-		<cfif True>
+		<cfif StructKeyExists(Url,'AppReload')>
 			<cfset Result = Result AND onApplicationStart()/>
 		</cfif>
 
diff --git a/cfcs/jre-utils.cfc b/cfcs/jre-utils.cfc
index c2417c5..d651431 100644
--- a/cfcs/jre-utils.cfc
+++ b/cfcs/jre-utils.cfc (view file)
@@ -1,12 +1,13 @@
-<cfcomponent output="false" displayname="jre-utils v0.6">
+<cfcomponent output="false" displayname="jrex v0.8ish">
 
 
 	<cffunction name="init" output="false" access="public">
 		<cfargument name="DefaultFlags"        type="String"  default="MULTILINE"/>
-		<cfargument name="IgnoreInvalidFlags"  type="Boolean" default="false"/>
-		<cfargument name="BackslashReferences" type="Boolean" default="false"/>
+		<cfargument name="IgnoreInvalidFlags"  type="Boolean" default="false" />
+		<cfargument name="BackslashReferences" type="Boolean" default="false" />
+		<cfargument name="SetNullGroupsBlank"  type="Boolean" default="true"  />
 
-		<cfset var CurrentFlag = ""/>
+		<cfset var CurProp = 0 />
 
 		<cfset This.Flags = 
 			{ UNIX_LINES       = 1
@@ -18,9 +19,16 @@
 			, CANON_EQ         = 128
 			}/>
 
+
 		<cfset This.DefaultFlags = This.parseFlags( Arguments.DefaultFlags , Arguments.IgnoreInvalidFlags )/>
 
-		<cfset This.BackslashReferences = Arguments.BackslashReferences/>
+		<cfloop index="CurProp" list="BackslashReferences,SetNullGroupsBlank">
+			<cfset This[CurProp] = Arguments[CurProp] />
+		</cfloop>
+
+
+		<cfset Variables.PatternCache = {} />
+
 
 		<!--- In CFMX we cannot define a "Replace" function directly. --->
 		<cfset This.replace   = _replace/>
@@ -31,8 +39,25 @@
 
 
 
-	<cffunction name="Struct" returntype="Struct" access="private"><cfreturn Arguments/></cffunction>
+	<cffunction name="onMissingMethod" returntype="any" output="false" access="public">
+		<cfargument name="MissingMethodName"      type="String" />
+		<cfargument name="MissingMethodArguments" type="Struct" />
 
+		<cfif right(Arguments.MissingMethodName,6) EQ 'NOCASE'>
+			<cfset var TargetFunction = left(Arguments.MissingMethodName,len(Arguments.MissingMethodName)-6) />
+			<cfset var Args = Arguments.MissingMethodArguments />
+
+			<cfif StructKeyExists(Args,'Flags')>
+				<cfset Args.Flags = BitOr( Args.Flags , This.Flags.CASE_INSENSITIVE ) />
+			<cfelse>
+				<cfset Args.Flags = BitOr( This.DefaultFlags , This.Flags.CASE_INSENSITIVE ) />
+			</cfif>
+
+			<cfset var Function = This[TargetFunction] />
+			<cfreturn Function(ArgumentCollection=Args) />
+		</cfif>
+
+	</cffunction>
 
 
 
@@ -61,37 +86,126 @@
 	</cffunction>
 
 
+	<cffunction name="compilePattern" output="false" access="public">
+		<cfargument name="Regex" type="String" />
+		<cfargument name="Flags" type="String" />
 
-	<cffunction name="matches" returntype="Boolean" output="false" access="public">
+		<cfset var Key = Hash(Arguments.Regex&Arguments.Flags) />
+
+		<cfif NOT StructKeyExists(Variables.PatternCache,Key)>
+			<cfset Variables.PatternCache[Key] = createObject("java","java.util.regex.Pattern")
+				.compile( Arguments.Regex , parseFlags(Arguments.Flags) ) />
+		</cfif>
+
+		<cfreturn Variables.PatternCache[Key]/>
+	</cffunction>
+
+
+
+	<cffunction name="flushPatternCache" returntype="void" outout="false" access="public"
+		hint="Clears cache for specified pattern, or all patterns if none specified.">
+		<cfargument name="Regex" type="String" required="false" />
+		<cfargument name="Flags" type="String" required="false" />
+
+		<cfif StructKeyExists(Arguments,'Regex')>
+			<cfparam name="Arguments.Flags" default="#This.DefaultFlags#"/>
+
+			<cfset StructDelete( Variables.PatternCache , Hash(serialize(Arguments)) ) />
+
+		<cfelseif StructKeyExists(Arguments,'Flags')>
+			<cfthrow
+				message = "Argument 'flags' can only be used in conjunction with argument 'regex'."
+				type    = "JreUtils.FlushPatternCache.InvalidArgument.Flags"
+			/>
+
+		<cfelse>
+			<cfset Variables.PatternCache = {} />
+
+		</cfif>
+
+	</cffunction>
+
+
+
+
+	<cffunction name="get" returntype="Array" output="false" access="public">
 		<cfargument name="Text"    type="String"/>
 		<cfargument name="Regex"   type="String"/>
 		<cfargument name="Flags"   default="#This.DefaultFlags#"/>
 
-		<cfset var Pattern = createObject("java","java.util.regex.Pattern")
-			.compile( Arguments.Regex , parseFlags(Arguments.Flags) )/>
+		<cfset var Pattern = compilePattern( Arguments.Regex , Arguments.Flags )/>
 		<cfset var Matcher = Pattern.Matcher(Arguments.Text)/>
+		<cfset var Matches = ArrayNew(1)/>
 
 		<cfloop condition="Matcher.find()">
-			<cfreturn True/>
+			<cfset ArrayAppend(Matches,Matcher.Group())/>
 		</cfloop>
 
-		<cfreturn False/>
+		<cfreturn Matches/>
 	</cffunction>
 
 
 
-	<cffunction name="get" returntype="Array" output="false" access="public">
+	<cffunction name="getFirst" returntype="String" output="false" access="public">
+		<cfargument name="Text"    type="String"/>
+		<cfargument name="Regex"   type="String"/>
+		<cfargument name="Flags"   default="#This.DefaultFlags#"/>
+
+		<cfset var Pattern = compilePattern( Arguments.Regex , Arguments.Flags )/>
+		<cfset var Matcher = Pattern.Matcher(Arguments.Text)/>
+
+		<cfif Matcher.find()>
+			<cfreturn Matcher.Group() />
+		<cfelse>
+			<cfreturn '' />
+		</cfif>
+	</cffunction>
+
+
+
+	<cffunction name="getCount" returntype="Numeric" output="false" access="public">
+		<cfargument name="Text"    type="String"/>
+		<cfargument name="Regex"   type="String"/>
+		<cfargument name="Flags"   default="#This.DefaultFlags#"/>
+
+		<cfset var Pattern = compilePattern( Arguments.Regex , Arguments.Flags )/>
+		<cfset var Matcher = Pattern.Matcher(Arguments.Text)/>
+		<cfset var Count = 0 />
+
+		<cfloop condition="Matcher.find()">
+			<cfset Count++ />
+		</cfloop>
+
+		<cfreturn Count />
+	</cffunction>
+
+
+
+	<cffunction name="getGroups" returntype="Array" output="false" access="public">
 		<cfargument name="Text"    type="String"/>
 		<cfargument name="Regex"   type="String"/>
+		<cfargument name="SetNullGroupsBlank" type="Boolean" default="#This.SetNullGroupsBlank#"/>
 		<cfargument name="Flags"   default="#This.DefaultFlags#"/>
 
-		<cfset var Pattern = CreateObject("java","java.util.regex.Pattern")
-			.compile( Arguments.Regex , parseFlags(Arguments.Flags) )/>
+		<cfset var Pattern = compilePattern( Arguments.Regex , Arguments.Flags )/>
 		<cfset var Matcher = Pattern.Matcher(Arguments.Text)/>
 		<cfset var Matches = ArrayNew(1)/>
 
+
 		<cfloop condition="Matcher.find()">
-			<cfset ArrayAppend(Matches,Matcher.Group())/>
+			<cfset CurMatch = 
+				{ match = Matcher.Group()
+				, groups = ArrayNew(1)
+				}/>
+			<cfloop index="i" from="1" to="#Matcher.groupCount()#">
+				<cfif (Matcher.start(i) EQ -1) AND Arguments.SetNullGroupsBlank >
+					<cfset ArrayAppend(CurMatch.Groups,'')/>
+				<cfelse>
+					<cfset ArrayAppend(CurMatch.Groups,Matcher.group(i))/>
+				</cfif>
+			</cfloop>
+
+			<cfset ArrayAppend(Matches,CurMatch)/>
 		</cfloop>
 
 		<cfreturn Matches/>
@@ -99,19 +213,69 @@
 
 
 
-	<cffunction name="getNoCase" returntype="Array" output="false" access="public">
+
+	<!--- \ match* - clones of get* with first two arguments swapped. --->
+
+	<cffunction name="match" returntype="Array" output="false" access="public"
+		hint="This function swaps argument order for consistency with rematch">
+		<cfargument name="Regex"   type="String"/>
+		<cfargument name="Text"    type="String"/>
+		<cfargument name="Flags"   default="#This.DefaultFlags#"/>
+
+		<cfreturn This.get( ArgumentCollection = Arguments )/>
+	</cffunction>
+
+
+
+	<cffunction name="matchFirst" returntype="String" output="false" access="public">
+		<cfargument name="Regex"   type="String"/>
+		<cfargument name="Text"    type="String"/>
+		<cfargument name="Flags"   default="#This.DefaultFlags#"/>
+
+		<cfreturn This.getFirst( ArgumentCollection = Arguments )/>
+	</cffunction>
+
+
+
+	<cffunction name="matchCount" returntype="String" output="false" access="public">
+		<cfargument name="Regex"   type="String"/>
 		<cfargument name="Text"    type="String"/>
+		<cfargument name="Flags"   default="#This.DefaultFlags#"/>
+
+		<cfreturn This.getCount( ArgumentCollection = Arguments )/>
+	</cffunction>
+
+
+
+	<cffunction name="matchGroups" returntype="Array" output="false" access="public">
 		<cfargument name="Regex"   type="String"/>
+		<cfargument name="Text"    type="String"/>
+		<cfargument name="SetNullGroupsBlank" type="Boolean" default="#This.SetNullGroupsBlank#"/>
 		<cfargument name="Flags"   default="#This.DefaultFlags#"/>
 
-		<cfreturn This.get
-			( Text  : Arguments.Text
-			, Regex : Arguments.Regex
-			, Flags : BitOr( Arguments.Flags , This.Flags.CASE_INSENSITIVE )
-			)/>
+		<cfreturn This.getGroups( ArgumentCollection = Arguments ) />
 	</cffunction>
 
 
+	<!--- / match* --->
+
+
+
+	<cffunction name="matches" returntype="Boolean" output="false" access="public">
+		<cfargument name="Text"    type="String"/>
+		<cfargument name="Regex"   type="String"/>
+		<cfargument name="Flags"   default="#This.DefaultFlags#"/>
+
+		<cfset var Pattern = compilePattern( Arguments.Regex , Arguments.Flags )/>
+		<cfset var Matcher = Pattern.Matcher(Arguments.Text)/>
+
+		<cfloop condition="Matcher.find()">
+			<cfreturn true/>
+		</cfloop>
+
+		<cfreturn false/>
+	</cffunction>
+
 
 
 	<cffunction name="_replace" returntype="String" output="false" access="public">
@@ -119,6 +283,7 @@
 		<cfargument name="Regex"       type="String"/>
 		<cfargument name="Replacement" type="Any"    hint="String or UDF"/>
 		<cfargument name="Scope"       type="String" default="ONE" hint="ONE,ALL"/>
+		<cfargument name="Flags"       type="String" default="#This.DefaultFlags#"/>
 
 		<cfset var String     = ""/>
 		<cfset var Pattern    = ""/>
@@ -144,7 +309,7 @@
 
 		<cfelse>
 
-			<cfset Pattern = createObject("java","java.util.regex.Pattern").compile(Arguments.Regex)/>
+			<cfset Pattern = compilePattern( Arguments.Regex , Arguments.Flags )/>
 			<cfset Matcher = Pattern.Matcher( Arguments.Text )/>
 			<cfset Results = createObject("java","java.lang.StringBuffer").init()/>
 
@@ -173,20 +338,16 @@
 
 
 
-	<cffunction name="escape" returntype="String" output="false" access="public">
-		<cfargument name="Text" type="String"/>
-		<cfset var Result = Arguments.Text/>
-		<cfset var Symbol = ""/>
-		<cfset var EscapeChars = "\,.,[,],(,),^,$,|,?,*,+,{,}"/>
-
-		<cfloop index="Symbol" list="#EscapeChars#">
-			<cfset Result = replace( Result , Symbol , '\'&Symbol , 'all')/>
-		</cfloop>
+	<cffunction name="split" returntype="Array" output="false" access="public">
+		<cfargument name="Text"    type="String"/>
+		<cfargument name="Regex"   type="String"/>
+		<cfargument name="Flags"   default="#This.DefaultFlags#"/>
 
-		<cfreturn Result />
+		<cfreturn compilePattern( Arguments.Regex , Arguments.Flags )
+			.split(Arguments.Text)
+			/>
 	</cffunction>
 
 
 
-
 </cfcomponent>
\ No newline at end of file
diff --git a/cfcs/qpscanner.cfc b/cfcs/qpscanner.cfc
index 8e8e6c9..d5fc334 100644
--- a/cfcs/qpscanner.cfc
+++ b/cfcs/qpscanner.cfc (view file)
@@ -44,17 +44,15 @@
 		<cfset Variables.AlertData = QueryNew(Variables.ResultFields)/>
 
 		<cfsavecontent variable="RegexList"><cfoutput>
-			findQueries      |(?si)(<#cf#query[^p]).*?(?=</#cf#query>)
-			findQueryTag     |(?si)(<#cf#query(?!p)[^>]{0,300}>)
+			findQueries      |(?si)(<#cf#query\b)(?:[^<]++|<(?!/#cf#query>))+(?=</#cf#query>)
+			findQueryTag     |(?si)(<#cf#query[^p][^>]++>)
 			isQueryOfQuery   |(?si)dbtype\s*=\s*["']query["']
-			killParams       |(?si)<#cf#queryparam[^>]+>
-			killCfTag        |(?si)<#cf#[a-z]{2,}[^>]*> <!--- Deliberately excludes Custom Tags and CFX --->
+			killParams       |(?si)<#cf#queryparam[^>]++>
+			killCfTag        |(?si)<#cf#[a-z]{2,}[^>]*+> <!--- Deliberately excludes Custom Tags and CFX --->
 			killOrderBy      |(?si)\bORDER BY\b.*?$
 			killBuiltIn      |(?si)##(#ListChangeDelims(This.BuiltInFunctions,'|')#)\([^)]*\)##
 			findScopes       |(?si)(?<=##([a-z]{1,20}\()?)[^\(##<]+?(?=\.[^##<]+?##)
-			findName         |(?si)(?<=(<#cf#query[^>]{0,300}\bname=")).*?(?="[^>]{0,300}>)
 			findClientScopes |(?i)\b(#ListChangeDelims(This.ClientScopes,'|')#)\b
-			isCfmlFile       |(?i)\.cf(c|ml?)$
 		</cfoutput></cfsavecontent>
 
 		<cfloop index="Rex" list="#RegexList#" delimiters="#Chr(10)#">
@@ -108,6 +106,7 @@
 		<cfset var CurrentTarget = -1/>
 		<cfset var process = true/>
 		<cfset var jre = Variables.jre/>
+		<cfset var Ext = 0 />
 
 		<cfif DirectoryExists(Arguments.DirName)>
 
@@ -136,13 +135,19 @@
 
 						<cfset scan( CurrentTarget )/>
 
-					<cfelseif jre.matches( CurrentTarget , Variables.Regexes.isCfmlFile )>
-						<cfset This.Totals.FileCount = This.Totals.FileCount + 1 />
-
-						<cfset qryCurData = hunt( CurrentTarget )/>
+					<cfelse>
+						<cfset Ext = LCase(ListLast(CurrentTarget,'.')) >
+
+						<cfif Ext EQ 'cfc' OR Ext EQ 'cfm' OR Ext EQ 'cfml'>
+	
+							<cfset This.Totals.FileCount = This.Totals.FileCount + 1 />
+	
+							<cfset qryCurData = hunt( CurrentTarget )/>
+	
+							<cfif qryCurData.RecordCount>
+								<cfset Variables.AlertData = QueryAppend( Variables.AlertData , qryCurData )/>
+							</cfif>
 
-						<cfif qryCurData.RecordCount>
-							<cfset Variables.AlertData = QueryAppend( Variables.AlertData , qryCurData )/>
 						</cfif>
 
 					</cfif>
@@ -219,33 +224,37 @@
 				<cfset qryResult.QueryCode[CurRow] = jre.replace( QueryCode , Chr(13) , Chr(10) , 'all' ) />
 				<cfset qryResult.QueryCode[CurRow] = jre.replace( qryResult.QueryCode[CurRow] , Chr(10)&Chr(10) , Chr(10) , 'all' ) />
 				<cfif This.showScopeInfo >
-					<cfset qryResult.ScopeList[CurRow] = ArrayToList( ArrayUnique( jre.get( rekCode , REX.findScopes ) ) ) />
+					<cfset qryResult.ScopeList[CurRow] = [] />
+					<cfloop index="CurScope" array="#jre.get( rekCode , REX.findScopes )#">
+						<cfif NOT ArrayFind(qryResult.ScopeList[CurRow],CurScope)>
+							<cfset ArrayAppend(qryResult.ScopeList[CurRow],CurScope)>
+						</cfif>
+					</cfloop>
 
 					<cfset qryResult.ContainsClientScope[CurRow] = false/>
 					<cfif This.highlightClientScopes>
 						<cfloop index="CurrentScope" list="#This.ClientScopes#">
-							<cfif ListFind( qryResult.ScopeList[CurRow] , CurrentScope )>
+							<cfif ArrayFind( qryResult.ScopeList[CurRow] , CurrentScope )>
 								<cfset qryResult.ContainsClientScope[CurRow] = true/>
 								<cfbreak/>
 							</cfif>
 						</cfloop>
 					</cfif>
+					
+					<cfset qryResult.ScopeList[CurRow] = ArrayToList(qryResult.ScopeList[CurRow]) />
 				</cfif>
 
-				<!--- CF8 doesn't support get()[1] so need to use two lines: --->
-				<cfset QueryTagCode = jre.get( Matches[i] , REX.findQueryTag )/>
-				<cfset QueryTagCode = QueryTagCode[1] />
+				<cfset QueryTagCode = jre.getFirst( Matches[i] , REX.findQueryTag )/>
 
 				<cfset BeforeQueryCode = ListFirst ( replace ( ' '&FileData&' ' , Matches[i] , UniqueToken ) , UniqueToken )/>
 
-
 				<cfset StartLine = 1+ArrayLen( jre.get( BeforeQueryCode , chr(10) ) )/>
 				<cfset LineCount = ArrayLen( jre.get( Matches[i] , chr(10) ) )/>
 
 
 				<cfset qryResult.QueryStartLine[CurRow] = StartLine/>
 				<cfset qryResult.QueryEndLine[CurRow]   = StartLine + LineCount />
-				<cfset qryResult.QueryName[CurRow]      = ArrayToList( jre.get( ListLast(QueryTagCode,chr(10)) , REX.findName ) )/>
+				<cfset qryResult.QueryName[CurRow]      = jre.getFirst(QueryTagCode,'(?<=\bname\s{0,10}=\s{0,10}(["'']))\S(?=\1)') />
 				<cfset qryResult.QueryId[CurRow]        = createUuid() />
 				<cfif NOT Len( qryResult.QueryName[CurRow] )>
 					<cfset qryResult.QueryName[CurRow] = "[unknown]"/>
@@ -268,32 +277,11 @@
 	</cffunction>
 
 
-
-
-
-
-
-
-	<cffunction name="ArrayUnique" returntype="Array" output="false" access="private">
-		<cfargument name="ArrayVar" type="Array"/>
-		<cfset var UniqueToken = Chr(65536)/>
-		<cfset var Result = duplicate(Arguments.ArrayVar)/>
-		<cfset ArraySort(Result,'text')/>
-		<cfset Result = ArrayToList( Result , UniqueToken )/>
-		<!--- TODO: MINOR: FIX: Using \b works for the ScopeList, but is not good enough for general use - why not using UniqueToken? --->
-		<cfset Result = REreplace( Result & UniqueToken , '(\b(.*?)\b)\1+' , '\1' , 'all' )/>
-		<cfset Result = ListToArray( Result , UniqueToken )/>
-		<!--- TODO: MINOR: Ideally, the original array order should be restored. --->
-		<cfreturn Result/>
-	</cffunction>
-
-
-
 	<cffunction name="QueryAppend" returntype="Query" output="false" access="private">
 		<cfargument name="QueryOne" type="Query"/>
 		<cfargument name="QueryTwo" type="Query"/>
 		<cfset var Result = -1/>
-		<!--- Bug fix for CF8 --->
+		<!--- Bug fix for CF9 --->
 		<cfif NOT Arguments.QueryOne.RecordCount><cfreturn Arguments.QueryTwo /></cfif>
 		<cfif NOT Arguments.QueryTwo.RecordCount><cfreturn Arguments.QueryOne /></cfif>
 		<!--- / --->