Files
UnrealEngine/Engine/Source/Programs/Shared/EpicGames.UHT/Parsers/UhtHeaderFileParser.cs
2025-05-18 13:04:45 +08:00

1433 lines
44 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using EpicGames.Core;
using EpicGames.UHT.Tables;
using EpicGames.UHT.Tokenizer;
using EpicGames.UHT.Types;
using EpicGames.UHT.Utils;
namespace EpicGames.UHT.Parsers
{
/// <summary>
/// Keyword parse results
/// </summary>
public enum UhtParseResult
{
/// <summary>
/// Keyword was handled
/// </summary>
Handled,
/// <summary>
/// Keyword wasn't handled (more attempts will be made to match)
/// </summary>
Unhandled,
/// <summary>
/// Keyword is invalid
/// </summary>
Invalid,
}
/// <summary>
/// Compiler directives
/// </summary>
[Flags]
[SuppressMessage("Usage", "CA2217:Do not mark enums with FlagsAttribute")]
public enum UhtCompilerDirective
{
/// <summary>
/// No compile directives
/// </summary>
None = 0,
/// <summary>
/// This indicates we are in a "#if CPP" block
/// </summary>
CPPBlock = 1 << 0,
/// <summary>
/// This indicates we are in a "#if !CPP" block
/// </summary>
NotCPPBlock = 1 << 1,
/// <summary>
/// This indicates we are in a "#if 0" block
/// </summary>
ZeroBlock = 1 << 2,
/// <summary>
/// This indicates we are in a "#if 1" block
/// </summary>
OneBlock = 1 << 3,
/// <summary>
/// This indicates we are in a "#if WITH_EDITOR" block
/// </summary>
WithEditor = 1 << 4,
/// <summary>
/// This indicates we are in a "#if WITH_EDITORONLY_DATA" block
/// </summary>
WithEditorOnlyData = 1 << 5,
/// <summary>
/// This indicates we are in a "#if WITH_HOT_RELOAD" block
/// </summary>
WithHotReload = 1 << 6,
/// <summary>
/// This indicates we are in a "#if WITH_ENGINE" block
/// </summary>
WithEngine = 1 << 7,
/// <summary>
/// This indicates we are in a "#if WITH_COREUOBJECT" block
/// </summary>
WithCoreUObject = 1 << 8,
/// <summary>
/// This indicates we are in a "#if WITH_VERSE_VM" block
/// </summary>
WithVerseVM = 1 << 9,
/// <summary>
/// This directive is unrecognized and does not change the code generation at all
/// </summary>
Unrecognized = 1 << 10,
/// <summary>
/// This indicates we are in a "#if WITH_TESTS" block
/// </summary>
WithTests = 1 << 11,
/// <summary>
/// This indicates we are in a "#if WITH_VERSE_BPVM" block
/// </summary>
WithVerseBPVM = 1 << 12,
/// <summary>
/// The following flags are always ignored when keywords test for allowed conditional blocks
/// </summary>
AllowedCheckIgnoredFlags = CPPBlock | NotCPPBlock | ZeroBlock | OneBlock | WithHotReload,
/// <summary>
/// Default compiler directives to be allowed
/// </summary>
DefaultAllowedCheck = WithEditor | WithEditorOnlyData | WithVerseVM | WithTests | WithVerseBPVM,
/// <summary>
/// All flags are allowed
/// </summary>
SilenceAllowedCheck = ~None,
}
/// <summary>
/// Helper methods for testing flags. These methods perform better than the generic HasFlag which hits
/// the GC and stalls.
/// </summary>
public static class UhtCompilerDirectiveExtensions
{
private static readonly Lazy<List<string>> s_names = new (() =>
{
List<string> outList = new();
FieldInfo[] fields = typeof(UhtCompilerDirective).GetFields();
for (int bit = 0; bit < 32; ++bit)
{
bool found = false;
uint mask = (uint)1 << bit;
foreach (FieldInfo field in fields)
{
if (field.IsSpecialName)
{
continue;
}
object? value = field.GetValue(null);
if (value != null)
{
if (mask == (uint)value)
{
outList.Add(GetCompilerDirectiveText((UhtCompilerDirective)value));
found = true;
break;
}
}
}
if (!found)
{
outList.Add($"0x{mask:X8}");
}
}
return outList;
});
/// <summary>
/// Return the text associated with the given compiler directive
/// </summary>
/// <param name="compilerDirective">Directive in question</param>
/// <returns>String representation</returns>
public static string GetCompilerDirectiveText(this UhtCompilerDirective compilerDirective)
{
switch (compilerDirective)
{
case UhtCompilerDirective.CPPBlock: return "CPP";
case UhtCompilerDirective.NotCPPBlock: return "!CPP";
case UhtCompilerDirective.ZeroBlock: return "0";
case UhtCompilerDirective.OneBlock: return "1";
case UhtCompilerDirective.WithHotReload: return UhtNames.WithHotReload;
case UhtCompilerDirective.WithEditor: return UhtNames.WithEditor;
case UhtCompilerDirective.WithEditorOnlyData: return UhtNames.WithEditorOnlyData;
case UhtCompilerDirective.WithEngine: return UhtNames.WithEngine;
case UhtCompilerDirective.WithCoreUObject: return UhtNames.WithCoreUObject;
case UhtCompilerDirective.WithVerseVM: return UhtNames.WithVerseVM;
case UhtCompilerDirective.WithVerseBPVM: return UhtNames.WithVerseBPVM;
case UhtCompilerDirective.WithTests: return UhtNames.WithTests;
default: return "<unrecognized>";
}
}
/// <summary>
/// Return a define scope based on the given compiler directives
/// </summary>
/// <param name="compilerDirectives">Directives in question</param>
/// <returns>Matching define scope</returns>
public static UhtDefineScope GetDefaultDefineScopes(this UhtCompilerDirective compilerDirectives)
{
UhtDefineScope defineScope = UhtDefineScope.None;
if (compilerDirectives.HasAnyFlags(UhtCompilerDirective.WithTests))
{
defineScope |= UhtDefineScope.Tests;
}
if (compilerDirectives.HasAnyFlags(UhtCompilerDirective.WithVerseBPVM))
{
defineScope |= UhtDefineScope.VerseBPVM;
}
if (compilerDirectives.HasAnyFlags(UhtCompilerDirective.WithVerseVM))
{
defineScope |= UhtDefineScope.VerseVM;
}
return defineScope;
}
/// <summary>
/// Return a string list of the given compiler directives
/// </summary>
/// <param name="inFlags"></param>
/// <returns></returns>
public static List<string> ToStringList(this UhtCompilerDirective inFlags)
{
List<string> names = s_names.Value;
ulong intFlags = (ulong)inFlags;
List<string> outList = new();
for (int bit = 0; bit < 32; ++bit)
{
ulong mask = (ulong)1 << bit;
if (mask > intFlags)
{
break;
}
if ((mask & intFlags) != 0)
{
outList.Add(names[bit]);
}
}
return outList;
}
/// <summary>
/// Test to see if any of the specified flags are set
/// </summary>
/// <param name="inFlags">Current flags</param>
/// <param name="testFlags">Flags to test for</param>
/// <returns>True if any of the flags are set</returns>
public static bool HasAnyFlags(this UhtCompilerDirective inFlags, UhtCompilerDirective testFlags)
{
return (inFlags & testFlags) != 0;
}
/// <summary>
/// Test to see if all of the specified flags are set
/// </summary>
/// <param name="inFlags">Current flags</param>
/// <param name="testFlags">Flags to test for</param>
/// <returns>True if all the flags are set</returns>
public static bool HasAllFlags(this UhtCompilerDirective inFlags, UhtCompilerDirective testFlags)
{
return (inFlags & testFlags) == testFlags;
}
/// <summary>
/// Test to see if a specific set of flags have a specific value.
/// </summary>
/// <param name="inFlags">Current flags</param>
/// <param name="testFlags">Flags to test for</param>
/// <param name="matchFlags">Expected value of the tested flags</param>
/// <returns>True if the given flags have a specific value.</returns>
public static bool HasExactFlags(this UhtCompilerDirective inFlags, UhtCompilerDirective testFlags, UhtCompilerDirective matchFlags)
{
return (inFlags & testFlags) == matchFlags;
}
}
/// <summary>
/// Specifiers for public, private, and protected
/// </summary>
[UnrealHeaderTool]
public static class UhtAccessSpecifierKeywords
{
#region Keywords
[UhtKeyword(Extends = UhtTableNames.ClassBase, Keyword = "public", AllowedCompilerDirectives = UhtCompilerDirective.SilenceAllowedCheck)]
[UhtKeyword(Extends = UhtTableNames.ScriptStruct, Keyword = "public", AllowedCompilerDirectives = UhtCompilerDirective.SilenceAllowedCheck)]
[SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "Attribute accessed method")]
[SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Attribute accessed method")]
private static UhtParseResult PublicKeyword(UhtParsingScope topScope, UhtParsingScope actionScope, ref UhtToken token)
{
return SetAccessSpecifier(topScope, UhtAccessSpecifier.Public);
}
[UhtKeyword(Extends = UhtTableNames.ClassBase, Keyword = "protected", AllowedCompilerDirectives = UhtCompilerDirective.SilenceAllowedCheck)]
[UhtKeyword(Extends = UhtTableNames.ScriptStruct, Keyword = "protected", AllowedCompilerDirectives = UhtCompilerDirective.SilenceAllowedCheck)]
[SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "Attribute accessed method")]
[SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Attribute accessed method")]
private static UhtParseResult ProtectedKeyword(UhtParsingScope topScope, UhtParsingScope actionScope, ref UhtToken token)
{
return SetAccessSpecifier(topScope, UhtAccessSpecifier.Protected);
}
[UhtKeyword(Extends = UhtTableNames.ClassBase, Keyword = "private", AllowedCompilerDirectives = UhtCompilerDirective.SilenceAllowedCheck)]
[UhtKeyword(Extends = UhtTableNames.ScriptStruct, Keyword = "private", AllowedCompilerDirectives = UhtCompilerDirective.SilenceAllowedCheck)]
[SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "Attribute accessed method")]
[SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Attribute accessed method")]
private static UhtParseResult PrivateKeyword(UhtParsingScope topScope, UhtParsingScope actionScope, ref UhtToken token)
{
return SetAccessSpecifier(topScope, UhtAccessSpecifier.Private);
}
#endregion
private static UhtParseResult SetAccessSpecifier(UhtParsingScope topScope, UhtAccessSpecifier accessSpecifier)
{
topScope.AccessSpecifier = accessSpecifier;
topScope.TokenReader.Require(':');
return UhtParseResult.Handled;
}
}
/// <summary>
/// Header file parser
/// </summary>
public class UhtHeaderFileParser : IUhtTokenPreprocessor
{
private struct CompilerDirective
{
public UhtCompilerDirective _element;
public UhtCompilerDirective _composite;
}
/// <summary>
/// Header file being parsed
/// </summary>
public UhtHeaderFile HeaderFile { get; }
/// <summary>
/// Module containing the header file
/// </summary>
public UhtModule Module => HeaderFile.Module;
/// <summary>
/// Token reader for the header
/// </summary>
public IUhtTokenReader TokenReader { get; }
/// <summary>
/// If true, the inclusion of the generated header file was seen
/// </summary>
public bool SpottedAutogeneratedHeaderInclude { get; set; } = false;
/// <summary>
/// If set, the preprocessor is run in a C++ UHT compatibility mode where only a subset
/// of #if class of preprocessor statements are allowed.
/// </summary>
public string? RestrictedPreprocessorContext { get; set; } = null;
/// <summary>
/// Stack of current #if states
/// </summary>
private readonly List<CompilerDirective> _compilerDirectives = new();
/// <summary>
/// Stack of current #if states saved as part of the preprocessor state
/// </summary>
private readonly List<CompilerDirective> _savedCompilerDirectives = new();
/// <summary>
/// Current top of the parsing scopes. Classes, structures and functions all allocate scopes.
/// </summary>
private UhtParsingScope? _topScope = null;
/// <summary>
/// Parse the given header file
/// </summary>
/// <param name="headerFile">Header file to parse</param>
/// <returns>Parser</returns>
public static UhtHeaderFileParser Parse(UhtHeaderFile headerFile)
{
UhtHeaderFileParser headerParser = new(headerFile);
using UhtParsingScope topScope = new(headerParser, headerFile.Session.GetKeywordTable(UhtTableNames.Global));
headerParser.ParseStatements();
if (!headerParser.SpottedAutogeneratedHeaderInclude && headerParser.HeaderFile.Data.Length > 0)
{
bool noExportClassesOnly = true;
bool missingGeneratedHeader = false;
foreach (UhtType type in headerParser.HeaderFile.Children)
{
if (type is UhtClass classObj)
{
if (classObj.ClassType != UhtClassType.NativeInterface && !classObj.ClassExportFlags.HasAnyFlags(UhtClassExportFlags.NoExport))
{
noExportClassesOnly = false;
break;
}
}
else if (!headerParser.HeaderFile.IsNoExportTypes)
{
missingGeneratedHeader = true;
}
}
string logMessage = $"The given include must appear at the top of the header following all other includes: '#include \"{headerParser.HeaderFile.GeneratedHeaderFileName}\"'";
if (!noExportClassesOnly)
{
headerParser.HeaderFile.LogError(logMessage);
}
if (missingGeneratedHeader)
{
UhtIssueBehavior missingGeneratedHeaderBehavior = headerFile.Module.IsPartOfEngine ? headerFile.Session.Config!.EngineMissingGeneratedHeaderIncludeBehavior
: headerFile.Session.Config!.NonEngineMissingGeneratedHeaderIncludeBehavior;
switch (missingGeneratedHeaderBehavior)
{
case UhtIssueBehavior.AllowSilently:
break;
case UhtIssueBehavior.AllowAndLog:
headerParser.HeaderFile.LogTrace(logMessage);
break;
default:
headerParser.HeaderFile.LogError(logMessage);
break;
}
}
}
return headerParser;
}
private UhtHeaderFileParser(UhtHeaderFile headerFile)
{
TokenReader = new UhtTokenBufferReader(headerFile, headerFile.Data.Memory);
HeaderFile = headerFile;
TokenReader.TokenPreprocessor = this;
}
/// <summary>
/// Push a new scope
/// </summary>
/// <param name="scope">Scope to push</param>
/// <exception cref="UhtIceException">Throw if the new scope isn't parented by the current scope</exception>
public void PushScope(UhtParsingScope scope)
{
if (scope.ParentScope != _topScope)
{
throw new UhtIceException("Pushing a new scope whose parent isn't the current top scope.");
}
_topScope = scope;
}
/// <summary>
/// Pop the given scope
/// </summary>
/// <param name="scope">Scope to be popped</param>
/// <exception cref="UhtIceException">Thrown if the given scope isn't the top scope</exception>
public void PopScope(UhtParsingScope scope)
{
if (scope != _topScope)
{
throw new UhtIceException("Attempt to pop a scope that isn't the top scope");
}
_topScope = scope.ParentScope;
}
/// <summary>
/// Return the current compiler directive
/// </summary>
/// <returns>Enumeration flags for all active compiler directives</returns>
public UhtCompilerDirective GetCurrentCompositeCompilerDirective()
{
return _compilerDirectives.Count > 0 ? _compilerDirectives[^1]._composite : UhtCompilerDirective.None;
}
/// <summary>
/// Get the current compiler directive without any parent scopes merged in
/// </summary>
/// <returns>Current compiler directive</returns>
public UhtCompilerDirective GetCurrentNonCompositeCompilerDirective()
{
return _compilerDirectives.Count > 0 ? _compilerDirectives[^1]._element : UhtCompilerDirective.None;
}
#region ITokenPreprocessor implementation
/// <inheritdoc/>
public bool ParsePreprocessorDirective(ref UhtToken token, bool isBeingIncluded, out bool clearComments, out bool illegalContentsCheck)
{
clearComments = true;
if (ParseDirectiveInternal(isBeingIncluded))
{
clearComments = ClearCommentsCompilerDirective();
}
illegalContentsCheck = !GetCurrentNonCompositeCompilerDirective().HasAnyFlags(UhtCompilerDirective.ZeroBlock | UhtCompilerDirective.WithEditorOnlyData);
return IncludeCurrentCompilerDirective();
}
/// <inheritdoc/>
public void SaveState()
{
_savedCompilerDirectives.Clear();
_savedCompilerDirectives.AddRange(_compilerDirectives);
}
/// <inheritdoc/>
public void RestoreState()
{
_compilerDirectives.Clear();
_compilerDirectives.AddRange(_savedCompilerDirectives);
}
#endregion
#region Statement parsing
/// <summary>
/// Parse all statements in the header file
/// </summary>
public void ParseStatements()
{
ParseStatements((char)0, (char)0, true);
}
/// <summary>
/// Parse the statements between the given symbols
/// </summary>
/// <param name="initiator">Starting symbol</param>
/// <param name="terminator">Ending symbol</param>
/// <param name="logUnhandledKeywords">If true, log any unhandled keywords</param>
public void ParseStatements(char initiator, char terminator, bool logUnhandledKeywords)
{
if (_topScope == null)
{
return;
}
if (initiator != 0)
{
TokenReader.Require(initiator);
}
while (true)
{
UhtToken token = TokenReader.GetToken();
if (token.TokenType.IsEndType())
{
if (_topScope != null && _topScope.ParentScope == null)
{
CheckEof(ref token);
}
return;
}
else if (terminator != 0 && token.IsSymbol(terminator))
{
return;
}
if (_topScope != null)
{
ParseStatement(_topScope, ref token, logUnhandledKeywords);
TokenReader.ClearComments();
}
}
}
/// <summary>
/// Parse a statement
/// </summary>
/// <param name="topScope">Current top scope</param>
/// <param name="token">Token starting the statement</param>
/// <param name="logUnhandledKeywords">If true, log unhandled keywords</param>
/// <returns>Always returns true ATM</returns>
public static bool ParseStatement(UhtParsingScope topScope, ref UhtToken token, bool logUnhandledKeywords)
{
UhtParseResult parseResult = UhtParseResult.Unhandled;
switch (token.TokenType)
{
case UhtTokenType.Identifier:
parseResult = DispatchKeyword(topScope, ref token);
break;
case UhtTokenType.Symbol:
// C++ UHT TODO - Generate errors when extra ';' are found.
// UPROPERTY has code to remove extra ';'
if (token.IsSymbol(';'))
{
UhtToken nextToken = topScope.TokenReader.PeekToken();
if (nextToken.TokenType.IsEndType())
{
topScope.TokenReader.LogError("Extra ';' before end of file");
}
else
{
topScope.TokenReader.LogError($"Extra ';' before '{nextToken}'");
}
return true;
}
parseResult = DispatchKeyword(topScope, ref token);
break;
}
if (parseResult == UhtParseResult.Unhandled)
{
parseResult = DispatchCatchAll(topScope, ref token);
}
if (parseResult == UhtParseResult.Unhandled)
{
parseResult = ProbablyAnUnknownObjectLikeMacro(topScope.TokenReader, ref token);
}
if (parseResult == UhtParseResult.Unhandled && logUnhandledKeywords)
{
topScope.Session.LogUnhandledKeywordError(topScope.TokenReader, token);
}
if (parseResult == UhtParseResult.Unhandled || parseResult == UhtParseResult.Invalid)
{
if (topScope.ScopeType is UhtClass classObj)
{
using UhtTokenRecorder recorder = new(topScope, ref token);
topScope.TokenReader.SkipDeclaration(ref token);
if (recorder.Stop())
{
if (classObj.Declarations != null)
{
UhtDeclaration declaration = classObj.Declarations[classObj.Declarations.Count - 1];
if (topScope.HeaderParser.CheckForConstructor(classObj, declaration))
{
}
else if (topScope.HeaderParser.CheckForDestructor(classObj, declaration))
{
}
else if (classObj.ClassType == UhtClassType.Class)
{
if (topScope.HeaderParser.CheckForSerialize(classObj, declaration))
{
}
}
}
}
}
else
{
topScope.TokenReader.SkipDeclaration(ref token);
}
}
return true;
}
private static UhtParseResult DispatchKeyword(UhtParsingScope topScope, ref UhtToken token)
{
UhtParseResult parseResult = UhtParseResult.Unhandled;
for (UhtParsingScope? currentScope = topScope; currentScope != null && parseResult == UhtParseResult.Unhandled; currentScope = currentScope.ParentScope)
{
if (currentScope.ScopeKeywordTable.TryGetValue(token.Value, out UhtKeyword keywordInfo))
{
if (keywordInfo.AllScopes || topScope == currentScope)
{
UhtCompilerDirective currentDirective = topScope.HeaderParser.GetCurrentCompositeCompilerDirective() & ~UhtCompilerDirective.AllowedCheckIgnoredFlags;
if (currentDirective.HasAnyFlags(~keywordInfo.AllowedCompilerDirectives))
{
List<string> strings = keywordInfo.AllowedCompilerDirectives.ToStringList();
string directives = UhtUtilities.MergeTypeNames(strings, "or", false);
topScope.TokenReader.LogError($"'{token.Value}' must not be inside preprocessor blocks, except for {directives}");
}
parseResult = keywordInfo.Delegate(topScope, currentScope, ref token);
}
}
}
return parseResult;
}
private static UhtParseResult DispatchCatchAll(UhtParsingScope topScope, ref UhtToken token)
{
for (UhtParsingScope? currentScope = topScope; currentScope != null; currentScope = currentScope.ParentScope)
{
foreach (UhtKeywordCatchAllDelegate catchAll in currentScope.ScopeKeywordTable.CatchAlls)
{
UhtParseResult parseResult = catchAll(topScope, ref token);
if (parseResult != UhtParseResult.Unhandled)
{
return parseResult;
}
}
}
return UhtParseResult.Unhandled;
}
/// <summary>
/// Tests if an identifier looks like a macro which doesn't have a following open parenthesis.
/// </summary>
/// <param name="tokenReader">Token reader</param>
/// <param name="token">The current token that initiated the process</param>
/// <returns>Result if matching the token</returns>
private static UhtParseResult ProbablyAnUnknownObjectLikeMacro(IUhtTokenReader tokenReader, ref UhtToken token)
{
// Non-identifiers are not macros
if (!token.IsIdentifier())
{
return UhtParseResult.Unhandled;
}
// Macros must start with a capitalized alphanumeric character or underscore
char firstChar = token.Value.Span[0];
if (firstChar != '_' && (firstChar < 'A' || firstChar > 'Z'))
{
return UhtParseResult.Unhandled;
}
// We'll guess about it being a macro based on it being fully-capitalized with at least one underscore.
int underscoreCount = 0;
foreach (char ch in token.Value.Span[1..])
{
if (ch == '_')
{
++underscoreCount;
}
else if ((ch < 'A' || ch > 'Z') && (ch < '0' || ch > '9'))
{
return UhtParseResult.Unhandled;
}
}
// We look for at least one underscore as a convenient way of allowing many known macros
// like FORCEINLINE and CONSTEXPR, and non-macros like FPOV and TCHAR.
if (underscoreCount == 0)
{
return UhtParseResult.Unhandled;
}
// Identifiers which end in _API are known
if (token.Value.Span.Length > 4 && token.Value.Span.EndsWith("_API"))
{
return UhtParseResult.Unhandled;
}
// Ignore certain known macros or identifiers that look like macros.
if (token.IsValue("FORCEINLINE_DEBUGGABLE") ||
token.IsValue("FORCEINLINE_STATS") ||
token.IsValue("SIZE_T"))
{
return UhtParseResult.Unhandled;
}
// Check if there's an open parenthesis following the token.
return tokenReader.PeekToken().IsSymbol('(') ? UhtParseResult.Unhandled : UhtParseResult.Handled;
}
private void CheckEof(ref UhtToken token)
{
if (_compilerDirectives.Count > 0)
{
throw new UhtException(TokenReader, token.InputLine, "Missing #endif");
}
}
#endregion
#region Internals
/// <summary>
/// Parse a preprocessor directive.
/// </summary>
/// <param name="isBeingIncluded">If true, then this directive is in an active block</param>
/// <returns>True if we should check to see if tokenizer should clear comments</returns>
private bool ParseDirectiveInternal(bool isBeingIncluded)
{
bool checkClearComments = false;
// Collect all the lines of the preprocessor statement including any continuations.
// We assume that the vast majority of lines will not be continuations. So avoid using the
// string builder as much as possible.
int startingLine = TokenReader.InputLine;
StringViewBuilder builder = new();
while (true)
{
UhtToken lineToken = TokenReader.GetLine();
if (lineToken.TokenType != UhtTokenType.Line)
{
break;
}
if (lineToken.Value.Span.Length > 0 && lineToken.Value.Span[^1] == '\\')
{
builder.Append(new StringView(lineToken.Value, 0, lineToken.Value.Span.Length - 1));
}
else
{
builder.Append(lineToken.Value);
break;
}
}
StringView line = builder.ToStringView();
// Create a token reader we will use to decode
UhtTokenBufferReader lineTokenReader = new(HeaderFile, line.Memory);
lineTokenReader.InputLine = startingLine;
if (!lineTokenReader.TryOptionalIdentifier(out UhtToken directive))
{
if (isBeingIncluded)
{
throw new UhtException(TokenReader, directive.InputLine, "Missing compiler directive after '#'");
}
return checkClearComments;
}
if (directive.IsValue("error"))
{
CheckRestrictedMode();
if (isBeingIncluded)
{
throw new UhtException(TokenReader, directive.InputLine, "#error directive encountered");
}
}
else if (directive.IsValue("pragma"))
{
CheckRestrictedMode();
// Ignore all pragmas
}
else if (directive.IsValue("linenumber"))
{
CheckRestrictedMode();
if (!lineTokenReader.TryOptionalConstInt(out int newInputLine))
{
throw new UhtException(TokenReader, directive.InputLine, "Missing line number in line number directive");
}
TokenReader.InputLine = newInputLine;
}
else if (directive.IsValue("include"))
{
CheckRestrictedMode();
if (isBeingIncluded)
{
if (SpottedAutogeneratedHeaderInclude)
{
HeaderFile.LogError("#include found after .generated.h file - the .generated.h file should always be the last #include in a header");
}
StringView includeNameString = new StringView();
UhtToken includeName = lineTokenReader.GetToken();
if (includeName.IsConstString())
{
includeNameString = includeName.GetUnescapedString(HeaderFile);
}
else if (includeName.IsSymbol('<'))
{
includeNameString = new StringView(lineTokenReader.GetRawString('>', UhtRawStringOptions.DontConsumeTerminator).Memory.Trim());
}
if (includeNameString.Length > 0)
{
if (HeaderFile.GeneratedHeaderFileName.AsSpan().Equals(includeNameString.Span, StringComparison.OrdinalIgnoreCase))
{
SpottedAutogeneratedHeaderInclude = true;
}
if (!includeNameString.Span.Contains(".generated.h", StringComparison.Ordinal))
{
HeaderFile.AddReferencedHeader(includeNameString.ToString(), UhtHeaderReferenceType.Include);
}
}
}
}
else if (directive.IsValue("if"))
{
checkClearComments = true;
PushCompilerDirective(ParserConditional(lineTokenReader));
if (IsRestrictedDirective(GetCurrentNonCompositeCompilerDirective()))
{
CheckRestrictedMode();
}
}
else if (directive.IsValue("ifdef") || directive.IsValue("ifndef"))
{
checkClearComments = true;
PushCompilerDirective(UhtCompilerDirective.Unrecognized);
}
else if (directive.IsValue("elif"))
{
checkClearComments = true;
UhtCompilerDirective oldCompilerDirective = PopCompilerDirective(directive);
UhtCompilerDirective newCompilerDirective = ParserConditional(lineTokenReader);
if (SupportsElif(oldCompilerDirective) != SupportsElif(newCompilerDirective))
{
throw new UhtException(TokenReader, directive.InputLine,
$"Mixing {oldCompilerDirective.GetCompilerDirectiveText()} with {newCompilerDirective.GetCompilerDirectiveText()} in an #elif preprocessor block is not supported");
}
PushCompilerDirective(newCompilerDirective);
if (IsRestrictedDirective(GetCurrentNonCompositeCompilerDirective()))
{
CheckRestrictedMode();
}
}
else if (directive.IsValue("else"))
{
checkClearComments = true;
UhtCompilerDirective oldCompilerDirective = PopCompilerDirective(directive);
switch (oldCompilerDirective)
{
case UhtCompilerDirective.ZeroBlock:
PushCompilerDirective(UhtCompilerDirective.OneBlock);
break;
case UhtCompilerDirective.OneBlock:
PushCompilerDirective(UhtCompilerDirective.ZeroBlock);
break;
case UhtCompilerDirective.NotCPPBlock:
PushCompilerDirective(UhtCompilerDirective.CPPBlock);
break;
case UhtCompilerDirective.CPPBlock:
PushCompilerDirective(UhtCompilerDirective.NotCPPBlock);
break;
case UhtCompilerDirective.WithEngine:
PushCompilerDirective(UhtCompilerDirective.Unrecognized);
break;
case UhtCompilerDirective.WithCoreUObject:
PushCompilerDirective(UhtCompilerDirective.Unrecognized);
break;
case UhtCompilerDirective.WithHotReload:
throw new UhtException(TokenReader, directive.InputLine, "Can not use WITH_HOT_RELOAD with an #else clause");
default:
PushCompilerDirective(oldCompilerDirective);
break;
}
if (IsRestrictedDirective(GetCurrentNonCompositeCompilerDirective()))
{
CheckRestrictedMode();
}
}
else if (directive.IsValue("endif"))
{
PopCompilerDirective(directive);
}
else if (directive.IsValue("define"))
{
CheckRestrictedMode();
}
else if (directive.IsValue("undef"))
{
CheckRestrictedMode();
}
else
{
if (isBeingIncluded)
{
throw new UhtException(TokenReader, directive.InputLine, $"Unrecognized compiler directive {directive.Value}");
}
}
return checkClearComments;
}
private void CheckRestrictedMode()
{
if (RestrictedPreprocessorContext != null)
{
TokenReader.LogError($"Preprocessor statement not allowed while {RestrictedPreprocessorContext}");
}
}
private UhtCompilerDirective ParserConditional(UhtTokenBufferReader lineTokenReader)
{
// Get any possible ! and the identifier
UhtToken define = lineTokenReader.GetToken();
bool notPresent = define.IsSymbol('!');
if (notPresent)
{
define = lineTokenReader.GetToken();
}
//COMPATIBILITY-TODO
// UModel.h contains a compound #if where the leading one is !CPP.
// Checking for this being the only token causes that to fail
#if COMPATIBILITY_DISABLE
// Make sure there is nothing left
UhtToken end = LineTokenReader.GetToken();
if (!end.TokenType.IsEndType())
{
return UhtCompilerDirective.Unrecognized;
}
#endif
switch (define.TokenType)
{
case UhtTokenType.DecimalConst:
if (define.IsValue("0"))
{
return UhtCompilerDirective.ZeroBlock;
}
else if (define.IsValue("1"))
{
return UhtCompilerDirective.OneBlock;
}
break;
case UhtTokenType.Identifier:
if (define.IsValue(UhtNames.WithEditorOnlyData))
{
return notPresent ? UhtCompilerDirective.Unrecognized : UhtCompilerDirective.WithEditorOnlyData;
}
else if (define.IsValue(UhtNames.WithEditor))
{
return notPresent ? UhtCompilerDirective.Unrecognized : UhtCompilerDirective.WithEditor;
}
else if (define.IsValue(UhtNames.WithHotReload))
{
return UhtCompilerDirective.WithHotReload;
}
else if (define.IsValue(UhtNames.WithEngine))
{
return notPresent ? UhtCompilerDirective.Unrecognized : UhtCompilerDirective.WithEngine;
}
else if (define.IsValue(UhtNames.WithCoreUObject))
{
return notPresent ? UhtCompilerDirective.Unrecognized : UhtCompilerDirective.WithCoreUObject;
}
else if (define.IsValue(UhtNames.WithVerseVM))
{
return notPresent ? UhtCompilerDirective.Unrecognized : UhtCompilerDirective.WithVerseVM;
}
else if (define.IsValue(UhtNames.WithVerseBPVM))
{
return notPresent ? UhtCompilerDirective.Unrecognized : UhtCompilerDirective.WithVerseBPVM;
}
else if (define.IsValue(UhtNames.WithTests))
{
return notPresent ? UhtCompilerDirective.Unrecognized : UhtCompilerDirective.WithTests;
}
else if (define.IsValue("CPP"))
{
return notPresent ? UhtCompilerDirective.NotCPPBlock : UhtCompilerDirective.CPPBlock;
}
else if (define.ValueStartsWith("UE_VERSION"))
{
return ParserEngineVersionConditional(define, notPresent, lineTokenReader);
}
break;
case UhtTokenType.EndOfFile:
case UhtTokenType.EndOfDefault:
case UhtTokenType.EndOfType:
case UhtTokenType.EndOfDeclaration:
throw new UhtException(TokenReader, define.InputLine, "#if with no expression");
}
return UhtCompilerDirective.Unrecognized;
}
private UhtCompilerDirective ParserEngineVersionConditional(UhtToken define, bool notPresent, UhtTokenBufferReader lineTokenReader)
{
lineTokenReader.Require('(');
UhtToken majorToken = lineTokenReader.GetToken();
lineTokenReader.Require(',');
UhtToken minorToken = lineTokenReader.GetToken();
lineTokenReader.Require(',');
UhtToken patchToken = lineTokenReader.GetToken();
lineTokenReader.Require(')');
EngineVersion engineVersion = HeaderFile.Session.EngineVersion;
int major, minor, patch;
majorToken.GetConstInt(out major);
minorToken.GetConstInt(out minor);
patchToken.GetConstInt(out patch);
EngineVersion defineVersion = new EngineVersion(major, minor, patch);
bool? conditionPasses = null;
if (define.IsValue("UE_VERSION_NEWER_THAN_OR_EQUAL"))
{
conditionPasses = engineVersion.CompareTo(defineVersion) >= 0;
}
else if (define.IsValue("UE_VERSION_NEWER_THAN"))
{
conditionPasses = engineVersion.CompareTo(defineVersion) > 0;
}
else if (define.IsValue("UE_VERSION_OLDER_THAN"))
{
conditionPasses = engineVersion.CompareTo(defineVersion) < 0;
}
if (conditionPasses.HasValue)
{
if (notPresent)
{
conditionPasses = !conditionPasses.Value;
}
return conditionPasses.Value ? UhtCompilerDirective.OneBlock : UhtCompilerDirective.ZeroBlock;
}
else
{
throw new UhtException(String.Format("Unknown engine version preprocessor conditional: {0}", define.Value));
}
}
/// <summary>
/// Add a new compiler directive to the stack
/// </summary>
/// <param name="compilerDirective">Directive to be added</param>
private void PushCompilerDirective(UhtCompilerDirective compilerDirective)
{
CompilerDirective newCompileDirective = new();
newCompileDirective._element = compilerDirective;
newCompileDirective._composite = GetCurrentCompositeCompilerDirective() | compilerDirective;
_compilerDirectives.Add(newCompileDirective);
}
/// <summary>
/// Remove the top level compiler directive from the stack
/// </summary>
private UhtCompilerDirective PopCompilerDirective(UhtToken token)
{
if (_compilerDirectives.Count == 0)
{
throw new UhtException(TokenReader, token.InputLine, $"Unmatched '#{token.Value}'");
}
UhtCompilerDirective compilerDirective = _compilerDirectives[^1]._element;
_compilerDirectives.RemoveAt(_compilerDirectives.Count - 1);
return compilerDirective;
}
private bool IncludeCurrentCompilerDirective()
{
if (_compilerDirectives.Count == 0)
{
return true;
}
return !GetCurrentCompositeCompilerDirective().HasAnyFlags(UhtCompilerDirective.CPPBlock | UhtCompilerDirective.ZeroBlock | UhtCompilerDirective.Unrecognized);
}
/// <summary>
/// The old UHT would preprocess the file and eliminate any #if blocks that were not required for
/// any contextual information. This results in comments before the #if block being considered
/// for the next definition. This routine classifies each #if block type into if comments should
/// be purged after the directive.
/// </summary>
/// <returns></returns>
/// <exception cref="UhtIceException"></exception>
private bool ClearCommentsCompilerDirective()
{
if (_compilerDirectives.Count == 0)
{
return true;
}
UhtCompilerDirective compilerDirective = _compilerDirectives[^1]._element;
switch (compilerDirective)
{
case UhtCompilerDirective.CPPBlock:
case UhtCompilerDirective.NotCPPBlock:
case UhtCompilerDirective.ZeroBlock:
case UhtCompilerDirective.OneBlock:
case UhtCompilerDirective.Unrecognized:
return false;
case UhtCompilerDirective.WithEditor:
case UhtCompilerDirective.WithEditorOnlyData:
case UhtCompilerDirective.WithEngine:
case UhtCompilerDirective.WithCoreUObject:
case UhtCompilerDirective.WithHotReload:
case UhtCompilerDirective.WithVerseVM:
case UhtCompilerDirective.WithVerseBPVM:
case UhtCompilerDirective.WithTests:
return true;
default:
throw new UhtIceException("Unknown compiler directive flag");
}
}
private static bool IsRestrictedDirective(UhtCompilerDirective compilerDirective)
{
switch (compilerDirective)
{
case UhtCompilerDirective.CPPBlock:
case UhtCompilerDirective.NotCPPBlock:
case UhtCompilerDirective.ZeroBlock:
case UhtCompilerDirective.OneBlock:
case UhtCompilerDirective.Unrecognized:
return false;
case UhtCompilerDirective.WithEditor:
case UhtCompilerDirective.WithEditorOnlyData:
case UhtCompilerDirective.WithEngine:
case UhtCompilerDirective.WithCoreUObject:
case UhtCompilerDirective.WithHotReload:
case UhtCompilerDirective.WithVerseVM:
case UhtCompilerDirective.WithVerseBPVM:
case UhtCompilerDirective.WithTests:
return true;
default:
throw new UhtIceException("Unknown compiler directive flag");
}
}
private static bool SupportsElif(UhtCompilerDirective compilerDirective)
{
return
compilerDirective == UhtCompilerDirective.WithEditor ||
compilerDirective == UhtCompilerDirective.WithEditorOnlyData ||
compilerDirective == UhtCompilerDirective.WithHotReload ||
compilerDirective == UhtCompilerDirective.WithEngine ||
compilerDirective == UhtCompilerDirective.WithCoreUObject ||
compilerDirective == UhtCompilerDirective.WithVerseVM ||
compilerDirective == UhtCompilerDirective.WithVerseBPVM ||
compilerDirective == UhtCompilerDirective.WithTests;
}
private static void SkipVirtualAndAPI(IUhtTokenReader replayReader)
{
while (true)
{
UhtToken peekToken = replayReader.PeekToken();
if (!peekToken.IsValue("virtual") && !peekToken.Value.Span.EndsWith("_API"))
{
break;
}
replayReader.ConsumeToken();
}
}
private bool CheckForConstructor(UhtClass classObj, UhtDeclaration declaration)
{
using UhtTokenReplayReaderBorrower borrowedReader = new(HeaderFile, HeaderFile.Data.Memory, declaration.Tokens, UhtTokenType.EndOfDeclaration);
IUhtTokenReader replayReader = borrowedReader.Reader;
// Allow explicit constructors
{
bool foundExplicit = replayReader.TryOptional("explicit");
if (replayReader.PeekToken().Value.Span.EndsWith("_API"))
{
replayReader.ConsumeToken();
if (!foundExplicit)
{
replayReader.TryOptional("explicit");
}
}
}
if (!replayReader.TryOptional(classObj.SourceName) ||
!replayReader.TryOptional('('))
{
return false;
}
bool oiCtor = false;
bool vtCtor = false;
if (!classObj.ClassExportFlags.HasAnyFlags(UhtClassExportFlags.HasDefaultConstructor) && replayReader.TryOptional(')'))
{
classObj.ClassExportFlags |= UhtClassExportFlags.HasDefaultConstructor;
}
else if (!classObj.ClassExportFlags.HasAllFlags(UhtClassExportFlags.HasObjectInitializerConstructor | UhtClassExportFlags.HasCustomVTableHelperConstructor))
{
bool isConst = false;
bool isRef = false;
int parenthesesNestingLevel = 1;
while (parenthesesNestingLevel != 0)
{
UhtToken token = replayReader.GetToken();
if (!token)
{
break;
}
// Template instantiation or additional parameter excludes ObjectInitializer constructor.
if (token.IsValue(',') || token.IsValue('<'))
{
oiCtor = false;
vtCtor = false;
break;
}
if (token.IsValue('('))
{
parenthesesNestingLevel++;
continue;
}
if (token.IsValue(')'))
{
parenthesesNestingLevel--;
continue;
}
if (token.IsValue("const"))
{
isConst = true;
continue;
}
if (token.IsValue('&'))
{
isRef = true;
continue;
}
// FPostConstructInitializeProperties is deprecated, but left here, so it won't break legacy code.
if (token.IsValue("FObjectInitializer") || token.IsValue("FPostConstructInitializeProperties"))
{
oiCtor = true;
}
if (token.IsValue("FVTableHelper"))
{
vtCtor = true;
}
}
// Parse until finish.
if (parenthesesNestingLevel != 0)
{
replayReader.SkipBrackets('(', ')', parenthesesNestingLevel);
}
if (oiCtor && isRef && isConst)
{
classObj.ClassExportFlags |= UhtClassExportFlags.HasObjectInitializerConstructor;
classObj.MetaData.Add(UhtNames.ObjectInitializerConstructorDeclared, "");
}
if (vtCtor && isRef)
{
classObj.ClassExportFlags |= UhtClassExportFlags.HasCustomVTableHelperConstructor;
}
}
if (!vtCtor)
{
classObj.ClassExportFlags |= UhtClassExportFlags.HasConstructor;
}
return false;
}
private bool CheckForDestructor(UhtClass classObj, UhtDeclaration declaration)
{
if (classObj.ClassExportFlags.HasAnyFlags(UhtClassExportFlags.HasDestructor))
{
return false;
}
using UhtTokenReplayReaderBorrower borrowedReader = new(HeaderFile, HeaderFile.Data.Memory, declaration.Tokens, UhtTokenType.EndOfDeclaration);
IUhtTokenReader replayReader = borrowedReader.Reader;
SkipVirtualAndAPI(replayReader);
if (replayReader.TryOptional('~') && replayReader.TryOptional(classObj.SourceName))
{
classObj.ClassExportFlags |= UhtClassExportFlags.HasDestructor;
return true;
}
return false;
}
bool CheckForSerialize(UhtClass classObj, UhtDeclaration declaration)
{
using UhtTokenReplayReaderBorrower borrowedReader = new(HeaderFile, HeaderFile.Data.Memory, declaration.Tokens, UhtTokenType.EndOfDeclaration);
IUhtTokenReader replayReader = borrowedReader.Reader;
SkipVirtualAndAPI(replayReader);
if (!replayReader.TryOptional("void") ||
!replayReader.TryOptional("Serialize") ||
!replayReader.TryOptional('('))
{
return false;
}
UhtToken token = replayReader.GetToken();
UhtSerializerArchiveType archiveType = UhtSerializerArchiveType.None;
if (token.IsValue("FArchive"))
{
if (replayReader.TryOptional('&'))
{
// Allow the declaration to not define a name for the archive parameter
if (!replayReader.PeekToken().IsValue(')'))
{
replayReader.SkipOne();
}
if (replayReader.TryOptional(')'))
{
archiveType = UhtSerializerArchiveType.Archive;
}
}
}
else if (token.IsValue("FStructuredArchive"))
{
if (replayReader.TryOptional("::") &&
replayReader.TryOptional("FRecord"))
{
// Allow the declaration to not define a name for the archive parameter
if (!replayReader.PeekToken().IsValue(')'))
{
replayReader.SkipOne();
}
if (replayReader.TryOptional(')'))
{
archiveType = UhtSerializerArchiveType.StructuredArchiveRecord;
}
}
}
else if (token.IsValue("FStructuredArchiveRecord"))
{
// Allow the declaration to not define a name for the archive parameter
if (!replayReader.PeekToken().IsValue(')'))
{
replayReader.SkipOne();
}
if (replayReader.TryOptional(')'))
{
archiveType = UhtSerializerArchiveType.StructuredArchiveRecord;
}
}
if (archiveType != UhtSerializerArchiveType.None)
{
// Found what we want!
if (declaration.CompilerDirectives == UhtCompilerDirective.None || declaration.CompilerDirectives == UhtCompilerDirective.WithEditorOnlyData)
{
classObj.SerializerArchiveType |= archiveType;
classObj.SerializerDefineScope = declaration.CompilerDirectives == UhtCompilerDirective.None ? UhtDefineScope.None : UhtDefineScope.EditorOnlyData;
}
else
{
classObj.LogError("Serialize functions must not be inside preprocessor blocks, except for WITH_EDITORONLY_DATA");
}
return true;
}
return false;
}
#endregion
}
}