diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..97bcc38 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +tab_width = 4 +trim_trailing_whitespace = true +insert_final_newline = true +charset=utf-8 diff --git a/ServerCodeExciser/InjectionTable.cs b/ServerCodeExciser/InjectionTable.cs new file mode 100644 index 0000000..dfa7738 --- /dev/null +++ b/ServerCodeExciser/InjectionTable.cs @@ -0,0 +1,66 @@ +using SharpCompress.Common; +using System; +using System.Collections.Generic; + +namespace ServerCodeExciser +{ + public class Marker + { + public bool Start { get; } + public string OptElse { get; } + public string Context { get; } + + public Marker(string context, bool start, string optElse = "") + { + Start = start; + OptElse = optElse; + Context = context; + } + + public void Write(ScriptBuilder builder) + { + if (Start) + { + builder.AddLine($"#ifdef WITH_SERVER // {Context}"); + } + else + { + if (builder.IsInScope("WITH_SERVER")) + { + var values = OptElse.Split(new char[] {'\r', '\n'}, StringSplitOptions.RemoveEmptyEntries); + foreach (var v in values) + { + builder.AddLine(v); + } + } + builder.AddLine($"#endif // WITH_SERVER {Context}"); + } + } + } + + public class InjectionTable + { + private readonly Dictionary> m_table = new Dictionary>(); + + public void Add(int line, Marker value) + { + if (m_table.TryGetValue(line, out var list)) + { + list.Add(value); + } + else + { + m_table.Add(line, new List { value }); + } + } + + public IEnumerable Get(int line) + { + if (m_table.TryGetValue(line, out var list)) + { + return list; + } + return Array.Empty(); + } + } +} diff --git a/ServerCodeExciser/PreprocessorTokenStream.cs b/ServerCodeExciser/PreprocessorTokenStream.cs new file mode 100644 index 0000000..44ae66b --- /dev/null +++ b/ServerCodeExciser/PreprocessorTokenStream.cs @@ -0,0 +1,87 @@ +using Antlr4.Runtime; +using Antlr4.Runtime.Misc; +using Microsoft.Build.Framework; +using System; +using static System.Formats.Asn1.AsnWriter; + +namespace ServerCodeExciser +{ + public class Preprocessor : ITokenSource + { + private readonly ITokenSource m_tokenSource; + private readonly ScopeStack m_scope = new ScopeStack(); + + public Preprocessor(ITokenSource tokenSource) + { + m_tokenSource = tokenSource; + } + + public int Line + { + get { return m_tokenSource.Line; } + } + + public int Column + { + get { return m_tokenSource.Column; } + } + + public ICharStream InputStream + { + get { return m_tokenSource.InputStream; } + } + + public string SourceName + { + get { return m_tokenSource.SourceName; } + } + + public ITokenFactory TokenFactory + { + get { return m_tokenSource.TokenFactory; } + set { m_tokenSource.TokenFactory = value; } + } + + public IToken NextToken() + { + var token = m_tokenSource.NextToken(); + while (token.Type == UnrealAngelscriptLexer.Preprocessor || m_scope.IsInScope("WITH_SERVER")) + { + if (token.Type == UnrealAngelscriptLexer.Preprocessor) + { + Process(token.Text); + } + else + { + //Console.WriteLine($"skipping: " + token.Text); + } + token = m_tokenSource.NextToken(); + } + return token; + } + + private void Process(string line) + { + if (line.StartsWith("#ifdef ")) + { + m_scope.Push(line); + } + else if (line.StartsWith("#if ")) + { + m_scope.Push(line); + } + else if (line.StartsWith("#ifndef ")) + { + m_scope.Push(line); + } + else if (line.StartsWith("#else")) + { + m_scope.Else(out var name); + } + else if (line.StartsWith("#endif")) + { + m_scope.Pop(out var name); + } + } + } +} diff --git a/ServerCodeExciser/Properties/launchSettings.json b/ServerCodeExciser/Properties/launchSettings.json new file mode 100644 index 0000000..3a01398 --- /dev/null +++ b/ServerCodeExciser/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "ServerCodeExciser": { + "commandName": "Project", + "commandLineArgs": "D:\\p4\\task\\Games\\Shared\\Plugins\\AIScript\\Script -o D:\\out" + } + } +} \ No newline at end of file diff --git a/ServerCodeExciser/ScopeStack.cs b/ServerCodeExciser/ScopeStack.cs new file mode 100644 index 0000000..62f08c6 --- /dev/null +++ b/ServerCodeExciser/ScopeStack.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; + +namespace ServerCodeExciser +{ + public class ScopeStack + { + private Dictionary m_scopes = new Dictionary(); + private Stack m_scope = new Stack(); + private int m_else = 0; + + public bool Push(string name) + { + if (name.StartsWith("#ifdef ")) + { + name = TrimAndStripComments(name.Substring(7)); + } + else if (name.StartsWith("#if ")) + { + name = TrimAndStripComments(name.Substring(4)); + } + else if (name.StartsWith("#ifndef ")) + { + name = "!" + TrimAndStripComments(name.Substring(8)); + } + + m_scope.Push(name); + + if (m_scopes.ContainsKey(name) && m_scopes[name] > 0) + { + m_scopes[name] += 1; + return false; + } + else + { + m_scopes[name] = 1; + return true; + } + } + + public void Else(out string name) + { + name = m_scope.Pop(); + m_scopes[name] -= 1; + var o = (name[0] == '!') ? name.Substring(1) : ("!" + name); + Push(o); + } + + public bool Pop(out string name) + { + if (m_scope.Count <= 0) + { + name = string.Empty; + return false; + } + name = m_scope.Pop(); + m_scopes[name] -= 1; + return m_scopes[name] == 0; + } + + public bool IsInScope(string name) + { + if (m_scopes.TryGetValue(name, out var count)) + { + return count > 0; + } + return false; + } + + private string TrimAndStripComments(string text) + { + int idx = text.IndexOf("//"); + if (idx >= 0) + { + return text.Substring(0, idx).Trim(); + } + return text.Trim(); + } + } +} diff --git a/ServerCodeExciser/ScriptBuilder.cs b/ServerCodeExciser/ScriptBuilder.cs new file mode 100644 index 0000000..23a10d7 --- /dev/null +++ b/ServerCodeExciser/ScriptBuilder.cs @@ -0,0 +1,71 @@ +using System; +using System.Text; + +namespace ServerCodeExciser +{ + public class ScriptBuilder + { + private ScopeStack m_scope = new ScopeStack(); + private StringBuilder m_text = new StringBuilder(); + + public void AddLine(string line) + { + var trimmedLine = line.Trim(); + if (trimmedLine.StartsWith("#ifdef ")) + { + if (m_scope.Push(trimmedLine)) + { + m_text.AppendLine(trimmedLine); + } + } + else if (trimmedLine.StartsWith("#if ")) + { + if (m_scope.Push(trimmedLine)) + { + m_text.AppendLine(trimmedLine); + } + } + else if (trimmedLine.StartsWith("#ifndef ")) + { + if (m_scope.Push(trimmedLine)) + { + m_text.AppendLine(trimmedLine); + } + } + else if (trimmedLine.StartsWith("#else")) + { + if (m_scope.Pop(out var name)) + { + m_text.AppendLine($"#else // {name}"); + name = (name[0] == '!') ? name.Substring(1) : ("!" + name); + m_scope.Push(name); + } + else + { + m_text.AppendLine($"#else"); + } + } + else if (trimmedLine.StartsWith("#endif")) + { + if (m_scope.Pop(out var name)) + { + m_text.AppendLine($"#endif // {name}"); + } + } + else + { + m_text.AppendLine(line); + } + } + + public override string ToString() + { + return m_text.ToString(); + } + + public bool IsInScope(string name) + { + return m_scope.IsInScope(name); + } + } +} diff --git a/ServerCodeExciser/ServerCodeExciser.cs b/ServerCodeExciser/ServerCodeExciser.cs index 21de86d..89519d2 100644 --- a/ServerCodeExciser/ServerCodeExciser.cs +++ b/ServerCodeExciser/ServerCodeExciser.cs @@ -1,4 +1,4 @@ -using ServerCodeExcisionCommon; +using ServerCodeExcisionCommon; using Spectre.Console; using Spectre.Console.Cli; using System; @@ -9,7 +9,7 @@ using System.Text.Json.Serialization; using UnrealAngelscriptServerCodeExcision; -namespace ServerCodeExcision +namespace ServerCodeExciser { internal sealed class ServerCodeExciserCommand : Command { @@ -75,7 +75,7 @@ public override ValidationResult Validate() class RootPaths { [JsonPropertyName("AngelscriptScriptRoots")] - public string[] AngelscriptScriptRoots { get;set;} = Array.Empty(); + public string[] AngelscriptScriptRoots { get; set; } = Array.Empty(); } public override int Execute([NotNull] CommandContext context, [NotNull] Settings settings) @@ -99,7 +99,7 @@ public override int Execute([NotNull] CommandContext context, [NotNull] Settings var desc = File.ReadAllText(settings.InputPath); var paths = JsonSerializer.Deserialize(desc); if (paths != null) - { + { parameters.InputPaths.UnionWith(paths.AngelscriptScriptRoots); } else @@ -108,7 +108,7 @@ public override int Execute([NotNull] CommandContext context, [NotNull] Settings return (int)EExciserReturnValues.InternalExcisionError; } } - else if(Directory.Exists(settings.InputPath)) + else if (Directory.Exists(settings.InputPath)) { parameters.InputPaths.Add(settings.InputPath); } @@ -143,7 +143,7 @@ public override int Execute([NotNull] CommandContext context, [NotNull] Settings } - public class ServerCodeExciser + public class ServerCodeExciserProgram { public static int Main(string[] args) { diff --git a/ServerCodeExciser/ServerCodeExcisionProcessor.cs b/ServerCodeExciser/ServerCodeExcisionProcessor.cs index cc66c4a..357388b 100644 --- a/ServerCodeExciser/ServerCodeExcisionProcessor.cs +++ b/ServerCodeExciser/ServerCodeExcisionProcessor.cs @@ -1,12 +1,11 @@ +using Antlr4.Runtime; +using ServerCodeExcisionCommon; using System; using System.Collections.Generic; using System.IO; -using System.Text; using System.Text.RegularExpressions; -using Antlr4.Runtime; -using ServerCodeExcisionCommon; -namespace ServerCodeExcision +namespace ServerCodeExciser { public class ServerCodeExcisionParameters { @@ -25,9 +24,10 @@ public class ServerCodeExcisionParameters public class ServerCodeExcisionProcessor { - private ServerCodeExcisionParameters _parameters; - private List _functionExciseRegexes; - private List _fullyExciseRegexes; + private readonly ServerCodeExcisionParameters _parameters; + private readonly List _functionExciseRegexes; + private readonly List _fullyExciseRegexes; + private readonly List _filesFailedToParse = new List(); public ServerCodeExcisionProcessor(ServerCodeExcisionParameters parameters) { @@ -108,7 +108,7 @@ public EExciserReturnValues ExciseServerCode(string filePattern, IServerCodeExci if (stats.CharactersExcised > 0) { System.Diagnostics.Debug.Assert(stats.TotalNrCharacters > 0, "Something is terribly wrong. We have excised characters, but no total characters..?"); - var excisionRatio = (float)stats.CharactersExcised / (float)stats.TotalNrCharacters * 100.0f; + var excisionRatio = stats.CharactersExcised / (float)stats.TotalNrCharacters * 100.0f; Console.WriteLine("Excised {0:0.00}% of server only code in file ({1}/{2}): {3}", excisionRatio, fileIdx + 1, allFiles.Length, fileName); } @@ -122,7 +122,8 @@ public EExciserReturnValues ExciseServerCode(string filePattern, IServerCodeExci } catch (Exception e) { - Console.WriteLine("Failed to parse ({0}/{1}): {2}", fileIdx + 1, allFiles.Length, fileName); + Console.Error.WriteLine("Failed to parse ({0}/{1}): {2}", fileIdx + 1, allFiles.Length, fileName); + _filesFailedToParse.Add(fileName); } } } @@ -149,7 +150,7 @@ public EExciserReturnValues ExciseServerCode(string filePattern, IServerCodeExci if (globalStats.CharactersExcised > 0) { System.Diagnostics.Debug.Assert(globalStats.TotalNrCharacters > 0, "Something is terribly wrong."); - var totalExcisionRatio = (float)globalStats.CharactersExcised / (float)globalStats.TotalNrCharacters * 100.0f; + var totalExcisionRatio = globalStats.CharactersExcised / (float)globalStats.TotalNrCharacters * 100.0f; Console.WriteLine("----------------------------"); Console.WriteLine("Excised {0:0.00}% ({1}/{2} characters) of server only code from the script files.", totalExcisionRatio, globalStats.CharactersExcised, globalStats.TotalNrCharacters); @@ -157,6 +158,11 @@ public EExciserReturnValues ExciseServerCode(string filePattern, IServerCodeExci var timeTaken = endTime - startTime; Console.WriteLine("Excision took {0:0} hours, {1:0} minutes and {2:0.0} seconds.\n\n", timeTaken.Hours, timeTaken.Minutes, timeTaken.Seconds); + foreach (var file in _filesFailedToParse) + { + Console.Error.WriteLine($"Failed to parse: {file}"); + } + if (_parameters.RequiredExcisionRatio > 0.0f && totalExcisionRatio < _parameters.RequiredExcisionRatio) { Console.Error.WriteLine("A required excision ratio of {0}% was set, but excision only reached {1}%!", _parameters.RequiredExcisionRatio, totalExcisionRatio); @@ -181,22 +187,18 @@ private ExcisionStats ProcessCodeFile(string fileName, string inputPath, EExcisi stats.TotalNrCharacters = script.Length; // Setup parsing and output. - List> serverCodeInjections = new List>(); + var injections = new InjectionTable(); var inputStream = new AntlrInputStream(script); var lexer = excisionLanguage.CreateLexer(inputStream); lexer.AddErrorListener(new ExcisionLexerErrorListener()); - var commonTokenStream = new CommonTokenStream(lexer); + var commonTokenStream = new CommonTokenStream(new Preprocessor(lexer)); var parser = excisionLanguage.CreateParser(commonTokenStream); - var answerText = new StringBuilder(); - answerText.Append(script); IServerCodeVisitor? visitor = null; if (excisionMode == EExcisionMode.Full) { - // We want to excise this entire file. - serverCodeInjections.Add(new KeyValuePair(0, excisionLanguage.ServerScopeStartString + "\r\n")); - serverCodeInjections.Add(new KeyValuePair(script.Length, excisionLanguage.ServerScopeEndString)); - stats.CharactersExcised += script.Length; + injections.Add(0, new Marker("A", true)); + injections.Add(script.Length, new Marker("A", false)); } else if (excisionMode == EExcisionMode.AllFunctions) { @@ -230,62 +232,45 @@ private ExcisionStats ProcessCodeFile(string fileName, string inputPath, EExcisi // First process all server only scopes. foreach (ServerOnlyScopeData currentScope in visitor.DetectedServerOnlyScopes) { - if (currentScope.StartIndex == -1 - || currentScope.StopIndex == -1 - || InjectedMacroAlreadyExistsAtLocation(answerText, currentScope.StartIndex, true, excisionLanguage.ServerScopeStartString) - || InjectedMacroAlreadyExistsAtLocation(answerText, currentScope.StartIndex, false, excisionLanguage.ServerScopeStartString) - || InjectedMacroAlreadyExistsAtLocation(answerText, currentScope.StopIndex, false, excisionLanguage.ServerScopeEndString)) - { - continue; - } - - // If there are already injected macros where we want to go, we should skip injecting. - System.Diagnostics.Debug.Assert(currentScope.StopIndex > currentScope.StartIndex, "There must be some invalid pattern here! Stop is before start!"); - serverCodeInjections.Add(new KeyValuePair(currentScope.StartIndex, excisionLanguage.ServerScopeStartString)); - serverCodeInjections.Add(new KeyValuePair(currentScope.StopIndex, currentScope.Opt_ElseContent + excisionLanguage.ServerScopeEndString)); - stats.CharactersExcised += currentScope.StopIndex - currentScope.StartIndex; + injections.Add(currentScope.StartLine, new Marker(currentScope.Context, true)); + injections.Add(currentScope.StopLine, new Marker(currentScope.Context, false, currentScope.Opt_ElseContent)); } + } - // Next we must add dummy reference variables if they exist. - foreach (KeyValuePair> dummyRefDataPair in visitor.ClassStartIdxDummyReferenceData) + // generate new script. + var builder = new ScriptBuilder(); + using (var reader = new StringReader(script)) + { + int lineIndex = 1; + for (; ; ) { - var dummyRefDataBlockString = new StringBuilder(); - var dummyVarScope = "#ifndef " + excisionLanguage.ServerPrecompilerSymbol; - dummyRefDataBlockString.Append(dummyVarScope); - foreach (var dummyVarDef in dummyRefDataPair.Value) + var line = reader.ReadLine(); + if (line == null) + break; + + foreach (var text in injections.Get(lineIndex)) { - dummyRefDataBlockString.Append("\r\n\t" + dummyVarDef); + text.Write(builder); } - dummyRefDataBlockString.Append("\r\n" + excisionLanguage.ServerScopeEndString + "\r\n"); - - // If there is already a block of dummy reference variables we skip adding new ones, there is no guarantee we are adding the right code. - if (InjectedMacroAlreadyExistsAtLocation(answerText, dummyRefDataPair.Key, false, dummyVarScope + "\r\n")) + if (line.Contains("UEmbarkServerEventsSubsystem::Get()") && !builder.IsInScope("WITH_SERVER")) { - continue; + builder.AddLine("// The next line is server only code, but we cannot suggest a fix."); } - serverCodeInjections.Add(new KeyValuePair(dummyRefDataPair.Key, dummyRefDataBlockString.ToString())); + builder.AddLine(line); + lineIndex++; } } - // Now sort them in the reverse order, since adding later will not affect earlier adds. - serverCodeInjections.Sort(delegate (KeyValuePair pair1, KeyValuePair pair2) - { - return pair2.Key.CompareTo(pair1.Key); - }); - - // Now insert them in that reversed order. - bool fileHasChanged = false; - foreach (var injection in serverCodeInjections) - { - answerText.Insert(injection.Key, injection.Value); - fileHasChanged = true; - } + // detect changes. + var newText = builder.ToString(); + bool fileHasChanged = newText != script; if (fileHasChanged || _parameters.ShouldOutputUntouchedFiles) { - var outputPath = (!string.IsNullOrEmpty(_parameters.OutputPath)) ? Path.Combine(_parameters.OutputPath, relativePath) : fileName; + stats.CharactersExcised = newText.Length - script.Length; + var outputPath = !string.IsNullOrEmpty(_parameters.OutputPath) ? Path.Combine(_parameters.OutputPath, relativePath) : fileName; var outputDirectoryPath = Path.GetDirectoryName(outputPath)!; if (!Directory.Exists(outputDirectoryPath)) { @@ -302,7 +287,7 @@ private ExcisionStats ProcessCodeFile(string fileName, string inputPath, EExcisi File.SetAttributes(outputPath, FileAttributes.Normal); } - File.WriteAllText(outputPath, answerText.ToString()); + File.WriteAllText(outputPath, newText); } } catch (Exception e) @@ -313,20 +298,7 @@ private ExcisionStats ProcessCodeFile(string fileName, string inputPath, EExcisi return stats; } - - private bool InjectedMacroAlreadyExistsAtLocation(StringBuilder script, int index, bool lookAhead, string macro) - { - int startIndex = lookAhead ? index : (index - macro.Length); - int endIndex = lookAhead ? (index + macro.Length) : index; - - if (startIndex < 0 || startIndex >= script.Length - || endIndex < 0 || endIndex >= script.Length) - { - return false; - } - - string scriptSection = script.ToString(startIndex, macro.Length); - return scriptSection == macro; - } } } + + diff --git a/ServerCodeExciserTest/ExcisionIntegrationTests.cs b/ServerCodeExciserTest/ExcisionIntegrationTests.cs index ef5a84e..add238a 100644 --- a/ServerCodeExciserTest/ExcisionIntegrationTests.cs +++ b/ServerCodeExciserTest/ExcisionIntegrationTests.cs @@ -1,150 +1,150 @@ +using ServerCodeExciser; +using ServerCodeExcisionCommon; using System; using System.IO; -using ServerCodeExcision; -using ServerCodeExcisionCommon; public class IntegrationTests { - private static string TestProblemPath = @"Problems"; - //private static string TestProblemPath = @"ProblemTestBed"; - private static string TestAnswerPath = @"Answers"; - private static string TestSolutionPath = @"Solutions"; - - private static string CommonSubPath = @"Common"; - private static string AngelscriptSubPath = @"Angelscript"; - - public static void Main(string[] args) - { - ConsoleColor initialColor = Console.ForegroundColor; - bool excisionWasSuccessful = true; - int nrCorrectAnswers = 0; - int nrErrors = 0; - int nrProblems = 0; - - var commonProblemPath = Path.Combine(TestProblemPath, CommonSubPath); - var commonSolutionPath = Path.Combine(TestSolutionPath, CommonSubPath); - - // Run for Angelscript - excisionWasSuccessful = excisionWasSuccessful && RunExciserIntegrationTests("as", - Path.Combine(TestProblemPath, AngelscriptSubPath), - Path.Combine(TestAnswerPath, AngelscriptSubPath), - Path.Combine(TestSolutionPath, AngelscriptSubPath), - commonProblemPath, - commonSolutionPath, - ref nrCorrectAnswers, ref nrErrors, ref nrProblems); - - if (excisionWasSuccessful) - { - Console.ForegroundColor = initialColor; - Console.WriteLine("----------------------------"); - Console.WriteLine(nrCorrectAnswers > 0 && nrCorrectAnswers == nrProblems - ? string.Format("{0} test(s) ran successfully.", nrCorrectAnswers) - : string.Format("{0} error(s) detected running {1} tests", nrErrors, nrProblems)); - } - } - - private static bool RunExciserIntegrationTests(string fileExtension, string testProblemPath, string testAnswerPath, string testSolutionPath, string commonProblemPath, string commonSolutionPath, ref int nrCorrectAnswers, ref int nrErrors, ref int nrProblems) - { - string problemPath = Path.Combine(Environment.CurrentDirectory, testProblemPath); - string answerPath = Path.Combine(Environment.CurrentDirectory, testAnswerPath); - string solutionPath = Path.Combine(Environment.CurrentDirectory, testSolutionPath); - - // First copy common problems to their language folders and rename them so they are picked up by the exciser if they exist. - if (Directory.Exists(commonProblemPath) && Directory.Exists(commonSolutionPath)) - { - CopyCommonTestFiles(fileExtension, problemPath, commonProblemPath); - CopyCommonTestFiles(fileExtension, solutionPath, commonSolutionPath); - } - - // Clean up earlier answers. - if (Directory.Exists(answerPath)) - { - Directory.Delete(answerPath, true); - } - - string[] exciserArgs = - { - problemPath, - "-u", - "-f", - "FullExcise1/.*", - "-a", - "AllFunctionExcise1/.*|||AllFunctionExcise2/.*", - "-o", - answerPath - }; - - var excisionReturnCode = (EExciserReturnValues)ServerCodeExciser.Main(exciserArgs); - if (excisionReturnCode != EExciserReturnValues.Success) - { - Console.Error.WriteLine("Excision error: " + excisionReturnCode); - return false; - } - - if (Directory.Exists(answerPath)) - { - foreach (var answerFilePath in Directory.EnumerateFiles(answerPath, "*." + fileExtension, SearchOption.AllDirectories)) - { - nrProblems++; - - var relativePath = Path.GetRelativePath(answerPath, answerFilePath); - var solutionFilePath = Path.Combine(solutionPath, relativePath); - var fileName = Path.GetFileName(answerFilePath); - - var answer = File.ReadAllText(answerFilePath); - var solution = File.ReadAllText(solutionFilePath); - - if(answer == solution) - { - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine(fileName + "'s answer matched the correct solution!"); - nrCorrectAnswers++; - } - else - { - Console.ForegroundColor = ConsoleColor.Red; - Console.Error.WriteLine(fileName + "'s failed!"); - nrErrors++; - } - } - } - else - { - Console.Error.WriteLine("No test answers found in path: " + answerPath); - return false; - } - - // Clean up common folders if it went well - CleanupTestFiles(problemPath); - CleanupTestFiles(solutionPath); - - return true; - } - - private static void CopyCommonTestFiles(string fileExtension, string targetRootPath, string commonRootPath) - { - // First clear target problem path - CleanupTestFiles(targetRootPath); - - var targetCommonPath = Path.Combine(targetRootPath, "Common"); - foreach (var commonProblemFilePath in Directory.EnumerateFiles(commonRootPath, "*.*", SearchOption.AllDirectories)) - { - var targetPath = Path.Combine(targetCommonPath, Path.GetRelativePath(commonRootPath, Path.ChangeExtension(commonProblemFilePath, fileExtension))); - var targetDirectory = Path.GetDirectoryName(targetPath); - if (targetDirectory != null) - { - Directory.CreateDirectory(targetDirectory); - File.Copy(commonProblemFilePath, targetPath); - } - } - } - - private static void CleanupTestFiles(string targetRootPath) - { - var targetCommonPath = Path.Combine(targetRootPath, "Common"); - if (Directory.Exists(targetCommonPath)) - { - Directory.Delete(targetCommonPath, true); - } - } + private static readonly string TestProblemPath = @"Problems"; + //private static string TestProblemPath = @"ProblemTestBed"; + private static readonly string TestAnswerPath = @"Answers"; + private static readonly string TestSolutionPath = @"Solutions"; + + private static readonly string CommonSubPath = @"Common"; + private static readonly string AngelscriptSubPath = @"Angelscript"; + + public static void Main(string[] args) + { + ConsoleColor initialColor = Console.ForegroundColor; + bool excisionWasSuccessful = true; + int nrCorrectAnswers = 0; + int nrErrors = 0; + int nrProblems = 0; + + var commonProblemPath = Path.Combine(TestProblemPath, CommonSubPath); + var commonSolutionPath = Path.Combine(TestSolutionPath, CommonSubPath); + + // Run for Angelscript + excisionWasSuccessful = excisionWasSuccessful && RunExciserIntegrationTests("as", + Path.Combine(TestProblemPath, AngelscriptSubPath), + Path.Combine(TestAnswerPath, AngelscriptSubPath), + Path.Combine(TestSolutionPath, AngelscriptSubPath), + commonProblemPath, + commonSolutionPath, + ref nrCorrectAnswers, ref nrErrors, ref nrProblems); + + if (excisionWasSuccessful) + { + Console.ForegroundColor = initialColor; + Console.WriteLine("----------------------------"); + Console.WriteLine(nrCorrectAnswers > 0 && nrCorrectAnswers == nrProblems + ? string.Format("{0} test(s) ran successfully.", nrCorrectAnswers) + : string.Format("{0} error(s) detected running {1} tests", nrErrors, nrProblems)); + } + } + + private static bool RunExciserIntegrationTests(string fileExtension, string testProblemPath, string testAnswerPath, string testSolutionPath, string commonProblemPath, string commonSolutionPath, ref int nrCorrectAnswers, ref int nrErrors, ref int nrProblems) + { + string problemPath = Path.Combine(Environment.CurrentDirectory, testProblemPath); + string answerPath = Path.Combine(Environment.CurrentDirectory, testAnswerPath); + string solutionPath = Path.Combine(Environment.CurrentDirectory, testSolutionPath); + + // First copy common problems to their language folders and rename them so they are picked up by the exciser if they exist. + if (Directory.Exists(commonProblemPath) && Directory.Exists(commonSolutionPath)) + { + CopyCommonTestFiles(fileExtension, problemPath, commonProblemPath); + CopyCommonTestFiles(fileExtension, solutionPath, commonSolutionPath); + } + + // Clean up earlier answers. + if (Directory.Exists(answerPath)) + { + Directory.Delete(answerPath, true); + } + + string[] exciserArgs = + { + problemPath, + "-u", + "-f", + "FullExcise1/.*", + "-a", + "AllFunctionExcise1/.*|||AllFunctionExcise2/.*", + "-o", + answerPath + }; + + var excisionReturnCode = (EExciserReturnValues)ServerCodeExciserProgram.Main(exciserArgs); + if (excisionReturnCode != EExciserReturnValues.Success) + { + Console.Error.WriteLine("Excision error: " + excisionReturnCode); + return false; + } + + if (Directory.Exists(answerPath)) + { + foreach (var answerFilePath in Directory.EnumerateFiles(answerPath, "*." + fileExtension, SearchOption.AllDirectories)) + { + nrProblems++; + + var relativePath = Path.GetRelativePath(answerPath, answerFilePath); + var solutionFilePath = Path.Combine(solutionPath, relativePath); + var fileName = Path.GetFileName(answerFilePath); + + var answer = File.ReadAllText(answerFilePath); + var solution = File.ReadAllText(solutionFilePath); + + if (answer == solution) + { + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine(fileName + "'s answer matched the correct solution!"); + nrCorrectAnswers++; + } + else + { + Console.ForegroundColor = ConsoleColor.Red; + Console.Error.WriteLine(fileName + "'s failed!"); + nrErrors++; + } + } + } + else + { + Console.Error.WriteLine("No test answers found in path: " + answerPath); + return false; + } + + // Clean up common folders if it went well + CleanupTestFiles(problemPath); + CleanupTestFiles(solutionPath); + + return true; + } + + private static void CopyCommonTestFiles(string fileExtension, string targetRootPath, string commonRootPath) + { + // First clear target problem path + CleanupTestFiles(targetRootPath); + + var targetCommonPath = Path.Combine(targetRootPath, "Common"); + foreach (var commonProblemFilePath in Directory.EnumerateFiles(commonRootPath, "*.*", SearchOption.AllDirectories)) + { + var targetPath = Path.Combine(targetCommonPath, Path.GetRelativePath(commonRootPath, Path.ChangeExtension(commonProblemFilePath, fileExtension))); + var targetDirectory = Path.GetDirectoryName(targetPath); + if (targetDirectory != null) + { + Directory.CreateDirectory(targetDirectory); + File.Copy(commonProblemFilePath, targetPath); + } + } + } + + private static void CleanupTestFiles(string targetRootPath) + { + var targetCommonPath = Path.Combine(targetRootPath, "Common"); + if (Directory.Exists(targetCommonPath)) + { + Directory.Delete(targetCommonPath, true); + } + } } diff --git a/ServerCodeExcisionCommon/IServerCodeExcisionLanguage.cs b/ServerCodeExcisionCommon/IServerCodeExcisionLanguage.cs index 9b5a1df..5a88ac0 100644 --- a/ServerCodeExcisionCommon/IServerCodeExcisionLanguage.cs +++ b/ServerCodeExcisionCommon/IServerCodeExcisionLanguage.cs @@ -2,23 +2,23 @@ namespace ServerCodeExcisionCommon { - public interface IServerCodeExcisionLanguage - { - List ServerOnlySymbolRegexes { get; } + public interface IServerCodeExcisionLanguage + { + List ServerOnlySymbolRegexes { get; } - List ServerOnlySymbols { get; } + List ServerOnlySymbols { get; } - string ServerPrecompilerSymbol { get; } - string ServerScopeStartString { get; } - string ServerScopeEndString { get; } + string ServerPrecompilerSymbol { get; } + string ServerScopeStartString { get; } + string ServerScopeEndString { get; } - Antlr4.Runtime.Lexer CreateLexer(Antlr4.Runtime.AntlrInputStream inputStream); - IServerCodeParser CreateParser(Antlr4.Runtime.CommonTokenStream tokenStream); - - IServerCodeVisitor CreateSimpleVisitor(string code); - IServerCodeVisitor CreateFunctionVisitor(string code); - IServerCodeVisitor CreateSymbolVisitor(string code); - - bool AnyServerOnlySymbolsInScript(string script); - } + Antlr4.Runtime.Lexer CreateLexer(Antlr4.Runtime.AntlrInputStream inputStream); + IServerCodeParser CreateParser(Antlr4.Runtime.CommonTokenStream tokenStream); + + IServerCodeVisitor CreateSimpleVisitor(string code); + IServerCodeVisitor CreateFunctionVisitor(string code); + IServerCodeVisitor CreateSymbolVisitor(string code); + + bool AnyServerOnlySymbolsInScript(string script); + } } \ No newline at end of file diff --git a/ServerCodeExcisionCommon/IServerCodeParser.cs b/ServerCodeExcisionCommon/IServerCodeParser.cs index 6cc48bb..6035f44 100644 --- a/ServerCodeExcisionCommon/IServerCodeParser.cs +++ b/ServerCodeExcisionCommon/IServerCodeParser.cs @@ -1,9 +1,7 @@ -using System.Collections.Generic; - namespace ServerCodeExcisionCommon { - public interface IServerCodeParser - { - Antlr4.Runtime.Tree.IParseTree GetParseTree(); - } + public interface IServerCodeParser + { + Antlr4.Runtime.Tree.IParseTree GetParseTree(); + } } \ No newline at end of file diff --git a/ServerCodeExcisionCommon/IServerCodeVisitor.cs b/ServerCodeExcisionCommon/IServerCodeVisitor.cs index f9953b7..3bc2c6a 100644 --- a/ServerCodeExcisionCommon/IServerCodeVisitor.cs +++ b/ServerCodeExcisionCommon/IServerCodeVisitor.cs @@ -2,26 +2,28 @@ namespace ServerCodeExcisionCommon { - public struct ServerOnlyScopeData - { - public int StartIndex; - public int StopIndex; - public string Opt_ElseContent; + public struct ServerOnlyScopeData + { + public string Context { get; } + public string Opt_ElseContent { get; set; } + public int StartLine { get; } + public int StopLine { get; } - public ServerOnlyScopeData(int startIndex, int stopIndex) - { - StartIndex = startIndex; - StopIndex = stopIndex; - Opt_ElseContent = ""; - } - } + public ServerOnlyScopeData(string context, int startLine, int stopLine) + { + Context = context; + StartLine = startLine; + StopLine = stopLine; + Opt_ElseContent = ""; + } + } - public interface IServerCodeVisitor - { - List DetectedServerOnlyScopes { get; } - Dictionary> ClassStartIdxDummyReferenceData { get; } - int TotalNumberOfFunctionCharactersVisited { get; } + public interface IServerCodeVisitor + { + List DetectedServerOnlyScopes { get; } + Dictionary> ClassStartIdxDummyReferenceData { get; } + int TotalNumberOfFunctionCharactersVisited { get; } - void VisitContext(Antlr4.Runtime.Tree.IParseTree context); - } + void VisitContext(Antlr4.Runtime.Tree.IParseTree context); + } } \ No newline at end of file diff --git a/ServerCodeExcisionCommon/ServerCodeExcisionUtils.cs b/ServerCodeExcisionCommon/ServerCodeExcisionUtils.cs index 78627c5..371bf70 100644 --- a/ServerCodeExcisionCommon/ServerCodeExcisionUtils.cs +++ b/ServerCodeExcisionCommon/ServerCodeExcisionUtils.cs @@ -1,221 +1,221 @@ +using Antlr4.Runtime; using System; using System.IO; using System.Linq; -using Antlr4.Runtime; namespace ServerCodeExcisionCommon { - public enum EExcisionLanguage - { - Unknown, - Angelscript - } - - public enum EExcisionMode - { - Full, - AllFunctions, - ServerOnlyScopes - } - - public enum EExpressionType - { - NotServerOnly, - ElseIsServerOnly, - EverythingAfterBranchIsServerOnly, - ServerOnly - } - - public enum EReturnType - { - NoReturn, - ReplacedReturn, - ReferenceReturn, - RootScopeReferenceReturn - } - - public struct ExcisionStats - { - // How many characters were actually removed. - public int CharactersExcised; - - // Could be file or function, depending on stats mode. - public int TotalNrCharacters; - } - - public struct StatementRun - { - public int StartLine; - public int StartColumn; - public int StopLine; - public int StopColumn; - - public StatementRun(int initialVal = -1) - { - StartLine = initialVal; - StartColumn = initialVal; - StopLine = initialVal; - StopColumn = initialVal; - } - } - - public struct ReturnData - { - public EReturnType ReturnType; - public string DefaultReturnString; - public StatementRun ReturnStatementRun; - - public ReturnData(string defaultReturnString = "") - { - ReturnType = EReturnType.NoReturn; - DefaultReturnString = defaultReturnString; - ReturnStatementRun = new StatementRun(); - } - } - - public enum EExciserReturnValues - { - Success, - BadInputPath, - InputPathEmpty, - BadOutputPath, - BadArgument, - UnknownExcisionLanguage, - NothingExcised, - InternalExcisionError, - RequiredExcisionRatioNotReached, - RequiresExcision - } - - public class ExcisionException : Exception - { - public ExcisionException(string excisionError, Exception innerException) : base(excisionError, innerException) {} - } - - public class ExcisionParserErrorListener : Antlr4.Runtime.IAntlrErrorListener - { - public void SyntaxError(TextWriter output, IRecognizer recognizer, IToken offendingSymbol, int line, int charPositionInLine, string msg, RecognitionException e) - { - throw new ExcisionException(string.Format("({0}:{1} - {2})", line, charPositionInLine, msg), e); - } - } - - public class ExcisionLexerErrorListener : Antlr4.Runtime.IAntlrErrorListener - { - public void SyntaxError(TextWriter output, IRecognizer recognizer, int offendingSymbol, int line, int charPositionInLine, string msg, RecognitionException e) - { - throw new ExcisionException(string.Format("({0}:{1} - {2})", line, charPositionInLine, msg), e); - } - } - - public static class ExcisionUtils - { - private static char[] NewLineChars = { '\r', '\n' }; - private static char[] SkippableScopeChars = { '\t', '\r', '\n' }; - - public static int FindScriptIndexForCodePoint(string script, int line, int column) - { - int cursor = 0; - int linesTraversed = 1; - while(cursor != -1) - { - if (linesTraversed == line) - { - break; - } - - int searchIdx = cursor; - int windows = script.IndexOf("\r\n", searchIdx); - int other = script.IndexOfAny(NewLineChars, searchIdx); - - if (windows <= other) - { - cursor = windows + 2; - } - else - { - cursor = other + 1; - } - - ++linesTraversed; - } - - return (linesTraversed == line) ? (cursor + column) : -1; - } - - public static int ShrinkServerScope(string script, int start, int end) - { - bool skip = true; - while (skip) - { - skip = false; - - int search = script.IndexOfAny(NewLineChars, start) + 2; - if ((search < end) && SkippableScopeChars.Contains(script.ElementAt(search))) - { - skip = true; - ++start; - } - } - - return start; - } - - public static Type FindFirstDirectChildOfType(Antlr4.Runtime.Tree.IParseTree currentContext) - where Type : class - { - if (currentContext == null) - { - return null; - } - - for(int childIdx = 0; childIdx < currentContext.ChildCount; childIdx++) - { - var child = currentContext.GetChild(childIdx) as Type; - if (child != null) - { - return child; - } - } - - return null; - } - - public static Type FindParentContextOfType(Antlr4.Runtime.Tree.IParseTree currentContext) - where Type : class - { - if (currentContext == null) - { - return null; - } - - Type candidate = currentContext as Type; - if (candidate != null) - { - return candidate; - } - - return FindParentContextOfType(currentContext.Parent); - } - - public static Type FindDirectParentContextOfTypeWithDifferentSourceInterval(Antlr4.Runtime.Tree.IParseTree currentContext, Antlr4.Runtime.Misc.Interval initialSourceInterval) - where Type : class - { - if (currentContext == null) - { - return null; - } - - if (currentContext.SourceInterval.a == initialSourceInterval.a && currentContext.SourceInterval.b == initialSourceInterval.b) - { - // Go further up - return FindDirectParentContextOfTypeWithDifferentSourceInterval(currentContext.Parent, initialSourceInterval); - } - else - { - // We are now at the first ancestor with a different source interval - return currentContext as Type; - } - } - } + public enum EExcisionLanguage + { + Unknown, + Angelscript + } + + public enum EExcisionMode + { + Full, + AllFunctions, + ServerOnlyScopes + } + + public enum EExpressionType + { + NotServerOnly, + ElseIsServerOnly, + EverythingAfterBranchIsServerOnly, + ServerOnly + } + + public enum EReturnType + { + NoReturn, + ReplacedReturn, + ReferenceReturn, + RootScopeReferenceReturn + } + + public struct ExcisionStats + { + // How many characters were actually removed. + public int CharactersExcised; + + // Could be file or function, depending on stats mode. + public int TotalNrCharacters; + } + + public struct StatementRun + { + public int StartLine; + public int StartColumn; + public int StopLine; + public int StopColumn; + + public StatementRun(int initialVal = -1) + { + StartLine = initialVal; + StartColumn = initialVal; + StopLine = initialVal; + StopColumn = initialVal; + } + } + + public struct ReturnData + { + public EReturnType ReturnType; + public string DefaultReturnString; + public StatementRun ReturnStatementRun; + + public ReturnData(string defaultReturnString = "") + { + ReturnType = EReturnType.NoReturn; + DefaultReturnString = defaultReturnString; + ReturnStatementRun = new StatementRun(); + } + } + + public enum EExciserReturnValues + { + Success, + BadInputPath, + InputPathEmpty, + BadOutputPath, + BadArgument, + UnknownExcisionLanguage, + NothingExcised, + InternalExcisionError, + RequiredExcisionRatioNotReached, + RequiresExcision + } + + public class ExcisionException : Exception + { + public ExcisionException(string excisionError, Exception innerException) : base(excisionError, innerException) { } + } + + public class ExcisionParserErrorListener : Antlr4.Runtime.IAntlrErrorListener + { + public void SyntaxError(TextWriter output, IRecognizer recognizer, IToken offendingSymbol, int line, int charPositionInLine, string msg, RecognitionException e) + { + throw new ExcisionException(string.Format("({0}:{1} - {2})", line, charPositionInLine, msg), e); + } + } + + public class ExcisionLexerErrorListener : Antlr4.Runtime.IAntlrErrorListener + { + public void SyntaxError(TextWriter output, IRecognizer recognizer, int offendingSymbol, int line, int charPositionInLine, string msg, RecognitionException e) + { + throw new ExcisionException(string.Format("({0}:{1} - {2})", line, charPositionInLine, msg), e); + } + } + + public static class ExcisionUtils + { + private static readonly char[] NewLineChars = { '\r', '\n' }; + private static readonly char[] SkippableScopeChars = { '\t', '\r', '\n' }; + + public static int FindScriptIndexForCodePoint(string script, int line, int column) + { + int cursor = 0; + int linesTraversed = 1; + while (cursor != -1) + { + if (linesTraversed == line) + { + break; + } + + int searchIdx = cursor; + int windows = script.IndexOf("\r\n", searchIdx); + int other = script.IndexOfAny(NewLineChars, searchIdx); + + if (windows <= other) + { + cursor = windows + 2; + } + else + { + cursor = other + 1; + } + + ++linesTraversed; + } + + return (linesTraversed == line) ? (cursor + column) : -1; + } + + public static int ShrinkServerScope(string script, int start, int end) + { + bool skip = true; + while (skip) + { + skip = false; + + int search = script.IndexOfAny(NewLineChars, start) + 2; + if ((search < end) && SkippableScopeChars.Contains(script.ElementAt(search))) + { + skip = true; + ++start; + } + } + + return start; + } + + public static Type FindFirstDirectChildOfType(Antlr4.Runtime.Tree.IParseTree currentContext) + where Type : class + { + if (currentContext == null) + { + return null; + } + + for (int childIdx = 0; childIdx < currentContext.ChildCount; childIdx++) + { + var child = currentContext.GetChild(childIdx) as Type; + if (child != null) + { + return child; + } + } + + return null; + } + + public static Type FindParentContextOfType(Antlr4.Runtime.Tree.IParseTree currentContext) + where Type : class + { + if (currentContext == null) + { + return null; + } + + Type candidate = currentContext as Type; + if (candidate != null) + { + return candidate; + } + + return FindParentContextOfType(currentContext.Parent); + } + + public static Type FindDirectParentContextOfTypeWithDifferentSourceInterval(Antlr4.Runtime.Tree.IParseTree currentContext, Antlr4.Runtime.Misc.Interval initialSourceInterval) + where Type : class + { + if (currentContext == null) + { + return null; + } + + if (currentContext.SourceInterval.a == initialSourceInterval.a && currentContext.SourceInterval.b == initialSourceInterval.b) + { + // Go further up + return FindDirectParentContextOfTypeWithDifferentSourceInterval(currentContext.Parent, initialSourceInterval); + } + else + { + // We are now at the first ancestor with a different source interval + return currentContext as Type; + } + } + } } diff --git a/UnrealAngelscriptParser/Grammar/UnrealAngelscriptLexer.g4 b/UnrealAngelscriptParser/Grammar/UnrealAngelscriptLexer.g4 index 9e9c355..ecf4e43 100644 --- a/UnrealAngelscriptParser/Grammar/UnrealAngelscriptLexer.g4 +++ b/UnrealAngelscriptParser/Grammar/UnrealAngelscriptLexer.g4 @@ -357,5 +357,4 @@ Whitespace: [ \t]+ -> skip; Newline: ('\r' '\n'? | '\n') -> skip; BlockComment: '/*' .*? '*/' -> skip; LineComment: '//' ~ [\r\n]* -> skip; -PreprocessorBranchRemoval: '#else' .*? '#endif' -> skip; -Preprocessor: ('#if' | '#ifdef' | '#else' | '#endif') ~ [\r\n]* -> skip; \ No newline at end of file +Preprocessor: ('#if' | '#ifdef' | '#else' | '#endif') ~ [\r\n]*; diff --git a/UnrealAngelscriptParser/Grammar/UnrealAngelscriptParser.g4 b/UnrealAngelscriptParser/Grammar/UnrealAngelscriptParser.g4 index 45a3f72..7d53e69 100644 --- a/UnrealAngelscriptParser/Grammar/UnrealAngelscriptParser.g4 +++ b/UnrealAngelscriptParser/Grammar/UnrealAngelscriptParser.g4 @@ -499,7 +499,8 @@ memberdeclaration: | functionDefinition | aliasDeclaration | emptyDeclaration - | accessDeclaration; + | accessDeclaration + | declarationseq; propertyDefinition: uproperty? (accessSpecifier | accessPattern)? Default? declSpecifierSeq? (memberDeclaratorList | assignmentList)? Semi; diff --git a/UnrealAngelscriptServerCodeExcision/UnrealAngelsciptServerCodeParser.cs b/UnrealAngelscriptServerCodeExcision/UnrealAngelsciptServerCodeParser.cs index d7a847b..8214e77 100644 --- a/UnrealAngelscriptServerCodeExcision/UnrealAngelsciptServerCodeParser.cs +++ b/UnrealAngelscriptServerCodeExcision/UnrealAngelsciptServerCodeParser.cs @@ -2,19 +2,19 @@ namespace UnrealAngelscriptServerCodeExcision { - public class UnrealAngelscriptServerCodeParser : IServerCodeParser - { - UnrealAngelscriptParser _parser; + public class UnrealAngelscriptServerCodeParser : IServerCodeParser + { + readonly UnrealAngelscriptParser _parser; - public UnrealAngelscriptServerCodeParser(Antlr4.Runtime.CommonTokenStream tokenStream) - { - _parser = new UnrealAngelscriptParser(tokenStream); - _parser.AddErrorListener(new ExcisionParserErrorListener()); - } + public UnrealAngelscriptServerCodeParser(Antlr4.Runtime.CommonTokenStream tokenStream) + { + _parser = new UnrealAngelscriptParser(tokenStream); + _parser.AddErrorListener(new ExcisionParserErrorListener()); + } - public Antlr4.Runtime.Tree.IParseTree GetParseTree() - { - return _parser.script(); - } - } + public Antlr4.Runtime.Tree.IParseTree GetParseTree() + { + return _parser.script(); + } + } } diff --git a/UnrealAngelscriptServerCodeExcision/UnrealAngelscriptFunctionVisitor.cs b/UnrealAngelscriptServerCodeExcision/UnrealAngelscriptFunctionVisitor.cs index a1d2b1f..7d2fcc2 100644 --- a/UnrealAngelscriptServerCodeExcision/UnrealAngelscriptFunctionVisitor.cs +++ b/UnrealAngelscriptServerCodeExcision/UnrealAngelscriptFunctionVisitor.cs @@ -1,17 +1,17 @@ namespace UnrealAngelscriptServerCodeExcision { - public class UnrealAngelscriptFunctionVisitor : UnrealAngelscriptSimpleVisitor - { - public UnrealAngelscriptFunctionVisitor(string script) - : base(script) - { - } + public class UnrealAngelscriptFunctionVisitor : UnrealAngelscriptSimpleVisitor + { + public UnrealAngelscriptFunctionVisitor(string script) + : base(script) + { + } - public override UnrealAngelscriptNode VisitFunctionBody(UnrealAngelscriptParser.FunctionBodyContext context) - { - // We want to decorate all function bodies! - DecorateFunctionBody(context); - return base.VisitFunctionBody(context); - } - } + public override UnrealAngelscriptNode VisitFunctionBody(UnrealAngelscriptParser.FunctionBodyContext context) + { + // We want to decorate all function bodies! + DecorateFunctionBody(context); + return base.VisitFunctionBody(context); + } + } } diff --git a/UnrealAngelscriptServerCodeExcision/UnrealAngelscriptServerCodeExcisionLanguage.cs b/UnrealAngelscriptServerCodeExcision/UnrealAngelscriptServerCodeExcisionLanguage.cs index ed188d8..4a46f9c 100644 --- a/UnrealAngelscriptServerCodeExcision/UnrealAngelscriptServerCodeExcisionLanguage.cs +++ b/UnrealAngelscriptServerCodeExcision/UnrealAngelscriptServerCodeExcisionLanguage.cs @@ -1,64 +1,65 @@ -using System.Collections.Generic; using ServerCodeExcisionCommon; +using System.Collections.Generic; namespace UnrealAngelscriptServerCodeExcision { - public class UnrealAngelscriptServerCodeExcisionLanguage : IServerCodeExcisionLanguage - { - private List _angelscriptServerOnlySymbolRegexes = new List - { - @"^System::IsServer\(\)$", - @"^[A-z]+\.HasAuthority\(\)$" - }; - public List ServerOnlySymbolRegexes { get { return _angelscriptServerOnlySymbolRegexes; } } + public class UnrealAngelscriptServerCodeExcisionLanguage : IServerCodeExcisionLanguage + { + private readonly List _angelscriptServerOnlySymbolRegexes = new List + { + @"^System::IsServer\(\)$", + @"^[A-z]+\.HasAuthority\(\)$", + @"^UEventAPI::", + }; + public List ServerOnlySymbolRegexes { get { return _angelscriptServerOnlySymbolRegexes; } } + + private readonly List _angelscriptServerOnlySymbols = new List + { + "hasauthority()", + "server" + }; + public List ServerOnlySymbols { get { return _angelscriptServerOnlySymbols; } } - private List _angelscriptServerOnlySymbols = new List - { - "hasauthority()", - "server" - }; - public List ServerOnlySymbols { get { return _angelscriptServerOnlySymbols; } } + public string ServerPrecompilerSymbol { get { return "WITH_SERVER"; } } + public string ServerScopeStartString { get { return "#ifdef " + ServerPrecompilerSymbol; } } + public string ServerScopeEndString { get { return "#endif // " + ServerPrecompilerSymbol; } } - public string ServerPrecompilerSymbol { get { return "WITH_SERVER"; } } - public string ServerScopeStartString { get { return "\r\n#ifdef " + ServerPrecompilerSymbol; } } - public string ServerScopeEndString { get { return "#endif // " + ServerPrecompilerSymbol + "\r\n"; } } + public Antlr4.Runtime.Lexer CreateLexer(Antlr4.Runtime.AntlrInputStream inputStream) + { + return new UnrealAngelscriptLexer(inputStream); + } - public Antlr4.Runtime.Lexer CreateLexer(Antlr4.Runtime.AntlrInputStream inputStream) - { - return new UnrealAngelscriptLexer(inputStream); - } + public IServerCodeParser CreateParser(Antlr4.Runtime.CommonTokenStream tokenStream) + { + return new UnrealAngelscriptServerCodeParser(tokenStream); + } - public IServerCodeParser CreateParser(Antlr4.Runtime.CommonTokenStream tokenStream) - { - return new UnrealAngelscriptServerCodeParser(tokenStream); - } + public IServerCodeVisitor CreateSimpleVisitor(string code) + { + return new UnrealAngelscriptSimpleVisitor(code); + } - public IServerCodeVisitor CreateSimpleVisitor(string code) - { - return new UnrealAngelscriptSimpleVisitor(code); - } + public IServerCodeVisitor CreateFunctionVisitor(string code) + { + return new UnrealAngelscriptFunctionVisitor(code); + } - public IServerCodeVisitor CreateFunctionVisitor(string code) - { - return new UnrealAngelscriptFunctionVisitor(code); - } + public IServerCodeVisitor CreateSymbolVisitor(string code) + { + return new UnrealAngelscriptSymbolVisitor(code, this); + } - public IServerCodeVisitor CreateSymbolVisitor(string code) - { - return new UnrealAngelscriptSymbolVisitor(code, this); - } - - public bool AnyServerOnlySymbolsInScript(string script) - { - foreach (var serverOnlySymbol in _angelscriptServerOnlySymbols) - { - if (script.ToLower().Contains(serverOnlySymbol)) - { - return true; - } - } + public bool AnyServerOnlySymbolsInScript(string script) + { + foreach (var serverOnlySymbol in _angelscriptServerOnlySymbols) + { + if (script.ToLower().Contains(serverOnlySymbol)) + { + return true; + } + } - return false; - } - } + return false; + } + } } diff --git a/UnrealAngelscriptServerCodeExcision/UnrealAngelscriptSimpleVisitor.cs b/UnrealAngelscriptServerCodeExcision/UnrealAngelscriptSimpleVisitor.cs index d8e3737..d4261fc 100644 --- a/UnrealAngelscriptServerCodeExcision/UnrealAngelscriptSimpleVisitor.cs +++ b/UnrealAngelscriptServerCodeExcision/UnrealAngelscriptSimpleVisitor.cs @@ -1,4 +1,3 @@ -using Antlr4.Runtime; using ServerCodeExcisionCommon; using System; using System.Collections.Generic; @@ -6,303 +5,302 @@ namespace UnrealAngelscriptServerCodeExcision { - public class UnrealAngelscriptNode - { - } - - public class UnrealAngelscriptSimpleVisitor : UnrealAngelscriptParserBaseVisitor, IServerCodeVisitor - { - public List DetectedServerOnlyScopes { get; protected set; } - public Dictionary> ClassStartIdxDummyReferenceData { get; protected set; } - public int TotalNumberOfFunctionCharactersVisited { get; protected set; } - - protected string Script; - - private static int Salt = 0; - - public UnrealAngelscriptSimpleVisitor(string script) - { - ClassStartIdxDummyReferenceData = new Dictionary>(); - DetectedServerOnlyScopes = new List(); - - TotalNumberOfFunctionCharactersVisited = 0; - Script = script; - } - - public void VisitContext(Antlr4.Runtime.Tree.IParseTree context) - { - Visit(context); - } - - public override UnrealAngelscriptNode VisitFunctionBody(UnrealAngelscriptParser.FunctionBodyContext context) - { - var functionStartIndex = ExcisionUtils.FindScriptIndexForCodePoint(Script, context.Start.Line, context.Start.Column) + 1; - var functionEndIndex = ExcisionUtils.FindScriptIndexForCodePoint(Script, context.Stop.Line, context.Stop.Column) + 1; - TotalNumberOfFunctionCharactersVisited += Math.Abs(functionEndIndex - functionStartIndex); - - return VisitChildren(context); - } - - protected ReturnData GetDefaultReturnStatementForScope(Antlr4.Runtime.Tree.IParseTree scopeContext) - { - // If the function has a return type, we must provide a valid replacement in the case the original one is compiled out. - var returnData = new ReturnData(); - - // First figure out the function's return type. - var functionDefinition = ExcisionUtils.FindParentContextOfType(scopeContext); - if (functionDefinition != null && functionDefinition.ChildCount > 1) - { - var returnTypeContext = ExcisionUtils.FindFirstDirectChildOfType(functionDefinition); - - // Now figure out if and what we should replace the return with. - returnData.ReturnType = GetDefaultReturnStatementForReturnType(returnTypeContext, out returnData.DefaultReturnString); - - if (returnData.ReturnType != EReturnType.NoReturn) - { - // Okay, we have a return type. We should check for a final return statement, and gather info about it. - var functionBody = functionDefinition.GetChild(functionDefinition.ChildCount - 1) as UnrealAngelscriptParser.CompoundStatementContext; - if (!IsLastStatementInScopeAReturn(scopeContext, ref returnData.ReturnStatementRun) - && functionBody == scopeContext) - { - // It seems our function has a return value, but doesn't end with a return statement. - // This must mean that all the return statements are in branches of the expression, and we should add our return definition at the end. - - returnData.ReturnStatementRun.StartLine = functionBody.Stop.Line; - returnData.ReturnStatementRun.StartColumn = functionBody.Stop.Column; - returnData.ReturnStatementRun.StopLine = functionBody.Stop.Line; - returnData.ReturnStatementRun.StopColumn = functionBody.Stop.Column; - } - } - } - - return returnData; - } - - protected bool IsLastStatementInScopeAReturn(Antlr4.Runtime.Tree.IParseTree scopeContext, ref StatementRun returnStatementRun) - { - if (scopeContext == null) - { - return false; - } - - var jumpContext = scopeContext as UnrealAngelscriptParser.JumpStatementContext; - if (jumpContext != null && jumpContext.GetChild(0).GetText() == "return") - { - returnStatementRun.StartLine = jumpContext.Start.Line; - returnStatementRun.StartColumn = jumpContext.Start.Column; - returnStatementRun.StopLine = jumpContext.Stop.Line; - returnStatementRun.StopColumn = jumpContext.Stop.Column; - return true; - } - - var compoundStatementContext = scopeContext as UnrealAngelscriptParser.CompoundStatementContext; - if (compoundStatementContext != null) - { - return IsLastStatementInScopeAReturn(compoundStatementContext.GetChild(scopeContext.ChildCount - 2), ref returnStatementRun); - } - - if (scopeContext.ChildCount < 1) - { - return false; - } - - var nextChild = scopeContext.GetChild(scopeContext.ChildCount - 1); - if (nextChild is UnrealAngelscriptParser.SelectionStatementContext) - { - // Disallow entering further branches. - return false; - } - - return IsLastStatementInScopeAReturn(nextChild, ref returnStatementRun); - } - - protected EReturnType GetDefaultReturnStatementForReturnType(UnrealAngelscriptParser.DeclSpecifierSeqContext returnTypeContext, out string defaultReturnStatement) - { - defaultReturnStatement = ""; - - if (returnTypeContext == null || returnTypeContext.GetText() == "void") - { - // Void return types means we don't have to do anything. - return EReturnType.NoReturn; - } - - // First, we need to figure out the type text. This is the full type without qualifiers. - string typeString = ""; - bool returnTypeFound = false; - - var asGenericContext = GetFirstChildOfType(returnTypeContext); - var simpleTypeContext = GetFirstChildOfType(returnTypeContext); - if (asGenericContext != null) - { - // It is some type of generic, we should probably just try to construct it. - typeString = asGenericContext.GetText().Replace("const", "const "); - defaultReturnStatement = string.Format("return {0}();", typeString); - returnTypeFound = true; - } - else if (simpleTypeContext != null) - { - // Could this simple type even be a class type..? - var classTypeContext = GetFirstChildOfType(simpleTypeContext, true); - if (classTypeContext != null) - { - if (classTypeContext.GetText().StartsWith("F")) - { - // Struct. Construct in place. - typeString = simpleTypeContext.GetText(); - defaultReturnStatement = string.Format("return {0}();", typeString); - } - else if (classTypeContext.GetText().StartsWith("E")) - { - // Enum. Force cast. - typeString = simpleTypeContext.GetText(); - defaultReturnStatement = string.Format("return {0}(0);", typeString); - } - else - { - // Class type, return null - defaultReturnStatement = "return nullptr;"; - } - } - else - { - // No, it was just a simple type, we'll use default values! - switch (simpleTypeContext.GetText()) - { - case "float": { defaultReturnStatement = "return 0.0f;"; break; } - case "double": { defaultReturnStatement = "return 0.0;"; break; } - case "bool": { defaultReturnStatement = "return false;"; break; } - - default: - { - // All kinds of different ints :) - defaultReturnStatement = "return 0;"; - break; - } - } - } - - returnTypeFound = true; - } - - // If the return type is some type of reference, we cannot replace it with a stack variable. - // This means we need to create a new dummy variable, and we also need to return a reference to it instead of something else. - if (returnTypeFound && returnTypeContext.ChildCount > 0 && - (returnTypeContext.GetChild(returnTypeContext.ChildCount - 1).GetText() == "&")) - { - string dummyReferenceVariableName = typeString.Replace("::", "").Replace(",", "").Replace("<", "").Replace(">", "") + "ReferenceDummy" + Salt++; - string dummyReferenceVariable = string.Format("{0} {1};", typeString, dummyReferenceVariableName); - - // For reference variables, we must register dummy variables to make sure something with longer lifetime scope can be returned. - // I'd really like to just use a static variable here, but since AS doesn't support that, things get messy. - int classStartIdx = -1; - var classSpecifierContext = ExcisionUtils.FindParentContextOfType(returnTypeContext); - if (classSpecifierContext != null && classSpecifierContext.ChildCount > 2) - { - var classMemberSpec = classSpecifierContext.GetChild(2) as UnrealAngelscriptParser.MemberSpecificationContext; - if (classMemberSpec != null) - { - classStartIdx = ExcisionUtils.FindScriptIndexForCodePoint(Script, classMemberSpec.Start.Line, 0); - } - } - - if (classStartIdx < 0) - { - // Reference return dected in non-class. Just leave this alone for now. - return EReturnType.RootScopeReferenceReturn; - } - - HashSet dummyVarSet = null; - if (ClassStartIdxDummyReferenceData.ContainsKey(classStartIdx)) - { - dummyVarSet = ClassStartIdxDummyReferenceData[classStartIdx]; - } - else - { - dummyVarSet = new HashSet(); - } - - dummyVarSet.Add(dummyReferenceVariable); - ClassStartIdxDummyReferenceData[classStartIdx] = dummyVarSet; - - defaultReturnStatement = string.Format("return {0};", dummyReferenceVariableName); - return EReturnType.ReferenceReturn; - } - - return returnTypeFound ? EReturnType.ReplacedReturn : EReturnType.NoReturn; - } - - protected Antlr4.Runtime.Tree.IParseTree GetFirstChildOfType(Antlr4.Runtime.Tree.IParseTree specifierSequence, bool searchReverse = false) - where T : class - { - if ((specifierSequence as T) != null) - { - return specifierSequence; - } - - if (searchReverse) - { - for (int childIdx = specifierSequence.ChildCount - 1; childIdx >= 0; childIdx--) - { - var childResult = GetFirstChildOfType(specifierSequence.GetChild(childIdx), searchReverse); - if (childResult != null) - { - return childResult; - } - } - } - else - { - for (int childIdx = 0; childIdx < specifierSequence.ChildCount; childIdx++) - { - var childResult = GetFirstChildOfType(specifierSequence.GetChild(childIdx), searchReverse); - if (childResult != null) - { - return childResult; - } - } - } - - return null; - } - - protected string BuildIndentationForColumnCount(int nrCols) - { - var indentation = new StringBuilder(""); - for (int indentIdx = 0; indentIdx < nrCols; indentIdx++) - { - indentation.Append("\t"); - } - - return indentation.ToString(); - } - - protected void DecorateFunctionBody(UnrealAngelscriptParser.FunctionBodyContext context) - { - if (context == null) - { - return; - } - - // If there is a return statement at the end, we must replace it with a suitable replacement, or code will stop compiling. - var returnData = GetDefaultReturnStatementForScope(context); - - ServerOnlyScopeData newData = new ServerOnlyScopeData( - ExcisionUtils.FindScriptIndexForCodePoint(Script, context.Start.Line, context.Start.Column) + 1, - ExcisionUtils.FindScriptIndexForCodePoint(Script, context.Stop.Line, 0)); - - if (returnData.ReturnType != EReturnType.NoReturn) - { - // We want to be one step inside the scope! - string scopeIndentation = BuildIndentationForColumnCount(context.Start.Column + 1); - newData.Opt_ElseContent = string.Format("#else\r\n{0}{1}\r\n", scopeIndentation, returnData.DefaultReturnString); - } - - // Don't detect scopes that are just one line. - if (context.Start.Line != context.Stop.Line - && returnData.ReturnType != EReturnType.RootScopeReferenceReturn) - { - DetectedServerOnlyScopes.Add(newData); - } - } - } + public class UnrealAngelscriptNode + { + } + + public class UnrealAngelscriptSimpleVisitor : UnrealAngelscriptParserBaseVisitor, IServerCodeVisitor + { + public List DetectedServerOnlyScopes { get; protected set; } + public Dictionary> ClassStartIdxDummyReferenceData { get; protected set; } + public int TotalNumberOfFunctionCharactersVisited { get; protected set; } + + protected string Script; + + private static int Salt = 0; + + public UnrealAngelscriptSimpleVisitor(string script) + { + ClassStartIdxDummyReferenceData = new Dictionary>(); + DetectedServerOnlyScopes = new List(); + + TotalNumberOfFunctionCharactersVisited = 0; + Script = script; + } + + public void VisitContext(Antlr4.Runtime.Tree.IParseTree context) + { + Visit(context); + } + + public override UnrealAngelscriptNode VisitFunctionBody(UnrealAngelscriptParser.FunctionBodyContext context) + { + var functionStartIndex = ExcisionUtils.FindScriptIndexForCodePoint(Script, context.Start.Line, context.Start.Column) + 1; + var functionEndIndex = ExcisionUtils.FindScriptIndexForCodePoint(Script, context.Stop.Line, context.Stop.Column) + 1; + TotalNumberOfFunctionCharactersVisited += Math.Abs(functionEndIndex - functionStartIndex); + + return VisitChildren(context); + } + + protected ReturnData GetDefaultReturnStatementForScope(Antlr4.Runtime.Tree.IParseTree scopeContext) + { + // If the function has a return type, we must provide a valid replacement in the case the original one is compiled out. + var returnData = new ReturnData(); + + // First figure out the function's return type. + var functionDefinition = ExcisionUtils.FindParentContextOfType(scopeContext); + if (functionDefinition != null && functionDefinition.ChildCount > 1) + { + var returnTypeContext = ExcisionUtils.FindFirstDirectChildOfType(functionDefinition); + + // Now figure out if and what we should replace the return with. + returnData.ReturnType = GetDefaultReturnStatementForReturnType(returnTypeContext, out returnData.DefaultReturnString); + + if (returnData.ReturnType != EReturnType.NoReturn) + { + // Okay, we have a return type. We should check for a final return statement, and gather info about it. + var functionBody = functionDefinition.GetChild(functionDefinition.ChildCount - 1) as UnrealAngelscriptParser.CompoundStatementContext; + if (!IsLastStatementInScopeAReturn(scopeContext, ref returnData.ReturnStatementRun) + && functionBody == scopeContext) + { + // It seems our function has a return value, but doesn't end with a return statement. + // This must mean that all the return statements are in branches of the expression, and we should add our return definition at the end. + + returnData.ReturnStatementRun.StartLine = functionBody.Stop.Line; + returnData.ReturnStatementRun.StartColumn = functionBody.Stop.Column; + returnData.ReturnStatementRun.StopLine = functionBody.Stop.Line; + returnData.ReturnStatementRun.StopColumn = functionBody.Stop.Column; + } + } + } + + return returnData; + } + + protected bool IsLastStatementInScopeAReturn(Antlr4.Runtime.Tree.IParseTree scopeContext, ref StatementRun returnStatementRun) + { + if (scopeContext == null) + { + return false; + } + + var jumpContext = scopeContext as UnrealAngelscriptParser.JumpStatementContext; + if (jumpContext != null && jumpContext.GetChild(0).GetText() == "return") + { + returnStatementRun.StartLine = jumpContext.Start.Line; + returnStatementRun.StartColumn = jumpContext.Start.Column; + returnStatementRun.StopLine = jumpContext.Stop.Line; + returnStatementRun.StopColumn = jumpContext.Stop.Column; + return true; + } + + var compoundStatementContext = scopeContext as UnrealAngelscriptParser.CompoundStatementContext; + if (compoundStatementContext != null) + { + return IsLastStatementInScopeAReturn(compoundStatementContext.GetChild(scopeContext.ChildCount - 2), ref returnStatementRun); + } + + if (scopeContext.ChildCount < 1) + { + return false; + } + + var nextChild = scopeContext.GetChild(scopeContext.ChildCount - 1); + if (nextChild is UnrealAngelscriptParser.SelectionStatementContext) + { + // Disallow entering further branches. + return false; + } + + return IsLastStatementInScopeAReturn(nextChild, ref returnStatementRun); + } + + protected EReturnType GetDefaultReturnStatementForReturnType(UnrealAngelscriptParser.DeclSpecifierSeqContext returnTypeContext, out string defaultReturnStatement) + { + defaultReturnStatement = ""; + + if (returnTypeContext == null || returnTypeContext.GetText() == "void") + { + // Void return types means we don't have to do anything. + return EReturnType.NoReturn; + } + + // First, we need to figure out the type text. This is the full type without qualifiers. + string typeString = ""; + bool returnTypeFound = false; + + var asGenericContext = GetFirstChildOfType(returnTypeContext); + var simpleTypeContext = GetFirstChildOfType(returnTypeContext); + if (asGenericContext != null) + { + // It is some type of generic, we should probably just try to construct it. + typeString = asGenericContext.GetText().Replace("const", "const "); + defaultReturnStatement = string.Format("return {0}();", typeString); + returnTypeFound = true; + } + else if (simpleTypeContext != null) + { + // Could this simple type even be a class type..? + var classTypeContext = GetFirstChildOfType(simpleTypeContext, true); + if (classTypeContext != null) + { + if (classTypeContext.GetText().StartsWith("F")) + { + // Struct. Construct in place. + typeString = simpleTypeContext.GetText(); + defaultReturnStatement = string.Format("return {0}();", typeString); + } + else if (classTypeContext.GetText().StartsWith("E")) + { + // Enum. Force cast. + typeString = simpleTypeContext.GetText(); + defaultReturnStatement = string.Format("return {0}(0);", typeString); + } + else + { + // Class type, return null + defaultReturnStatement = "return nullptr;"; + } + } + else + { + // No, it was just a simple type, we'll use default values! + switch (simpleTypeContext.GetText()) + { + case "float": { defaultReturnStatement = "return 0.0f;"; break; } + case "double": { defaultReturnStatement = "return 0.0;"; break; } + case "bool": { defaultReturnStatement = "return false;"; break; } + + default: + { + // All kinds of different ints :) + defaultReturnStatement = "return 0;"; + break; + } + } + } + + returnTypeFound = true; + } + + // If the return type is some type of reference, we cannot replace it with a stack variable. + // This means we need to create a new dummy variable, and we also need to return a reference to it instead of something else. + if (returnTypeFound && returnTypeContext.ChildCount > 0 && + (returnTypeContext.GetChild(returnTypeContext.ChildCount - 1).GetText() == "&")) + { + string dummyReferenceVariableName = typeString.Replace("::", "").Replace(",", "").Replace("<", "").Replace(">", "") + "ReferenceDummy" + Salt++; + string dummyReferenceVariable = string.Format("{0} {1};", typeString, dummyReferenceVariableName); + + // For reference variables, we must register dummy variables to make sure something with longer lifetime scope can be returned. + // I'd really like to just use a static variable here, but since AS doesn't support that, things get messy. + int classStartIdx = -1; + var classSpecifierContext = ExcisionUtils.FindParentContextOfType(returnTypeContext); + if (classSpecifierContext != null && classSpecifierContext.ChildCount > 2) + { + var classMemberSpec = classSpecifierContext.GetChild(2) as UnrealAngelscriptParser.MemberSpecificationContext; + if (classMemberSpec != null) + { + classStartIdx = ExcisionUtils.FindScriptIndexForCodePoint(Script, classMemberSpec.Start.Line, 0); + } + } + + if (classStartIdx < 0) + { + // Reference return dected in non-class. Just leave this alone for now. + return EReturnType.RootScopeReferenceReturn; + } + + HashSet dummyVarSet = null; + if (ClassStartIdxDummyReferenceData.ContainsKey(classStartIdx)) + { + dummyVarSet = ClassStartIdxDummyReferenceData[classStartIdx]; + } + else + { + dummyVarSet = new HashSet(); + } + + dummyVarSet.Add(dummyReferenceVariable); + ClassStartIdxDummyReferenceData[classStartIdx] = dummyVarSet; + + defaultReturnStatement = string.Format("return {0};", dummyReferenceVariableName); + return EReturnType.ReferenceReturn; + } + + return returnTypeFound ? EReturnType.ReplacedReturn : EReturnType.NoReturn; + } + + protected Antlr4.Runtime.Tree.IParseTree GetFirstChildOfType(Antlr4.Runtime.Tree.IParseTree specifierSequence, bool searchReverse = false) + where T : class + { + if ((specifierSequence as T) != null) + { + return specifierSequence; + } + + if (searchReverse) + { + for (int childIdx = specifierSequence.ChildCount - 1; childIdx >= 0; childIdx--) + { + var childResult = GetFirstChildOfType(specifierSequence.GetChild(childIdx), searchReverse); + if (childResult != null) + { + return childResult; + } + } + } + else + { + for (int childIdx = 0; childIdx < specifierSequence.ChildCount; childIdx++) + { + var childResult = GetFirstChildOfType(specifierSequence.GetChild(childIdx), searchReverse); + if (childResult != null) + { + return childResult; + } + } + } + + return null; + } + + protected string BuildIndentationForColumnCount(int nrCols) + { + var indentation = new StringBuilder(""); + for (int indentIdx = 0; indentIdx < nrCols; indentIdx++) + { + indentation.Append("\t"); + } + + return indentation.ToString(); + } + + protected void DecorateFunctionBody(UnrealAngelscriptParser.FunctionBodyContext context) + { + if (context == null) + { + return; + } + + + // If there is a return statement at the end, we must replace it with a suitable replacement, or code will stop compiling. + var returnData = GetDefaultReturnStatementForScope(context); + + ServerOnlyScopeData newData = new ServerOnlyScopeData("F", context.Start.Line + 1, context.Stop.Line); + + if (returnData.ReturnType != EReturnType.NoReturn) + { + // We want to be one step inside the scope! + string scopeIndentation = BuildIndentationForColumnCount(context.Start.Column + 1); + newData.Opt_ElseContent = string.Format("#else\r\n{0}{1}\r\n", scopeIndentation, returnData.DefaultReturnString); + } + + // Don't detect scopes that are just one line. + if (context.Start.Line != context.Stop.Line + && returnData.ReturnType != EReturnType.RootScopeReferenceReturn) + { + DetectedServerOnlyScopes.Add(newData); + } + } + } } diff --git a/UnrealAngelscriptServerCodeExcision/UnrealAngelscriptSymbolVisitor.cs b/UnrealAngelscriptServerCodeExcision/UnrealAngelscriptSymbolVisitor.cs index 8fa1603..b778106 100644 --- a/UnrealAngelscriptServerCodeExcision/UnrealAngelscriptSymbolVisitor.cs +++ b/UnrealAngelscriptServerCodeExcision/UnrealAngelscriptSymbolVisitor.cs @@ -1,13 +1,13 @@ +using Antlr4.Runtime.Misc; using ServerCodeExcisionCommon; using System.Collections.Generic; -using System.Text; using System.Text.RegularExpressions; namespace UnrealAngelscriptServerCodeExcision { public class UnrealAngelscriptSymbolVisitor : UnrealAngelscriptSimpleVisitor { - private IServerCodeExcisionLanguage _language; + private readonly IServerCodeExcisionLanguage _language; public UnrealAngelscriptSymbolVisitor(string script, IServerCodeExcisionLanguage language) : base(script) @@ -43,7 +43,7 @@ public override UnrealAngelscriptNode VisitFunctionDefinition(UnrealAngelscriptP } } } - + // Check to see if function name ends with _Server for (int childIdx = 0; childIdx < context.ChildCount; childIdx++) { @@ -80,25 +80,25 @@ public override UnrealAngelscriptNode VisitSelectionStatement(UnrealAngelscriptP switch (IsExpressionServerOnly(conditionExpression)) { case EExpressionType.ServerOnly: - { - // Index 4 is the control scope for ifs - AddDetectedScopeFromSelectionChild(context, 4); - break; - } + { + // Index 4 is the control scope for ifs + AddDetectedScopeFromSelectionChild(context, 4); + break; + } case EExpressionType.EverythingAfterBranchIsServerOnly: - { - // We want to inject right after the if-branch child, and continue all the way to the end of the parent scope. - AddParentScopePostIfScope(context); - break; - } + { + // We want to inject right after the if-branch child, and continue all the way to the end of the parent scope. + AddParentScopePostIfScope(context); + break; + } case EExpressionType.ElseIsServerOnly: - { - // Index 5 is the control scope for elses - AddDetectedScopeFromSelectionChild(context, 6); - break; - } + { + // Index 5 is the control scope for elses + AddDetectedScopeFromSelectionChild(context, 6); + break; + } default: break; @@ -119,9 +119,7 @@ private void AddDetectedScopeFromSelectionChild(UnrealAngelscriptParser.Selectio // We want to move in one step, since our reference scope is the lower one. var returnData = GetDefaultReturnStatementForScope(selectionScope); - ServerOnlyScopeData newData = new ServerOnlyScopeData( - ExcisionUtils.FindScriptIndexForCodePoint(Script, selectionScope.Start.Line, selectionScope.Start.Column) + 1, - ExcisionUtils.FindScriptIndexForCodePoint(Script, selectionScope.Stop.Line, 0)); + ServerOnlyScopeData newData = new ServerOnlyScopeData("A", selectionScope.Start.Line + 1, selectionScope.Stop.Line); if (returnData.ReturnType != EReturnType.NoReturn) { @@ -141,9 +139,7 @@ private void AddDetectedScopeFromSelectionChild(UnrealAngelscriptParser.Selectio var oneLineScope = context.GetChild(childIdx) as UnrealAngelscriptParser.StatementContext; if (oneLineScope != null) { - ServerOnlyScopeData newData = new ServerOnlyScopeData( - MoveOneLine(ExcisionUtils.FindScriptIndexForCodePoint(Script, oneLineScope.Start.Line, 0), false), - MoveOneLine(ExcisionUtils.FindScriptIndexForCodePoint(Script, oneLineScope.Stop.Line, oneLineScope.Stop.Column) + 1, true)); + ServerOnlyScopeData newData = new ServerOnlyScopeData("B", oneLineScope.Start.Line, oneLineScope.Stop.Line); // If there is a return statement at the end, we must replace it with a suitable replacement, or code will stop compiling. // For one-liners, we actually remove the entire scope, which means we must replace it completely. @@ -199,9 +195,7 @@ private void AddParentScopePostIfScope(UnrealAngelscriptParser.SelectionStatemen var parentScope = ExcisionUtils.FindParentContextOfType(context); if (parentScope != null && ifScopeStopLine > 0 && ifScopeStopColumn > 0) { - ServerOnlyScopeData newData = new ServerOnlyScopeData( - ExcisionUtils.FindScriptIndexForCodePoint(Script, ifScopeStopLine, ifScopeStopColumn) + 1, - ExcisionUtils.FindScriptIndexForCodePoint(Script, parentScope.Stop.Line, 0)); + ServerOnlyScopeData newData = new ServerOnlyScopeData("C", ifScopeStopLine + 1, parentScope.Stop.Line); // If there is a return statement at the end, we must replace it with a suitable replacement, or code will stop compiling. var returnData = GetDefaultReturnStatementForScope(parentScope); @@ -242,6 +236,32 @@ private int MoveOneLine(int curScriptIdx, bool forward) return curScriptIdx; } + private bool IsAlreadyInServerScope(Antlr4.Runtime.Tree.IParseTree tree) + { + var parentScope = ExcisionUtils.FindParentContextOfType(tree); + + + return false; + } + + public override UnrealAngelscriptNode VisitStatement([NotNull] UnrealAngelscriptParser.StatementContext context) + { + if (IsAlreadyInServerScope(context)) + { + return base.VisitStatement(context); + } + + var text = context.GetText().Trim(); + if (MatchesAnyRegex(text, _language.ServerOnlySymbolRegexes)) + { + ServerOnlyScopeData newData = new ServerOnlyScopeData("D", context.Start.Line, context.Stop.Line + 1); + DetectedServerOnlyScopes.Add(newData); + } + + return base.VisitStatement(context); + } + + public override UnrealAngelscriptNode VisitPostfixExpression(UnrealAngelscriptParser.PostfixExpressionContext context) { // In assert statements, we want to find the compound statement we ourselves belong to, and inject macros after ourselves. @@ -259,13 +279,11 @@ public override UnrealAngelscriptNode VisitPostfixExpression(UnrealAngelscriptPa // If there is a return statement at the end, we must replace it with a suitable replacement, or code will stop compiling. var returnData = GetDefaultReturnStatementForScope(parentScope); - ServerOnlyScopeData newData = new ServerOnlyScopeData( - ExcisionUtils.FindScriptIndexForCodePoint(Script, simpleDeclaration.Stop.Line, simpleDeclaration.Stop.Column) + 1, - ExcisionUtils.FindScriptIndexForCodePoint(Script, parentScope.Stop.Line, 0)); + ServerOnlyScopeData newData = new ServerOnlyScopeData("E", simpleDeclaration.Stop.Line + 1, parentScope.Stop.Line); // We need to correct the start index to skip all the possible empty characters/new lines, // if not we can miss the detection of a manually placed #ifdef - newData.StartIndex = ExcisionUtils.ShrinkServerScope(Script, newData.StartIndex, newData.StopIndex); + //newData.StartIndex = ExcisionUtils.ShrinkServerScope(Script, newData.StartIndex, newData.StopIndex); if (returnData.ReturnType != EReturnType.NoReturn) { @@ -351,14 +369,14 @@ private EExpressionType IsExpressionServerOnly(Antlr4.Runtime.Tree.IParseTree ex if (isExpressionSimple && selectionContext != null && selectionContext.ChildCount > 4) { var dummyReturnStatement = new StatementRun(); - return IsLastStatementInScopeAReturn(selectionContext.GetChild(4), ref dummyReturnStatement) - ? EExpressionType.EverythingAfterBranchIsServerOnly + return IsLastStatementInScopeAReturn(selectionContext.GetChild(4), ref dummyReturnStatement) + ? EExpressionType.EverythingAfterBranchIsServerOnly : EExpressionType.ElseIsServerOnly; } } else { - return EExpressionType.ServerOnly; + return EExpressionType.ServerOnly; } } @@ -393,7 +411,7 @@ private Antlr4.Runtime.Tree.IParseTree FindServerOnlySymbolTree(Antlr4.Runtime.T } var maybeChild = FindServerOnlySymbolTree(childTree); - if(maybeChild != null) + if (maybeChild != null) { return maybeChild; } @@ -403,7 +421,7 @@ private Antlr4.Runtime.Tree.IParseTree FindServerOnlySymbolTree(Antlr4.Runtime.T } private bool MatchesAnyRegex(string expression, List Regexes) - { + { foreach (var serverOnlySymbolRegex in Regexes) { if (Regex.IsMatch(expression, serverOnlySymbolRegex))