Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions ServerCodeExciser/PreprocessorParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Antlr4.Runtime;
using ServerCodeExcisionCommon;

namespace ServerCodeExciser
{
public class PreprocessorParser
{
public static List<PreprocessorScope> Parse(BufferedTokenStream tokenStream)
{
var directives = tokenStream
.GetTokens()
.Where(t => t.Channel == UnrealAngelscriptLexer.PREPROCESSOR_CHANNEL)
.Where(t => t.Type == UnrealAngelscriptLexer.Directive)
.OrderBy(t => t.StartIndex)
.ToList();

var rootScopes = new List<PreprocessorScope>();
var ifStack = new Stack<PreprocessorScope>();

foreach (var token in directives)
{
switch (token.Text)
{
case var t when t.StartsWith("#if", StringComparison.Ordinal): // #if, #ifdef, #ifndef
{
var scope = CreateScope(token);
if (ifStack.TryPeek(out var parent))
{
parent.Children.Add(scope);
}
else
{
rootScopes.Add(scope);
}
ifStack.Push(scope);
}
break;

case var t when t.StartsWith("#elif", StringComparison.Ordinal) || t.StartsWith("#else", StringComparison.Ordinal): // #elif, #elifdef, #elifndef, #else
{
var scope = CreateScope(token);
if (ifStack.TryPeek(out var parent))
{
parent.Children.Add(scope);
// adjust #if / #elif scope for closing bounds.
parent.Span = new SourceSpan
{
Start = parent.Span.Start,
StartIndex = parent.Span.StartIndex,
End = new SourcePosition(token.Line, token.Column),
EndIndex = token.StartIndex,
};
}
}
break;

case var t when t.StartsWith("#endif", StringComparison.Ordinal):
{
if (ifStack.TryPop(out var parent))
{
// close parent (#if) to the range of the entire block.
parent.Span = new SourceSpan
{
Start = parent.Span.Start,
StartIndex = parent.Span.StartIndex,
End = new SourcePosition(token.Line, token.Column),
EndIndex = token.StopIndex,
};
}
}
break;
}
}

return rootScopes;
}

private static PreprocessorScope CreateScope(IToken token)
{
return new PreprocessorScope(
token.Text,
new SourceSpan(
token.Line,
token.Column,
token.Line,
token.Column,
token.StartIndex,
token.StopIndex
)
);
}
}
}
20 changes: 20 additions & 0 deletions ServerCodeExciser/PreprocessorScope.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Collections.Generic;
using ServerCodeExcisionCommon;

namespace ServerCodeExciser
{
public sealed class PreprocessorScope
{
public PreprocessorScope(string directive, SourceSpan span)
{
Directive = directive;
Span = span;
}

public string Directive { get; set; }

public SourceSpan Span { get; set; }

public List<PreprocessorScope> Children { get; set; } = new();
}
}
63 changes: 13 additions & 50 deletions ServerCodeExciser/ServerCodeExcisionProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -241,8 +241,12 @@ private ExcisionStats ProcessCodeFile(string fileName, string inputPath, EExcisi
}

// 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));
var preprocessorScopes = PreprocessorParser.Parse(commonTokenStream);
var detectedPreprocessorServerOnlyScopes = new List<PreprocessorScope>();
FindPreprocessorScopesForSymbolRecursive(
preprocessorScopes,
scope => scope.Directive.Contains(excisionLanguage.ServerScopeStartString, StringComparison.Ordinal),
detectedPreprocessorServerOnlyScopes);

// Process scopes we've evaluated must be server only.
foreach (ServerOnlyScopeData currentScope in visitor.DetectedServerOnlyScopes)
Expand All @@ -253,8 +257,8 @@ private ExcisionStats ProcessCodeFile(string fileName, string inputPath, EExcisi
}

// 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))
var (StartIndex, EndIndex) = TrimWhitespace(script, currentScope);
if (detectedPreprocessorServerOnlyScopes.Any(x => StartIndex >= x.Span.StartIndex && EndIndex <= x.Span.EndIndex))
{
continue; // We're inside an existing scope.
}
Expand Down Expand Up @@ -358,57 +362,16 @@ private static (int StartIndex, int StopIndex) TrimWhitespace(string script, Ser
return (startIndex, stopIndex);
}

private static List<(string Directive, int StartIndex, int StopIndex)> FindPreprocessorGuards(BufferedTokenStream tokenStream)
private static void FindPreprocessorScopesForSymbolRecursive(List<PreprocessorScope> scopes, Predicate<PreprocessorScope> predicate, List<PreprocessorScope> result)
{
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<IToken>();

foreach (var token in preprocessorDirectives)
foreach (var scope in scopes)
{
switch (token.Text)
if (predicate(scope))
{
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;
result.Add(scope);
}
FindPreprocessorScopesForSymbolRecursive(scope.Children, predicate, result);
}

return preprocessorGuards;
}

