Rob van der Woude's Scripting Pages
Powered by GeSHi

Source code for checkvarsphp.cs

(view source code of checkvarsphp.cs as plain text)

  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Text;
  6. using System.Text.RegularExpressions;
  7.  
  8.  
  9. namespace RobvanderWoude
  10. {
  11. 	internal class CheckVarsPHP
  12. 	{
  13. 		static readonly string progver = "1.02";
  14.  
  15.  
  16. 		static string scriptcode = String.Empty;
  17.  
  18.  
  19. 		static int Main( string[] args )
  20. 		{
  21. 			#region Initialize Variables
  22.  
  23. 			SortedList<string, int> functions = new SortedList<string, int>( );
  24. 			SortedList<string, int> variables = new SortedList<string, int>( );
  25. 			bool showfuncs = true;
  26. 			bool showvars = true;
  27. 			bool unusedonly = false;
  28. 			Regex regex;
  29. 			string pattern;
  30. 			string scriptext = String.Empty;
  31. 			string scriptfile = String.Empty;
  32. 			int columnwidth = 12;
  33. 			int unusedfuncs = 0;
  34. 			int unusedvars = 0;
  35.  
  36. 			#endregion Initialize Variables
  37.  
  38.  
  39. 			#region Command Line Parsing
  40.  
  41. 			if ( args.Length == 0 )
  42. 			{
  43. 				return ShowHelp( );
  44. 			}
  45.  
  46. 			foreach ( string arg in args )
  47. 			{
  48. 				if ( arg[0] == '/' )
  49. 				{
  50. 					if ( arg.ToUpper( ) == "/?" )
  51. 					{
  52. 						return ShowHelp( );
  53. 					}
  54. 					else if ( arg.ToUpper( ) == "/F" )
  55. 					{
  56. 						if ( !showvars )
  57. 						{
  58. 							return ShowHelp( "Duplicate command line switch /F" );
  59. 						}
  60. 						if ( !showfuncs )
  61. 						{
  62. 							return ShowHelp( "Use /F or /V or neither, but not both" );
  63. 						}
  64. 						showvars = false;
  65. 					}
  66. 					else if ( arg.ToUpper( ) == "/U" )
  67. 					{
  68. 						if ( unusedonly )
  69. 						{
  70. 							return ShowHelp( "Duplicate command line switch /U" );
  71. 						}
  72. 						unusedonly = true;
  73. 					}
  74. 					else if ( arg.ToUpper( ) == "/V" )
  75. 					{
  76. 						if ( !showfuncs )
  77. 						{
  78. 							return ShowHelp( "Duplicate command line switch /V" );
  79. 						}
  80. 						if ( !showvars )
  81. 						{
  82. 							return ShowHelp( "Use /F or /V or neither, but not both" );
  83. 						}
  84. 						showfuncs = false;
  85. 					}
  86. 					else
  87. 					{
  88. 						return ShowHelp( "Invalid command line switch \"{0}\"", arg );
  89. 					}
  90. 				}
  91. 				else
  92. 				{
  93. 					if ( !String.IsNullOrWhiteSpace( scriptfile ) )
  94. 					{
  95. 						return ShowHelp( "Duplicate command line argument for PHP file" );
  96. 					}
  97. 					if ( !File.Exists( arg ) )
  98. 					{
  99. 						return ShowHelp( "Invalid file name or file not found: \"{0}\"", arg );
  100. 					}
  101. 					scriptext = Path.GetExtension( arg ).ToLower( );
  102. 					if ( scriptext != ".php" )
  103. 					{
  104. 						return ShowHelp( "Invalid file type \"{0}\"", arg );
  105. 					}
  106. 					scriptfile = Path.GetFullPath( arg );
  107. 				}
  108.  
  109. 			}
  110.  
  111. 			if ( String.IsNullOrWhiteSpace( scriptfile ) )
  112. 			{
  113. 				return ShowHelp( "Please specify a source file" );
  114. 			}
  115.  
  116. 			#endregion Command Line Parsing
  117.  
  118.  
  119. 			#region Read File
  120.  
  121. 			// Read the code from the file
  122. 			scriptcode = File.ReadAllText( scriptfile, Encoding.UTF8 );
  123. 			// Strip everything NOT PHP
  124. 			if ( scriptcode.Contains( "<?php" ) )
  125. 			{
  126. 				string purephp = string.Empty;
  127. 				pattern = @"<\?php[\w\W]*?\?>";
  128. 				regex = new Regex( pattern );
  129. 				if ( regex.IsMatch( scriptcode ) )
  130. 				{
  131. 					foreach ( Match match in regex.Matches( scriptcode ) )
  132. 					{
  133. 						purephp += match.Value + "\n\n";
  134. 					}
  135. 				}
  136. 				scriptcode = purephp;
  137. 			}
  138. 			// Remove comment lines from the code (does NOT strip comments starting halfway on a line)
  139. 			pattern = @"(^|\n|\r)[ \t]*//[^\n\r]+";
  140. 			regex = new Regex( pattern );
  141. 			scriptcode = regex.Replace( scriptcode, String.Empty );
  142. 			// Remove comment blocks from the code (does NOT strip comments starting halfway on a line)
  143. 			pattern = @"(^|\n|\r)[ \t]*/\*[\w\W]*\*/";
  144. 			regex = new Regex( pattern );
  145. 			scriptcode = regex.Replace( scriptcode, String.Empty );
  146.  
  147. 			#endregion Read File
  148.  
  149.  
  150. 			#region List Functions
  151.  
  152. 			// Create a list of subroutines found in the code
  153. 			// function names are NOT case sensitive
  154. 			if ( showfuncs )
  155. 			{
  156. 				pattern = @"(?:^|\n|\r)[ \t]*(?:function)[ \t]+([A-Z_][^\s\(]+)\(";
  157. 				regex = new Regex( pattern, RegexOptions.IgnoreCase );
  158. 				if ( regex.IsMatch( scriptcode ) )
  159. 				{
  160. 					MatchCollection matches = regex.Matches( scriptcode );
  161. 					if ( matches.Count > 0 )
  162. 					{
  163. 						foreach ( Match match in matches )
  164. 						{
  165. 							bool listed = false;
  166. 							string func = match.Groups[1].Value;
  167. 							foreach ( string key in functions.Keys )
  168. 							{
  169. 								if ( func.ToLower( ) == key.ToLower( ) )
  170. 								{
  171. 									listed = true;
  172. 								}
  173. 							}
  174. 							if ( !listed )
  175. 							{
  176. 								functions[func] = 0;
  177. 								columnwidth = Math.Max( func.Length, columnwidth );
  178. 							}
  179. 						}
  180. 					}
  181. 				}
  182. 			}
  183.  
  184. 			#endregion List Functions
  185.  
  186.  
  187. 			#region Check for Nested Functions
  188.  
  189. 			if ( showfuncs && functions.Count > 1 )
  190. 			{
  191. 				pattern = @"function[\t ]+([_A-Z]\w*)[\t ]*\([^\n\r\)]*\)([\w\W]*?)function[\t ]+[_A-Z]";
  192. 				regex = new Regex( pattern, RegexOptions.IgnoreCase );
  193. 				if ( regex.IsMatch( scriptcode ) )
  194. 				{
  195. 					foreach ( Match match in regex.Matches( scriptcode ) )
  196. 					{
  197. 						int errorcount = 0;
  198. 						string funcname = match.Groups[1].Value;
  199. 						string funccode = match.Groups[2].Value;
  200. 						int curlybraces = 0;
  201. 						foreach ( char chr in funccode.ToCharArray( ) )
  202. 						{
  203. 							if ( chr == '{' )
  204. 							{
  205. 								curlybraces++;
  206. 							}
  207. 							else if ( chr == '}' )
  208. 							{
  209. 								curlybraces--;
  210. 							}
  211. 							if ( curlybraces < 0 )
  212. 							{
  213. 								errorcount++;
  214. 							}
  215. 						}
  216. 						if ( curlybraces != 0 )
  217. 						{
  218. 							errorcount++;
  219. 						}
  220. 						if ( errorcount > 0 )
  221. 						{
  222. 							RedLine( "Nested function or unterminated curly braces detected in function \"{0}\"\n\n", funcname );
  223. 						}
  224. 					}
  225. 				}
  226. 			}
  227.  
  228. 			#endregion Check for Nested Functions
  229.  
  230.  
  231. 			#region Check for Unterminated Curly Braces
  232.  
  233. 			if ( !CheckCurlyBraces( ) )
  234. 			{
  235. 				RedLine( "Unterminated curly braces detected\n\n" );
  236. 			}
  237.  
  238. 			#endregion Check for Unterminated Curly Braces
  239.  
  240.  
  241. 			#region List Variables
  242.  
  243. 			// Create a list of variables found in the code and count their occurrence
  244. 			// Variable names ARE case sensitive
  245. 			if ( showvars )
  246. 			{
  247. 				pattern = @"\$[A-Z_][A-Z0-9_]*(\[([\$'\""]?[A-Z_][A-Z0-9_]*['\""]?)\])?";
  248. 				regex = new Regex( pattern, RegexOptions.IgnoreCase );
  249. 				if ( regex.IsMatch( scriptcode ) )
  250. 				{
  251. 					MatchCollection matches = regex.Matches( scriptcode );
  252. 					if ( matches.Count > 0 )
  253. 					{
  254. 						foreach ( Match match in matches )
  255. 						{
  256. 							string varstring = match.ToString( );
  257. 							if ( !variables.ContainsKey( varstring ) )
  258. 							{
  259. 								variables[varstring] = 1;
  260. 								if ( varstring.Length > columnwidth )
  261. 								{
  262. 									columnwidth = varstring.Length;
  263. 								}
  264. 							}
  265. 							else
  266. 							{
  267. 								variables[varstring] += 1;
  268. 							}
  269. 						}
  270. 					}
  271. 				}
  272. 			}
  273.  
  274. 			#endregion List Variables
  275.  
  276.  
  277. 			#region Count and Display Functions Usage
  278.  
  279. 			// Iterate through the list of subroutines and count the occurrences of its name
  280. 			if ( showfuncs )
  281. 			{
  282. 				List<string> keys = new List<string>( functions.Keys );
  283. 				foreach ( string func in keys )
  284. 				{
  285. 					pattern = string.Format( @"\b{0}\(", func );
  286. 					regex = new Regex( pattern );
  287. 					if ( regex.IsMatch( scriptcode ) )
  288. 					{
  289. 						functions[func] = regex.Matches( scriptcode ).Count - 1;
  290. 					}
  291. 				}
  292. 				// Show the results
  293. 				if ( unusedonly )
  294. 				{
  295. 					Console.WriteLine( "{0} Unused Function{1}{2}", unusedfuncs, ( unusedfuncs == 1 ? String.Empty : "s" ), ( unusedfuncs == 0 ? String.Empty : ":" ) );
  296. 					Console.WriteLine( "{0}==============={1}{2}", new String( '=', unusedfuncs.ToString( ).Length ), ( unusedfuncs == 1 ? String.Empty : "=" ), ( unusedfuncs == 0 ? String.Empty : "=" ) );
  297. 				}
  298. 				else
  299. 				{
  300. 					Console.WriteLine( "{0,-" + columnwidth + "}    Occurrences:", "Function:" );
  301. 					Console.WriteLine( "{0,-" + columnwidth + "}    ============", "=========" );
  302. 				}
  303. 				foreach ( string key in functions.Keys )
  304. 				{
  305. 					if ( functions[key] == 0 )
  306. 					{
  307. 						if ( unusedonly )
  308. 						{
  309. 							Console.WriteLine( key );
  310. 						}
  311. 						else
  312. 						{
  313. 							RedLine( string.Format( "{0,-" + columnwidth + "}    {1}", key, functions[key] ) );
  314. 						}
  315. 						unusedfuncs += 1;
  316. 					}
  317. 					else if ( !unusedonly )
  318. 					{
  319. 						Console.WriteLine( "{0,-" + columnwidth + "}    {1}", key, functions[key] );
  320. 					}
  321. 				}
  322. 				Console.WriteLine( );
  323. 			}
  324.  
  325. 			#endregion Count and Display Functions Usage
  326.  
  327.  
  328. 			#region Count and Display Variables Usage
  329.  
  330. 			if ( showvars )
  331. 			{
  332. 				// Show the results
  333. 				if ( unusedonly )
  334. 				{
  335. 					Console.WriteLine( "{0} Unused Variable{1}{2}", unusedvars, ( unusedvars == 1 ? String.Empty : "s" ), ( unusedvars == 0 ? String.Empty : ":" ) );
  336. 					Console.WriteLine( "{0}================{1}{2}", new String( '=', unusedvars.ToString( ).Length ), ( unusedvars == 1 ? String.Empty : "=" ), ( unusedvars == 0 ? String.Empty : "=" ) );
  337. 				}
  338. 				else
  339. 				{
  340. 					Console.WriteLine( "{0,-" + columnwidth + "}    Occurrences:", "Variable:" );
  341. 					Console.WriteLine( "{0,-" + columnwidth + "}    ============", "=========" );
  342. 				}
  343. 				foreach ( string key in variables.Keys )
  344. 				{
  345. 					if ( variables[key] == 1 && !IsSuperGlobal( key ) && key[0] != '$' )
  346. 					{
  347. 						if ( unusedonly )
  348. 						{
  349. 							Console.WriteLine( key );
  350. 						}
  351. 						else
  352. 						{
  353. 							RedLine( String.Format( "{0,-" + columnwidth + "}    {1}", key, variables[key] ) );
  354. 						}
  355. 						unusedvars += 1;
  356. 					}
  357. 					else if ( !unusedonly )
  358. 					{
  359. 						Console.WriteLine( "{0,-" + columnwidth + "}    {1}", key, variables[key] );
  360. 					}
  361. 				}
  362. 				Console.WriteLine( );
  363. 			}
  364.  
  365. 			#endregion Count and Display Variables Usage
  366.  
  367.  
  368. 			int rc = 0;
  369. 			if ( showfuncs )
  370. 			{
  371. 				rc += unusedfuncs;
  372. 			}
  373. 			if ( showvars )
  374. 			{
  375. 				rc += unusedvars;
  376. 			}
  377. 			return rc;
  378. 		}
  379.  
  380.  
  381. 		static bool CheckCurlyBraces( )
  382. 		{
  383. 			if ( scriptcode.Count( o => ( o == '{' ) ) != scriptcode.Count( c => ( c == '}' ) ) )
  384. 			{
  385. 				return false;
  386. 			}
  387. 			string testtext = Regex.Replace( scriptcode, @"[^\{\}]", "" );
  388. 			int state = 0;
  389. 			for ( int i = 0; i < testtext.Length; i++ )
  390. 			{
  391. 				if ( testtext[i] == '{' )
  392. 				{
  393. 					state++;
  394. 				}
  395. 				else
  396. 				{
  397. 					state--;
  398. 				}
  399. 				if ( state < 0 )
  400. 				{
  401. 					return false;
  402. 				}
  403. 			}
  404. 			return true;
  405. 		}
  406.  
  407.  
  408. 		static bool IsSuperGlobal( string var )
  409. 		{
  410. 			string pattern = @"^\$(GLOBALS|_(SERVER|GET|POST|FILES|COOKIE|SESSION|REQUEST|ENV))(\[[^\]]+\])?$";
  411. 			Regex regex = new Regex( pattern );
  412. 			return regex.IsMatch( var.Trim( ) );
  413. 		}
  414.  
  415.  
  416. 		static void RedLine( string line, params object[] rlargs )
  417. 		{
  418. 			Console.ForegroundColor = ConsoleColor.Red;
  419. 			if ( rlargs.Length > 0 )
  420. 			{
  421. 				Console.WriteLine( line, rlargs );
  422. 			}
  423. 			else
  424. 			{
  425. 				Console.WriteLine( line );
  426. 			}
  427. 			Console.ResetColor( );
  428. 		}
  429.  
  430.  
  431. 		static int ShowHelp( params string[] errmsg )
  432. 		{
  433. 			#region Error Message
  434.  
  435. 			if ( errmsg.Length > 0 )
  436. 			{
  437. 				List<string> errargs = new List<string>( errmsg );
  438. 				errargs.RemoveAt( 0 );
  439. 				Console.Error.WriteLine( );
  440. 				Console.ForegroundColor = ConsoleColor.Red;
  441. 				Console.Error.Write( "ERROR:\t" );
  442. 				Console.ForegroundColor = ConsoleColor.White;
  443. 				Console.Error.WriteLine( errmsg[0], errargs.ToArray( ) );
  444. 				Console.ResetColor( );
  445. 			}
  446.  
  447. 			#endregion Error Message
  448.  
  449.  
  450. 			#region Help Text
  451.  
  452. 			/*
  453. 			CheckVarsPHP.exe,  Version 1.02
  454. 			Check PHP code for unused variables and functions
  455.  
  456. 			Usage:    CheckVarsPHP.exe  "phpfile"  [ /F | /V ]  [ /U ]
  457.  
  458. 			Where:    "phpfile"         is the PHP file to be examined
  459. 			          /F                tests Functions only
  460. 			                            (default: functions as well as variables)
  461. 			          /V                tests Variables only
  462. 			                            (default: functions as well as variables)
  463. 			          /U                list Unused functions and variables only
  464. 			                            (default: list all functions and variables)
  465.  
  466. 			Notes:    If functions are tested, a (limited) test for nested functions
  467. 			          and unterminated curly braces is performed as well.
  468. 			          In PHP variables are not declared, so a single occurrence of a
  469. 			          variable means it is not used, unless it is a superglobal which
  470. 			          may be declared elsewhere. Keep in mind though, that this program
  471. 			          does not take into account the variables' scope, so a variable
  472. 			          name used once in two functions may in fact be unused but will
  473. 			          escape detection by this program.
  474. 			          The program's return code equals the number of unused functions
  475. 			          and/or variables, or -1 in case of (command line) errors.
  476.  
  477. 			Written by Rob van der Woude
  478. 			https://www.robvanderwoude.com
  479. 			*/
  480.  
  481. 			#endregion Help Text
  482.  
  483.  
  484. 			#region Display Help Text
  485.  
  486. 			Console.Error.WriteLine( );
  487.  
  488. 			Console.Error.WriteLine( "CheckVarsPHP.exe,  Version {0}", progver );
  489.  
  490. 			Console.Error.WriteLine( "Check PHP code for unused variables and functions" );
  491.  
  492. 			Console.Error.WriteLine( );
  493.  
  494. 			Console.Error.Write( "Usage:    " );
  495. 			Console.ForegroundColor = ConsoleColor.White;
  496. 			Console.Error.WriteLine( "CheckVarsPHP.exe  \"phpfile\"  [ /F | /V ]  [ /U ]" );
  497. 			Console.ResetColor( );
  498.  
  499. 			Console.Error.WriteLine( );
  500.  
  501. 			Console.Error.Write( "Where:    " );
  502. 			Console.ForegroundColor = ConsoleColor.White;
  503. 			Console.Error.Write( "\"phpfile\"" );
  504. 			Console.ResetColor( );
  505. 			Console.Error.WriteLine( "         is the PHP file to be examined" );
  506.  
  507. 			Console.ForegroundColor = ConsoleColor.White;
  508. 			Console.Error.Write( "          /F" );
  509. 			Console.ResetColor( );
  510. 			Console.Error.Write( "                tests " );
  511. 			Console.ForegroundColor = ConsoleColor.White;
  512. 			Console.Error.Write( "F" );
  513. 			Console.ResetColor( );
  514. 			Console.Error.WriteLine( "unctions only" );
  515.  
  516. 			Console.Error.WriteLine( "                            (default: functions as well as variables)" );
  517.  
  518. 			Console.ForegroundColor = ConsoleColor.White;
  519. 			Console.Error.Write( "          /V" );
  520. 			Console.ResetColor( );
  521. 			Console.Error.Write( "                tests " );
  522. 			Console.ForegroundColor = ConsoleColor.White;
  523. 			Console.Error.Write( "V" );
  524. 			Console.ResetColor( );
  525. 			Console.Error.WriteLine( "ariables only" );
  526.  
  527. 			Console.Error.WriteLine( "                            (default: functions as well as variables)" );
  528.  
  529. 			Console.ForegroundColor = ConsoleColor.White;
  530. 			Console.Error.Write( "          /U" );
  531. 			Console.ResetColor( );
  532. 			Console.Error.Write( "                list " );
  533. 			Console.ForegroundColor = ConsoleColor.White;
  534. 			Console.Error.Write( "U" );
  535. 			Console.ResetColor( );
  536. 			Console.Error.WriteLine( "nused functions and variables only" );
  537.  
  538. 			Console.Error.WriteLine( "                            (default: list all functions and variables)" );
  539.  
  540. 			Console.Error.WriteLine( );
  541.  
  542. 			Console.Error.WriteLine( "Notes:    If functions are tested, a (limited) test for nested functions" );
  543.  
  544. 			Console.Error.WriteLine( "          and unterminated curly braces is performed as well." );
  545.  
  546. 			Console.Error.WriteLine( "          In PHP variables are not declared, so a single occurrence of a" );
  547.  
  548. 			Console.Error.WriteLine( "          variable means it is not used, unless it is a superglobal which" );
  549.  
  550. 			Console.Error.WriteLine( "          may be declared elsewhere. Keep in mind though, that this program" );
  551.  
  552. 			Console.Error.WriteLine( "          does not take into account the variables' scope, so a variable" );
  553.  
  554. 			Console.Error.WriteLine( "          name used once in two functions may in fact be unused but will" );
  555.  
  556. 			Console.Error.WriteLine( "          escape detection by this program." );
  557.  
  558. 			Console.Error.WriteLine( "          The program's return code equals the number of unused functions" );
  559.  
  560. 			Console.Error.WriteLine( "          and/or variables, or -1 in case of (command line) errors." );
  561.  
  562. 			Console.Error.WriteLine( );
  563.  
  564. 			Console.Error.WriteLine( "Written by Rob van der Woude" );
  565.  
  566. 			Console.Error.WriteLine( "https://www.robvanderwoude.com" );
  567.  
  568. 			#endregion Display Help Text
  569.  
  570.  
  571. 			return -1;
  572. 		}
  573. 	}
  574. }

page last modified: 2022-10-20