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

230 lines
6.9 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using EpicGames.Core;
using EpicGames.UHT.Utils;
namespace EpicGames.UHT.Tokenizer
{
/// <summary>
/// Collection of assorted utility token reader extensions
/// </summary>
public static class UhtTokenReaderUtilityExtensions
{
private static readonly HashSet<StringView> s_skipDeclarationWarningStrings = new()
{
"GENERATED_BODY",
"GENERATED_IINTERFACE_BODY",
"GENERATED_UCLASS_BODY",
"GENERATED_UINTERFACE_BODY",
"GENERATED_USTRUCT_BODY",
// Leaving these disabled ATM since they can exist in the code without causing compile issues
//"RIGVM_METHOD",
//"UCLASS",
//"UDELEGATE",
//"UENUM",
//"UFUNCTION",
//"UINTERFACE",
//"UPROPERTY",
//"USTRUCT",
};
/// <summary>
/// Try to parse an optional _API macro
/// </summary>
/// <param name="tokenReader">Token reader</param>
/// <param name="apiMacroToken">_API macro parsed</param>
/// <returns>True if an _API macro was parsed</returns>
public static bool TryOptionalAPIMacro(this IUhtTokenReader tokenReader, out UhtToken apiMacroToken)
{
ref UhtToken token = ref tokenReader.PeekToken();
if (token.IsIdentifier() && token.Value.Span.EndsWith("_API"))
{
apiMacroToken = token;
tokenReader.ConsumeToken();
return true;
}
apiMacroToken = new UhtToken();
return false;
}
/// <summary>
/// Given a declaration/statement that starts with the given token, skip the declaration in the header.
/// </summary>
/// <param name="tokenReader">Token reader</param>
/// <param name="token">Token that started the process</param>
/// <returns>true if there could be more header to process, false if the end was reached.</returns>
public static bool SkipDeclaration(this IUhtTokenReader tokenReader, ref UhtToken token)
{
// Consume all tokens until the end of declaration/definition has been found.
int nestedScopes = 0;
bool endOfDeclarationFound = false;
// Store the current value of PrevComment so it can be restored after we parsed everything.
{
using UhtTokenDisableComments disableComments = new(tokenReader);
// Check if this is a class/struct declaration in which case it can be followed by member variable declaration.
bool possiblyClassDeclaration = token.IsIdentifier() && (token.IsValue("class") || token.IsValue("struct"));
// (known) macros can end without ; or } so use () to find the end of the declaration.
// However, we don't want to use it with DECLARE_FUNCTION, because we need it to be treated like a function.
bool macroDeclaration = IsProbablyAMacro(token.Value) && !token.IsIdentifier("DECLARE_FUNCTION");
bool definitionFound = false;
char openingBracket = macroDeclaration ? '(' : '{';
char closingBracket = macroDeclaration ? ')' : '}';
bool retestCurrentToken = false;
while (true)
{
if (!retestCurrentToken)
{
token = tokenReader.GetToken();
if (token.TokenType.IsEndType())
{
break;
}
}
else
{
retestCurrentToken = false;
}
ReadOnlySpan<char> span = token.Value.Span;
// If this is a macro, consume it
// If we find parentheses at top-level and we think it's a class declaration then it's more likely
// to be something like: class UThing* GetThing();
if (possiblyClassDeclaration && nestedScopes == 0 && token.IsSymbol() && span.Length == 1 && span[0] == '(')
{
possiblyClassDeclaration = false;
}
if (token.IsSymbol() && span.Length == 1 && span[0] == ';' && nestedScopes == 0)
{
endOfDeclarationFound = true;
break;
}
if (token.IsIdentifier())
{
// Use a trivial pre-filter to avoid doing the search on things that aren't UE keywords we care about
if (span[0] == 'G' || span[0] == 'R' || span[0] == 'U')
{
if (s_skipDeclarationWarningStrings.Contains(token.Value))
{
tokenReader.LogWarning($"The identifier \'{token.Value}\' was detected in a block being skipped. Was this intentional?");
}
}
}
if (!macroDeclaration && token.IsIdentifier() && span.Equals("PURE_VIRTUAL", StringComparison.Ordinal) && nestedScopes == 0)
{
openingBracket = '(';
closingBracket = ')';
}
if (token.IsSymbol() && span.Length == 1 && span[0] == openingBracket)
{
// This is a function definition or class declaration.
definitionFound = true;
nestedScopes++;
}
else if (token.IsSymbol() && span.Length == 1 && span[0] == closingBracket)
{
nestedScopes--;
if (nestedScopes == 0)
{
// Could be a class declaration in all capitals, and not a macro
bool reallyEndDeclaration = true;
if (macroDeclaration)
{
reallyEndDeclaration = !tokenReader.TryPeekOptional('{');
}
if (reallyEndDeclaration)
{
endOfDeclarationFound = true;
break;
}
}
if (nestedScopes < 0)
{
throw new UhtException(tokenReader, token.InputLine, $"Unexpected '{closingBracket}'. Did you miss a semi-colon?");
}
}
else if (macroDeclaration && nestedScopes == 0)
{
macroDeclaration = false;
openingBracket = '{';
closingBracket = '}';
retestCurrentToken = true;
}
}
if (endOfDeclarationFound)
{
// Member variable declaration after class declaration (see bPossiblyClassDeclaration).
if (possiblyClassDeclaration && definitionFound)
{
// Should syntax errors be also handled when someone declares a variable after function definition?
// Consume the variable name.
if (tokenReader.IsEOF)
{
return true;
}
if (tokenReader.TryOptionalIdentifier())
{
tokenReader.Require(';');
}
}
// C++ allows any number of ';' after member declaration/definition.
while (tokenReader.TryOptional(';'))
{
}
}
}
// Successfully consumed C++ declaration unless mismatched pair of brackets has been found.
return nestedScopes == 0 && endOfDeclarationFound;
}
private static bool IsProbablyAMacro(StringView identifier)
{
ReadOnlySpan<char> span = identifier.Span;
if (span.Length == 0)
{
return false;
}
// Macros must start with a capitalized alphanumeric character or underscore
char firstChar = span[0];
if (firstChar != '_' && (firstChar < 'A' || firstChar > 'Z'))
{
return false;
}
// Test for known delegate and event macros.
if (span.StartsWith("DECLARE_MULTICAST_DELEGATE") ||
span.StartsWith("DECLARE_DELEGATE") ||
span.StartsWith("DECLARE_EVENT"))
{
return true;
}
// Failing that, we'll guess about it being a macro based on it being a fully-capitalized identifier.
foreach (char ch in span[1..])
{
if (ch != '_' && (ch < 'A' || ch > 'Z') && (ch < '0' || ch > '9'))
{
return false;
}
}
return true;
}
}
}