Sourcecode - SourcecodeCheck

Download SourcecodeCheck.cs


/*
 *
 *  OUT OF DATE:
 *   This code has been completely rewritten, the new version is working much better...
 *   However, it is not available as standalone executable, but is integrated in
 *   the open source SharpDevelop IDE.
 *   http://www.sharpdevelop.com/
 *
 */






////////////////////////////////////////////////
////           Sourcecode Check             ////
////  Version 1.0.2               01.08.04  ////
////          www.danielgrunwald.de         ////
////////////////////////////////////////////////
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.

using System;
using System.IO;
using System.Text;
using System.Collections;

/*
 * This program parses all *.cs file in its working directory and subdirectories and changes
 * them to be compliant with Mike's Coding Style Guide
 * http://www.icsharpcode.net/TechNotes/SharpDevelopCodingStyle03.pdf
 *
 * Things done by this script:
 *  - Parsing the file and calculating the correct indentation level
 *  - Completely replacing the indentation by the correct amount of tabs.
 *  - Pulling { on the same line as the construct (exceptions: namespace, class, interface, struct and method declarations)
 *  - Putting else, catch and finally on the same line as }
 *
 * Things this script should do, but does not do yet:
 *  - Putting { on the next line in class definitions
 *  - Breaking "if (a) b();" on two lines (and else on the 3rd line...)
 *  - Correctly indenting multiline array value definitions
 *
 * If you implement any missing feature or find some bug, please write an e-mail to
 * daniel@danielgrunwald.de
 *
 * Changelog:
 *  1.0.1: added line continuation, complete replacing of the indentation
 *         added conversion to UTF-8
 *  1.0.2: changed encoding detection to allow UTF-8 and UTF-8 Cookie
 */

public class MainClass
{
	// keywords that let an opening brace have it's own line.
	static string[] newline_keywords = new string[] { "namespace ", "class ", "interface ", "struct ", "enum ", "(" } ;
	// ( means every line containing an ( will have it's { on a new line
	
	// these are the exceptions to the above rule (so no_newline_keywords don't get a new line even if they have a newline_keyword)
	static string[] no_newline_keywords = new string[] { "if", "else if", "while", "using", "for", "lock", "foreach", "catch", "switch" } ;
	// Note: no_newline_keywords are found only with " (", so for "if" the check method searches for "if ("
	
	static string[] behind_closing = new string[] { "else", "catch", "finally" } ;
	
	// so there are three types:
	// Lines without keywords (e.g. properties, set/get-Accessors): they don't get a new line
	// Lines with newline_keywords (class definitions, method definitions)
	// Lines with no_newline_keywords (if, while, etc.)
	
	public static void Main()
	{
		Timing.Start(Timer.Total);
		Work(new DirectoryInfo("."));
		Timing.End(Timer.Total);
		Timing.Display();
		System.Threading.Thread.Sleep(2000);
	}
	
	static void Work(DirectoryInfo dir)
	{
		foreach (FileInfo file in dir.GetFiles("*.cs")) {
			Work(file);
		}
		foreach (DirectoryInfo subdir in dir.GetDirectories()) {
			if (subdir.Name.StartsWith("."))
				continue;
			if (subdir.Name == "bin" || subdir.Name == "obj")
				continue;
			Work(subdir);
		}
	}
	
	static bool MakeUTF8(string filename)
	{
		// Problem: Most files are saved as ISO-Latin-1
		// I want to convert all my sourcecode to UTF-8, so why not put that into this tool?
		// first attempt to detect the encoding
		FileStream fs = new FileStream(filename, FileMode.Open);
		bool changeEncoding = false;
		if (fs.ReadByte() < 128) { // no UTF-8 BOM
			int highbytes = 0;
			for(int i = 1; i < fs.Length; i++) {
				int d = fs.ReadByte();
				if (d < 128) {
					if (highbytes == 1) {
						// this cannot be UTF-8
						changeEncoding = true;
						break;
					}
					highbytes = 0;
				} else {
					highbytes += 1;
				}
			}
		}
		fs.Close();
		if (changeEncoding) {
			// check for byte ordering mark
			Timing.Start(Timer.Encoding);
			StreamReader sr = new StreamReader(filename, Encoding.Default);
			string data = sr.ReadToEnd();
			sr.Close();
			fs = new FileStream(filename, FileMode.Create, FileAccess.Write);
			StreamWriter w = new StreamWriter(fs, Encoding.UTF8);
			w.Write(data);
			w.Close();
			fs.Close();
			Timing.End(Timer.Encoding);
			return true;
		} else {
			return false;
		}
	}
	
