diff --git a/ServerCodeExciser/ServerCodeExcisionProcessor.cs b/ServerCodeExciser/ServerCodeExcisionProcessor.cs index c2bced6..dddbc48 100644 --- a/ServerCodeExciser/ServerCodeExcisionProcessor.cs +++ b/ServerCodeExciser/ServerCodeExcisionProcessor.cs @@ -1,10 +1,12 @@ +using Antlr4.Runtime; +using ServerCodeExcisionCommon; using System; +using System.Collections; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using System.Text.RegularExpressions; -using Antlr4.Runtime; -using ServerCodeExcisionCommon; namespace ServerCodeExcision { @@ -236,19 +238,25 @@ private ExcisionStats ProcessCodeFile(string fileName, string inputPath, EExcisi stats.TotalNrCharacters = visitor.TotalNumberOfFunctionCharactersVisited; } - // First process all server only scopes. + // Determine if there are any existing preprocessor server-code exclusions in the source file. + var detectedPreprocessorServerOnlyScopes = FindPreprocessorGuards(commonTokenStream) + .Where(x => x.Directive.Contains(excisionLanguage.ServerScopeStartString, StringComparison.Ordinal)); + + // Process scopes we've evaluated must be server only. foreach (ServerOnlyScopeData currentScope in visitor.DetectedServerOnlyScopes) { - if (currentScope.StartIndex == -1 - || currentScope.StopIndex == -1 - || InjectedMacroAlreadyExistsAtLocation(answerText, currentScope.StartIndex, true, true, excisionLanguage.ServerScopeStartString) - || InjectedMacroAlreadyExistsAtLocation(answerText, currentScope.StartIndex, false, false, excisionLanguage.ServerScopeStartString) - || InjectedMacroAlreadyExistsAtLocation(answerText, currentScope.StopIndex, false, false, excisionLanguage.ServerScopeEndString)) + if (currentScope.StartIndex == -1 || currentScope.StopIndex == -1) { continue; } - // If there are already injected macros where we want to go, we should skip injecting. + // Skip if there's already a server-code exclusion for the scope. (We don't want have duplicate guards.) + var (StartIndex, StopIndex) = TrimWhitespace(script, currentScope); + if (detectedPreprocessorServerOnlyScopes.Any(x => StartIndex >= x.StartIndex && StopIndex <= x.StopIndex)) + { + continue; // We're inside an existing scope. + } + 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, "\r\n" + excisionLanguage.ServerScopeStartString)); serverCodeInjections.Add(new KeyValuePair(currentScope.StopIndex, currentScope.Opt_ElseContent + excisionLanguage.ServerScopeEndString + "\r\n")); @@ -328,6 +336,79 @@ private static bool IsWhitespace(char c) return c == ' ' || c == '\t' || c == '\r' || c == '\n'; } + /// + /// Resize a scope range by excluding whitespace characters. + /// + private static (int StartIndex, int StopIndex) TrimWhitespace(string script, ServerOnlyScopeData scope) + { + var (startIndex, stopIndex) = (scope.StartIndex, scope.StopIndex); + + while (IsWhitespace(script[startIndex])) + { + startIndex++; + } + + while (IsWhitespace(script[stopIndex])) + { + stopIndex--; + } + + return (startIndex, stopIndex); + } + + private static List<(string Directive, int StartIndex, int StopIndex)> FindPreprocessorGuards(BufferedTokenStream tokenStream) + { + var preprocessorDirectives = tokenStream + .GetTokens() + .Where(t => t.Channel == UnrealAngelscriptLexer.PREPROCESSOR_CHANNEL) + .Where(t => t.Type == UnrealAngelscriptLexer.Directive) + .ToList(); + + var preprocessorGuards = new List<(string Directive, int StartIndex, int StopIndex)>(); + var ifStack = new Stack(); + + foreach (var token in preprocessorDirectives) + { + switch (token.Text) + { + case var t when t.StartsWith("#if", StringComparison.Ordinal): // #if, #ifdef, #ifndef + ifStack.Push(token); + break; + + case var t when t.StartsWith("#elif", StringComparison.Ordinal): // #elif, #elifdef, #elifndef + { + if (ifStack.TryPop(out var removed)) + { + preprocessorGuards.Add((removed.Text, removed.StartIndex, token.StopIndex)); + } + ifStack.Push(token); + } + break; + + case var t when t.StartsWith("#else", StringComparison.Ordinal): + { + if (ifStack.TryPop(out var removed)) + { + preprocessorGuards.Add((removed.Text, removed.StartIndex, token.StopIndex)); + } + ifStack.Push(token); + } + break; + + case var t when t.StartsWith("#endif", StringComparison.Ordinal): + { + if (ifStack.TryPop(out var removed)) + { + preprocessorGuards.Add((removed.Text, removed.StartIndex, token.StopIndex)); + } + } + break; + } + } + + return preprocessorGuards; + } + private bool InjectedMacroAlreadyExistsAtLocation(StringBuilder script, int index, bool lookAhead, bool ignoreWhitespace, string macro) { if (lookAhead) diff --git a/ServerCodeExciserTest/Problems/Common/AlreadyHasGuardsTest.common b/ServerCodeExciserTest/Problems/Common/AlreadyHasGuardsTest.common index ce3e832..45810fb 100644 --- a/ServerCodeExciserTest/Problems/Common/AlreadyHasGuardsTest.common +++ b/ServerCodeExciserTest/Problems/Common/AlreadyHasGuardsTest.common @@ -2,6 +2,10 @@ class UAlreadyHasGuardsTest { void Test() { +// Commented out block. +//#ifdef WITH_SERVER +//#endif + int SomethingBefore = 0; SomethingBefore++; @@ -16,4 +20,35 @@ class UAlreadyHasGuardsTest int ButNotThis = 0; ButNotThis++; } + + void Test2() + { +#ifdef WITH_SERVER + check(System::IsServer()); + int ThisMustBeGuarded = 0; + ThisMustBeGuarded++; +#endif // WITH_SERVER + } + + void Test3() + { + check(System::IsServer()); + +#ifdef WITH_SERVER + int ThisMustBeGuarded = 0; + ThisMustBeGuarded++; +#endif + } + + void Test4() + { + check(System::IsServer()); +#ifdef WITH_SERVER + +#if !RELEASE + int ThisMustBeGuarded = 0; + ThisMustBeGuarded++; +#endif +#endif + } }; diff --git a/ServerCodeExciserTest/Problems/Common/AlreadyHasGuardsTest.common.solution b/ServerCodeExciserTest/Problems/Common/AlreadyHasGuardsTest.common.solution index ce3e832..45810fb 100644 --- a/ServerCodeExciserTest/Problems/Common/AlreadyHasGuardsTest.common.solution +++ b/ServerCodeExciserTest/Problems/Common/AlreadyHasGuardsTest.common.solution @@ -2,6 +2,10 @@ class UAlreadyHasGuardsTest { void Test() { +// Commented out block. +//#ifdef WITH_SERVER +//#endif + int SomethingBefore = 0; SomethingBefore++; @@ -16,4 +20,35 @@ class UAlreadyHasGuardsTest int ButNotThis = 0; ButNotThis++; } + + void Test2() + { +#ifdef WITH_SERVER + check(System::IsServer()); + int ThisMustBeGuarded = 0; + ThisMustBeGuarded++; +#endif // WITH_SERVER + } + + void Test3() + { + check(System::IsServer()); + +#ifdef WITH_SERVER + int ThisMustBeGuarded = 0; + ThisMustBeGuarded++; +#endif + } + + void Test4() + { + check(System::IsServer()); +#ifdef WITH_SERVER + +#if !RELEASE + int ThisMustBeGuarded = 0; + ThisMustBeGuarded++; +#endif +#endif + } }; diff --git a/UnrealAngelscriptParser/Grammar/UnrealAngelscriptLexer.g4 b/UnrealAngelscriptParser/Grammar/UnrealAngelscriptLexer.g4 index 4a6357a..91ed24e 100644 --- a/UnrealAngelscriptParser/Grammar/UnrealAngelscriptLexer.g4 +++ b/UnrealAngelscriptParser/Grammar/UnrealAngelscriptLexer.g4 @@ -5,6 +5,8 @@ lexer grammar UnrealAngelscriptLexer; +channels { PREPROCESSOR_CHANNEL } + IntegerLiteral: DecimalLiteral Integersuffix? | OctalLiteral Integersuffix? @@ -32,6 +34,10 @@ UserDefinedLiteral: | UserDefinedStringLiteral | UserDefinedCharacterLiteral; +MultiLineMacro: '#' (~[\n]*? '\\' '\r'? '\n')+ ~ [\n]+ -> channel (PREPROCESSOR_CHANNEL); + +Directive: '#' ~ [\r\n]* -> channel (PREPROCESSOR_CHANNEL); + /* Angelscript reserved keywords https://www.angelcode.com/angelscript/sdk/docs/manual/doc_reserved_keywords.html @@ -39,6 +45,8 @@ UserDefinedLiteral: Cast: 'cast'; +From: 'from'; + Import: 'import'; Int: 'int'; @@ -53,6 +61,8 @@ Int64: 'int64'; Mixin: 'mixin'; +Out: 'out'; + Property: 'property'; UInt: 'uint'; @@ -379,7 +389,3 @@ Newline: ('\r' '\n'? | '\n') -> skip; BlockComment: '/*' .*? '*/' -> skip; LineComment: '//' ~ [\r\n]* -> skip; - -PreprocessorBranchRemoval: '#else' .*? '#endif' -> skip; - -Preprocessor: ('#if' | '#ifdef' | '#else' | '#endif') ~ [\r\n]* -> skip;