using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; namespace RobvanderWoude { internal class CheckVarsPHP { static readonly string progver = "1.02"; static string scriptcode = String.Empty; static int Main( string[] args ) { #region Initialize Variables SortedList functions = new SortedList( ); SortedList variables = new SortedList( ); bool showfuncs = true; bool showvars = true; bool unusedonly = false; Regex regex; string pattern; string scriptext = String.Empty; string scriptfile = String.Empty; int columnwidth = 12; int unusedfuncs = 0; int unusedvars = 0; #endregion Initialize Variables #region Command Line Parsing if ( args.Length == 0 ) { return ShowHelp( ); } foreach ( string arg in args ) { if ( arg[0] == '/' ) { if ( arg.ToUpper( ) == "/?" ) { return ShowHelp( ); } else if ( arg.ToUpper( ) == "/F" ) { if ( !showvars ) { return ShowHelp( "Duplicate command line switch /F" ); } if ( !showfuncs ) { return ShowHelp( "Use /F or /V or neither, but not both" ); } showvars = false; } else if ( arg.ToUpper( ) == "/U" ) { if ( unusedonly ) { return ShowHelp( "Duplicate command line switch /U" ); } unusedonly = true; } else if ( arg.ToUpper( ) == "/V" ) { if ( !showfuncs ) { return ShowHelp( "Duplicate command line switch /V" ); } if ( !showvars ) { return ShowHelp( "Use /F or /V or neither, but not both" ); } showfuncs = false; } else { return ShowHelp( "Invalid command line switch \"{0}\"", arg ); } } else { if ( !String.IsNullOrWhiteSpace( scriptfile ) ) { return ShowHelp( "Duplicate command line argument for PHP file" ); } if ( !File.Exists( arg ) ) { return ShowHelp( "Invalid file name or file not found: \"{0}\"", arg ); } scriptext = Path.GetExtension( arg ).ToLower( ); if ( scriptext != ".php" ) { return ShowHelp( "Invalid file type \"{0}\"", arg ); } scriptfile = Path.GetFullPath( arg ); } } if ( String.IsNullOrWhiteSpace( scriptfile ) ) { return ShowHelp( "Please specify a source file" ); } #endregion Command Line Parsing #region Read File // Read the code from the file scriptcode = File.ReadAllText( scriptfile, Encoding.UTF8 ); // Strip everything NOT PHP if ( scriptcode.Contains( ""; regex = new Regex( pattern ); if ( regex.IsMatch( scriptcode ) ) { foreach ( Match match in regex.Matches( scriptcode ) ) { purephp += match.Value + "\n\n"; } } scriptcode = purephp; } // Remove comment lines from the code (does NOT strip comments starting halfway on a line) pattern = @"(^|\n|\r)[ \t]*//[^\n\r]+"; regex = new Regex( pattern ); scriptcode = regex.Replace( scriptcode, String.Empty ); // Remove comment blocks from the code (does NOT strip comments starting halfway on a line) pattern = @"(^|\n|\r)[ \t]*/\*[\w\W]*\*/"; regex = new Regex( pattern ); scriptcode = regex.Replace( scriptcode, String.Empty ); #endregion Read File #region List Functions // Create a list of subroutines found in the code // function names are NOT case sensitive if ( showfuncs ) { pattern = @"(?:^|\n|\r)[ \t]*(?:function)[ \t]+([A-Z_][^\s\(]+)\("; regex = new Regex( pattern, RegexOptions.IgnoreCase ); if ( regex.IsMatch( scriptcode ) ) { MatchCollection matches = regex.Matches( scriptcode ); if ( matches.Count > 0 ) { foreach ( Match match in matches ) { bool listed = false; string func = match.Groups[1].Value; foreach ( string key in functions.Keys ) { if ( func.ToLower( ) == key.ToLower( ) ) { listed = true; } } if ( !listed ) { functions[func] = 0; columnwidth = Math.Max( func.Length, columnwidth ); } } } } } #endregion List Functions #region Check for Nested Functions if ( showfuncs && functions.Count > 1 ) { pattern = @"function[\t ]+([_A-Z]\w*)[\t ]*\([^\n\r\)]*\)([\w\W]*?)function[\t ]+[_A-Z]"; regex = new Regex( pattern, RegexOptions.IgnoreCase ); if ( regex.IsMatch( scriptcode ) ) { foreach ( Match match in regex.Matches( scriptcode ) ) { int errorcount = 0; string funcname = match.Groups[1].Value; string funccode = match.Groups[2].Value; int curlybraces = 0; foreach ( char chr in funccode.ToCharArray( ) ) { if ( chr == '{' ) { curlybraces++; } else if ( chr == '}' ) { curlybraces--; } if ( curlybraces < 0 ) { errorcount++; } } if ( curlybraces != 0 ) { errorcount++; } if ( errorcount > 0 ) { RedLine( "Nested function or unterminated curly braces detected in function \"{0}\"\n\n", funcname ); } } } } #endregion Check for Nested Functions #region Check for Unterminated Curly Braces if ( !CheckCurlyBraces( ) ) { RedLine( "Unterminated curly braces detected\n\n" ); } #endregion Check for Unterminated Curly Braces #region List Variables // Create a list of variables found in the code and count their occurrence // Variable names ARE case sensitive if ( showvars ) { pattern = @"\$[A-Z_][A-Z0-9_]*(\[([\$'\""]?[A-Z_][A-Z0-9_]*['\""]?)\])?"; regex = new Regex( pattern, RegexOptions.IgnoreCase ); if ( regex.IsMatch( scriptcode ) ) { MatchCollection matches = regex.Matches( scriptcode ); if ( matches.Count > 0 ) { foreach ( Match match in matches ) { string varstring = match.ToString( ); if ( !variables.ContainsKey( varstring ) ) { variables[varstring] = 1; if ( varstring.Length > columnwidth ) { columnwidth = varstring.Length; } } else { variables[varstring] += 1; } } } } } #endregion List Variables #region Count and Display Functions Usage // Iterate through the list of subroutines and count the occurrences of its name if ( showfuncs ) { List keys = new List( functions.Keys ); foreach ( string func in keys ) { pattern = string.Format( @"\b{0}\(", func ); regex = new Regex( pattern ); if ( regex.IsMatch( scriptcode ) ) { functions[func] = regex.Matches( scriptcode ).Count - 1; } } // Show the results if ( unusedonly ) { Console.WriteLine( "{0} Unused Function{1}{2}", unusedfuncs, ( unusedfuncs == 1 ? String.Empty : "s" ), ( unusedfuncs == 0 ? String.Empty : ":" ) ); Console.WriteLine( "{0}==============={1}{2}", new String( '=', unusedfuncs.ToString( ).Length ), ( unusedfuncs == 1 ? String.Empty : "=" ), ( unusedfuncs == 0 ? String.Empty : "=" ) ); } else { Console.WriteLine( "{0,-" + columnwidth + "} Occurrences:", "Function:" ); Console.WriteLine( "{0,-" + columnwidth + "} ============", "=========" ); } foreach ( string key in functions.Keys ) { if ( functions[key] == 0 ) { if ( unusedonly ) { Console.WriteLine( key ); } else { RedLine( string.Format( "{0,-" + columnwidth + "} {1}", key, functions[key] ) ); } unusedfuncs += 1; } else if ( !unusedonly ) { Console.WriteLine( "{0,-" + columnwidth + "} {1}", key, functions[key] ); } } Console.WriteLine( ); } #endregion Count and Display Functions Usage #region Count and Display Variables Usage if ( showvars ) { // Show the results if ( unusedonly ) { Console.WriteLine( "{0} Unused Variable{1}{2}", unusedvars, ( unusedvars == 1 ? String.Empty : "s" ), ( unusedvars == 0 ? String.Empty : ":" ) ); Console.WriteLine( "{0}================{1}{2}", new String( '=', unusedvars.ToString( ).Length ), ( unusedvars == 1 ? String.Empty : "=" ), ( unusedvars == 0 ? String.Empty : "=" ) ); } else { Console.WriteLine( "{0,-" + columnwidth + "} Occurrences:", "Variable:" ); Console.WriteLine( "{0,-" + columnwidth + "} ============", "=========" ); } foreach ( string key in variables.Keys ) { if ( variables[key] == 1 && !IsSuperGlobal( key ) && key[0] != '$' ) { if ( unusedonly ) { Console.WriteLine( key ); } else { RedLine( String.Format( "{0,-" + columnwidth + "} {1}", key, variables[key] ) ); } unusedvars += 1; } else if ( !unusedonly ) { Console.WriteLine( "{0,-" + columnwidth + "} {1}", key, variables[key] ); } } Console.WriteLine( ); } #endregion Count and Display Variables Usage int rc = 0; if ( showfuncs ) { rc += unusedfuncs; } if ( showvars ) { rc += unusedvars; } return rc; } static bool CheckCurlyBraces( ) { if ( scriptcode.Count( o => ( o == '{' ) ) != scriptcode.Count( c => ( c == '}' ) ) ) { return false; } string testtext = Regex.Replace( scriptcode, @"[^\{\}]", "" ); int state = 0; for ( int i = 0; i < testtext.Length; i++ ) { if ( testtext[i] == '{' ) { state++; } else { state--; } if ( state < 0 ) { return false; } } return true; } static bool IsSuperGlobal( string var ) { string pattern = @"^\$(GLOBALS|_(SERVER|GET|POST|FILES|COOKIE|SESSION|REQUEST|ENV))(\[[^\]]+\])?$"; Regex regex = new Regex( pattern ); return regex.IsMatch( var.Trim( ) ); } static void RedLine( string line, params object[] rlargs ) { Console.ForegroundColor = ConsoleColor.Red; if ( rlargs.Length > 0 ) { Console.WriteLine( line, rlargs ); } else { Console.WriteLine( line ); } Console.ResetColor( ); } static int ShowHelp( params string[] errmsg ) { #region Error Message if ( errmsg.Length > 0 ) { List errargs = new List( errmsg ); errargs.RemoveAt( 0 ); Console.Error.WriteLine( ); Console.ForegroundColor = ConsoleColor.Red; Console.Error.Write( "ERROR:\t" ); Console.ForegroundColor = ConsoleColor.White; Console.Error.WriteLine( errmsg[0], errargs.ToArray( ) ); Console.ResetColor( ); } #endregion Error Message #region Help Text /* CheckVarsPHP.exe, Version 1.02 Check PHP code for unused variables and functions Usage: CheckVarsPHP.exe "phpfile" [ /F | /V ] [ /U ] Where: "phpfile" is the PHP file to be examined /F tests Functions only (default: functions as well as variables) /V tests Variables only (default: functions as well as variables) /U list Unused functions and variables only (default: list all functions and variables) Notes: If functions are tested, a (limited) test for nested functions and unterminated curly braces is performed as well. In PHP variables are not declared, so a single occurrence of a variable means it is not used, unless it is a superglobal which may be declared elsewhere. Keep in mind though, that this program does not take into account the variables' scope, so a variable name used once in two functions may in fact be unused but will escape detection by this program. The program's return code equals the number of unused functions and/or variables, or -1 in case of (command line) errors. Written by Rob van der Woude https://www.robvanderwoude.com */ #endregion Help Text #region Display Help Text Console.Error.WriteLine( ); Console.Error.WriteLine( "CheckVarsPHP.exe, Version {0}", progver ); Console.Error.WriteLine( "Check PHP code for unused variables and functions" ); Console.Error.WriteLine( ); Console.Error.Write( "Usage: " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.WriteLine( "CheckVarsPHP.exe \"phpfile\" [ /F | /V ] [ /U ]" ); Console.ResetColor( ); Console.Error.WriteLine( ); Console.Error.Write( "Where: " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( "\"phpfile\"" ); Console.ResetColor( ); Console.Error.WriteLine( " is the PHP file to be examined" ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( " /F" ); Console.ResetColor( ); Console.Error.Write( " tests " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( "F" ); Console.ResetColor( ); Console.Error.WriteLine( "unctions only" ); Console.Error.WriteLine( " (default: functions as well as variables)" ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( " /V" ); Console.ResetColor( ); Console.Error.Write( " tests " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( "V" ); Console.ResetColor( ); Console.Error.WriteLine( "ariables only" ); Console.Error.WriteLine( " (default: functions as well as variables)" ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( " /U" ); Console.ResetColor( ); Console.Error.Write( " list " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( "U" ); Console.ResetColor( ); Console.Error.WriteLine( "nused functions and variables only" ); Console.Error.WriteLine( " (default: list all functions and variables)" ); Console.Error.WriteLine( ); Console.Error.WriteLine( "Notes: If functions are tested, a (limited) test for nested functions" ); Console.Error.WriteLine( " and unterminated curly braces is performed as well." ); Console.Error.WriteLine( " In PHP variables are not declared, so a single occurrence of a" ); Console.Error.WriteLine( " variable means it is not used, unless it is a superglobal which" ); Console.Error.WriteLine( " may be declared elsewhere. Keep in mind though, that this program" ); Console.Error.WriteLine( " does not take into account the variables' scope, so a variable" ); Console.Error.WriteLine( " name used once in two functions may in fact be unused but will" ); Console.Error.WriteLine( " escape detection by this program." ); Console.Error.WriteLine( " The program's return code equals the number of unused functions" ); Console.Error.WriteLine( " and/or variables, or -1 in case of (command line) errors." ); Console.Error.WriteLine( ); Console.Error.WriteLine( "Written by Rob van der Woude" ); Console.Error.WriteLine( "https://www.robvanderwoude.com" ); #endregion Display Help Text return -1; } } }