	static void Work(FileInfo file)
	{
		bool madeUTF8 = MakeUTF8(file.FullName);
		Timing.Start(Timer.Changing);
		StringBuilder b = new StringBuilder();
		StreamReader sr = new StreamReader(file.FullName, Encoding.UTF8);
		string line;
		State state = State.Code;
		int indent = 0;
		bool comment = false;
		bool lastLineComment;
		bool inString;
		bool continuation = false;
		int lastCommentStart = 0;
		Stack attributeStack = new Stack();
		char stringType = '"';
		string lastLine = "";
		while ((line = sr.ReadLine()) != null) {
			line = line.TrimEnd();
			string trimmed = line.TrimStart();
			if (state == State.Code) {
				// Skandal! welcher Schrotteditor fügt denn immer Leerzeichen ein?
				line = trimmed; // weg mit dem Müll
				// Jetzt kommen echte Tabs:
				if (line.StartsWith("}") && indent > 0)
					line = new String('\t', indent - 1) + line;
				else if (continuation && !line.StartsWith("{"))
					line = new String('\t', indent + 1) + line;
				else
				line = new String('\t', indent) + line;
			}
			lastLineComment = comment;
			comment = false;
			inString = false;
			char lastchar = ' ';
			bool backslash = false;
			for (int i = 0; i < line.Length; i++) {
				char c = line[i];
				if (state == State.Code && !comment) {
					if (inString) {
						if (backslash) {
							backslash = false;
						} else {
							if (c == '\\')
								backslash = true;
							if (c == stringType)
								inString = false;
						}
					} else {
						if (c == '{') {
							if (!char.IsWhiteSpace(lastchar)) {
								// always have a space before {
								line = line.Substring(0, i) + " " + line.Substring(i);
								i++;
							}
							if (i + 1 < line.Length) {
								if (!char.IsWhiteSpace(line[i + 1])) {
									line = line.Substring(0, i + 1) + " " + line.Substring(i + 1);
								}
							}
							continuation = false;
							indent++;
						} else if (c == '}') {
							if (!char.IsWhiteSpace(lastchar)) {
								// always have a space before {
								line = line.Substring(0, i) + " " + line.Substring(i);
								i++;
							}
							if (i + 1 < line.Length) {
								if (!char.IsWhiteSpace(line[i + 1])) {
									line = line.Substring(0, i + 1) + " " + line.Substring(i + 1);
								}
							}
							continuation = false;
							if (indent == 0)
								Console.WriteLine("Indentation problem in " + file.Name);
							else
							indent--;
						} else if (c == '#' || (c == '/' && lastchar == '/')) {
							// count #region etc. as comment to prevent line continuation
							comment = true; // one line comment
							lastCommentStart = i;
						} else if (c == '*' && lastchar == '/')
							state = State.Comment;
						else if (c == '"')
							inString = true;
						else if (c == '\'')
							inString = true;
						else if (c == ';')
							continuation = false;
						else if (c == '[')
							attributeStack.Push(continuation);
						else if (c == ']')
							continuation = (bool)attributeStack.Pop();
						else {
							if (c != ',' && c != '/' && !char.IsWhiteSpace(c))
								continuation = true;
						}
						if (inString) {
							// string betreten
							if (c == '"' && lastchar == '@')
								state = State.String;
							else
							stringType = c;
						}
					}
				} else if (state == State.Comment && c == '/' && lastchar == '*') {
					state = State.Code;
				} else if (state == State.String && c == '"') {
					// @"string"
					if (backslash) {
						backslash = false;
					} else if (i + 1 < line.Length) {
						if (line[i + 1] == '"')
							backslash = true;
						else
						state = State.Code;
					} else { state = State.Code; }
				}
				lastchar = c;
			}
			if (state == State.Code) {
				for (int i = 0; i < no_newline_keywords.Length; i++) {
					if (trimmed.StartsWith(no_newline_keywords[i] + "(") ||
						trimmed.StartsWith("} " + no_newline_keywords[i] + "(")) {
						int pos = line.IndexOf(no_newline_keywords[i]);
						line = line.Substring(0, pos) + no_newline_keywords[i] + " " +
							line.Substring(pos + no_newline_keywords[i].Length);
						break;
					}
				}
				if (lastLine.Trim() == "}") {
					for (int i = 0; i < behind_closing.Length; i++) {
						if (trimmed.StartsWith(behind_closing[i] + " ")) {
							b.Remove(b.Length - 2, 2); // remove last newline
							line = " " + trimmed;
						}
					}
				}
			}
			if (state == State.Code && trimmed == "{") {
				int i;
				for (i = 0; i < newline_keywords.Length; i++) {
					if (lastLine.IndexOf(newline_keywords[i]) >= 0)
						break;
				}
				bool joinLines = false;
				if (i == newline_keywords.Length) {
					joinLines = true;
				} else {
					for (i = 0; i < no_newline_keywords.Length; i++) {
						if (lastLine.IndexOf(no_newline_keywords[i] + " (") >= 0) {
							joinLines = true;
							break;
						}
					}
				}
				if (joinLines) {
					// put this bracket on the previous line
					b.Remove(b.Length - 2, 2); // remove last newline
					if (lastLineComment) {
						// if (a) // do something
						// {
						// -> put { in front of the comment
						b.Insert(b.Length - lastLine.Length + lastCommentStart - 1,
							"{\r\n" + new String('\t', indent));
						line = ""; // append nothing
					} else {
						line = " {";
					}
				}
			} else if (state == State.Code && line.EndsWith("{") && !line.EndsWith(" {")) {
				line = line.Substring(0, line.Length - 1) + " {";
			}
			b.Append(line);
			b.Append("\r\n");
			lastLine = line;
		}
		sr.Close();
		string data = b.ToString();
		Timing.End(Timer.Changing);
		sr = new StreamReader(file.FullName, Encoding.UTF8);
		string data2 = sr.ReadToEnd();
		if (data2 != data) {
			Timing.Start(Timer.Writing);
			sr.Close();
			FileStream fs = new FileStream(file.FullName, FileMode.Create, FileAccess.Write);
			StreamWriter w = new StreamWriter(fs, Encoding.UTF8);
			w.Write(data);
			w.Close();
			fs.Close();
			if (madeUTF8)
				Console.WriteLine(" ME {0}", file.FullName);
			else
			Console.WriteLine(" M  {0}", file.FullName);
			Timing.End(Timer.Writing);
		} else {
			sr.Close();
			if (madeUTF8)
				Console.WriteLine("  E {0}", file.FullName);
		}
	}
}

enum State
{
	Code,
	Comment,
	String
}

enum Timer
{
	Total = 0,
		Encoding = 1,
		Changing = 2,
		Writing = 3,
		EndOfEnum = 4
}


class Timing
{
	static int[] start = new int[(int)Timer.EndOfEnum];
	static int[] times = new int[(int)Timer.EndOfEnum];
	
	public static void Start(Timer timer)
	{
		start[(int)timer] = Environment.TickCount;
	}
	
	public static void End(Timer timer)
	{
		int end = Environment.TickCount;
		times[(int)timer] += (end - start[(int)timer]);
	}
	
	public static void Display()
	{
		Console.WriteLine("Total time:    {0} ms", times[(int)Timer.Total]);
		Console.WriteLine("Time encoding: {0} ms", times[(int)Timer.Encoding]);
		Console.WriteLine("Time working:  {0} ms", times[(int)Timer.Changing]);
		Console.WriteLine("Time writing:  {0} ms", times[(int)Timer.Writing]);
	}
}