private bool InjectedMacroAlreadyExistsAtLocation(StringBuilder script, int index, bool lookAhead, bool ignoreWhitespace, string macro)
Expand Down
58 changes: 58 additions & 0 deletions ServerCodeExciserTest/PreprocessorParserTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using Antlr4.Runtime;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace ServerCodeExciser.Tests
{
[TestClass]
public class PreprocessorParserTests
{
[TestMethod]
public void ConditionalBranchTest()
{
var script = "#ifdef WITH_SERVER\r\n" +
"#elif RELEASE\r\n" +
"#elif DEBUG\r\n" +
"#else\r\n" +
"#endif // WITH_SERVER\r\n";

var lexer = new UnrealAngelscriptLexer(new AntlrInputStream(script));
var tokenStream = new CommonTokenStream(lexer);
tokenStream.Fill();

var nodes = PreprocessorParser.Parse(tokenStream);
Assert.HasCount(1, nodes);
Assert.AreEqual("#ifdef WITH_SERVER", nodes[0].Directive);
Assert.AreEqual("#elif RELEASE", nodes[0].Children[0].Directive);
Assert.AreEqual("#elif DEBUG", nodes[0].Children[1].Directive);
Assert.AreEqual("#else", nodes[0].Children[2].Directive);
Assert.AreEqual(1, nodes[0].Span.Start.Line);
Assert.AreEqual(0, nodes[0].Span.Start.Column);
Assert.AreEqual(0, nodes[0].Span.StartIndex);
Assert.AreEqual(5, nodes[0].Span.End.Line);
Assert.AreEqual(0, nodes[0].Span.End.Column);
Assert.AreEqual(script.Length - "\r\n".Length - 1, nodes[0].Span.EndIndex);
}

[TestMethod]
public void NestedTest()
{
var script = "#ifdef WITH_SERVER\r\n" +
" #if RELEASE\r\n" +
" #elif DEBUG\r\n" +
" #endif // !RELEASE\r\n" +
"#endif // WITH_SERVER\r\n";

var lexer = new UnrealAngelscriptLexer(new AntlrInputStream(script));
var tokenStream = new CommonTokenStream(lexer);
tokenStream.Fill();

var nodes = PreprocessorParser.Parse(tokenStream);
Assert.HasCount(1, nodes);
Assert.AreEqual("#ifdef WITH_SERVER", nodes[0].Directive);
Assert.AreEqual("#if RELEASE", nodes[0].Children[0].Directive);
Assert.AreEqual("#elif DEBUG", nodes[0].Children[0].Children[0].Directive);
Assert.AreEqual(0, nodes[0].Span.End.Column);
Assert.AreEqual(script.Length - "\r\n".Length - 1, nodes[0].Span.EndIndex);
}
}
}
21 changes: 21 additions & 0 deletions ServerCodeExcisionCommon/SourcePosition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace ServerCodeExcisionCommon
{
/// <summary>
/// Represents a position in source code (line and column).
/// Lines and columns are 1-based to match editor conventions.
/// </summary>
public readonly struct SourcePosition
{
public int Line { get; }

public int Column { get; }

public SourcePosition(int line, int column)
{
Line = line;
Column = column;
}

public override string ToString() => $"({Line}:{Column})";
}
}
42 changes: 42 additions & 0 deletions ServerCodeExcisionCommon/SourceSpan.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;

namespace ServerCodeExcisionCommon
{
/// <summary>
/// Represents a span of source code from a start position to an end position.
/// </summary>
public struct SourceSpan
{
public SourcePosition Start { get; set; }

public SourcePosition End { get; set; }

/// <summary>
/// The absolute start index in the source text (0-based).
/// </summary>
public int StartIndex { get; set; }

/// <summary>
/// The absolute end index in the source text (0-based, exclusive).
/// </summary>
public int EndIndex { get; set; }

public SourceSpan(SourcePosition start, SourcePosition end, int startIndex, int endIndex)
{
if (startIndex > endIndex)
{
throw new ArgumentException($"{nameof(startIndex)} is greater than {nameof(endIndex)}");
}

Start = start;
End = end;
StartIndex = startIndex;
EndIndex = endIndex;
}

public SourceSpan(int startLine, int startColumn, int endLine, int endColumn, int startIndex, int endIndex)
: this(new SourcePosition(startLine, startColumn), new SourcePosition(endLine, endColumn), startIndex, endIndex)
{
}
}
}
Loading