You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
Added new define as an overall toggle - defaults to off. If turned on the functionality can be disabled using a console variable #rb sara.schvartzman #jira UE-145106 #preflight https://horde.devtools.epicgames.com/job/6230657b306f46da147a3188 #ROBOMERGE-OWNER: halfdan.ingvarsson #ROBOMERGE-AUTHOR: helge.mathee #ROBOMERGE-SOURCE: CL 19384087 via CL 19384325 via CL 19385988 via CL 19397856 via CL 19397869 #ROBOMERGE-BOT: UE5 (Release-Engine-Staging -> Main) (v926-19321884) [CL 19403124 by halfdan ingvarsson in ue5-main branch]
9798 lines
306 KiB
C++
9798 lines
306 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
|
|
#include "HeaderParser.h"
|
|
#include "UnrealHeaderTool.h"
|
|
#include "PropertyTypes.h"
|
|
#include "HAL/FileManager.h"
|
|
#include "Misc/CommandLine.h"
|
|
#include "Misc/ConfigCacheIni.h"
|
|
#include "Misc/FeedbackContext.h"
|
|
#include "UObject/Interface.h"
|
|
#include "GeneratedCodeVersion.h"
|
|
#include "ProfilingDebugging/ScopedTimers.h"
|
|
#include "NativeClassExporter.h"
|
|
#include "EngineAPI.h"
|
|
#include "ClassMaps.h"
|
|
#include "StringUtils.h"
|
|
#include "Misc/DefaultValueHelper.h"
|
|
#include "Manifest.h"
|
|
#include "Math/UnitConversion.h"
|
|
#include "Exceptions.h"
|
|
#include "UnrealTypeDefinitionInfo.h"
|
|
#include "Containers/EnumAsByte.h"
|
|
#include "Algo/AllOf.h"
|
|
#include "Algo/Find.h"
|
|
#include "Algo/FindSortedStringCaseInsensitive.h"
|
|
#include "Misc/ScopeExit.h"
|
|
#include "Misc/Timespan.h"
|
|
#include "HAL/PlatformTime.h"
|
|
#include <atomic>
|
|
|
|
#include "Specifiers/CheckedMetadataSpecifiers.h"
|
|
#include "Specifiers/EnumSpecifiers.h"
|
|
#include "Specifiers/FunctionSpecifiers.h"
|
|
#include "Specifiers/InterfaceSpecifiers.h"
|
|
#include "Specifiers/StructSpecifiers.h"
|
|
#include "Specifiers/VariableSpecifiers.h"
|
|
|
|
// Globals for common class definitions
|
|
extern FUnrealClassDefinitionInfo* GUObjectDef;
|
|
extern FUnrealClassDefinitionInfo* GUClassDef;
|
|
extern FUnrealClassDefinitionInfo* GUInterfaceDef;
|
|
|
|
/*-----------------------------------------------------------------------------
|
|
Constants & declarations.
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
enum {MAX_ARRAY_SIZE=2048};
|
|
|
|
namespace
|
|
{
|
|
static const FName NAME_Comment(TEXT("Comment"));
|
|
static const FName NAME_ToolTip(TEXT("ToolTip"));
|
|
static const FName NAME_DocumentationPolicy(TEXT("DocumentationPolicy"));
|
|
static const FName NAME_AllowPrivateAccess(TEXT("AllowPrivateAccess"));
|
|
static const FName NAME_ExposeOnSpawn(TEXT("ExposeOnSpawn"));
|
|
static const FName NAME_NativeConst(TEXT("NativeConst"));
|
|
static const FName NAME_NativeConstTemplateArg(TEXT("NativeConstTemplateArg"));
|
|
static const FName NAME_BlueprintInternalUseOnly(TEXT("BlueprintInternalUseOnly"));
|
|
static const FName NAME_DeprecatedFunction(TEXT("DeprecatedFunction"));
|
|
static const FName NAME_BlueprintSetter(TEXT("BlueprintSetter"));
|
|
static const FName NAME_BlueprintGetter(TEXT("BlueprintGetter"));
|
|
static const FName NAME_Category(TEXT("Category"));
|
|
static const FName NAME_ReturnValue(TEXT("ReturnValue"));
|
|
static const FName NAME_CppFromBpEvent(TEXT("CppFromBpEvent"));
|
|
static const FName NAME_CustomThunk(TEXT("CustomThunk"));
|
|
static const FName NAME_EditInline(TEXT("EditInline"));
|
|
static const FName NAME_IncludePath(TEXT("IncludePath"));
|
|
static const FName NAME_ModuleRelativePath(TEXT("ModuleRelativePath"));
|
|
static const FName NAME_IsBlueprintBase(TEXT("IsBlueprintBase"));
|
|
static const FName NAME_CannotImplementInterfaceInBlueprint(TEXT("CannotImplementInterfaceInBlueprint"));
|
|
static const FName NAME_UIMin(TEXT("UIMin"));
|
|
static const FName NAME_UIMax(TEXT("UIMax"));
|
|
}
|
|
|
|
const FName FHeaderParserNames::NAME_HideCategories(TEXT("HideCategories"));
|
|
const FName FHeaderParserNames::NAME_ShowCategories(TEXT("ShowCategories"));
|
|
const FName FHeaderParserNames::NAME_SparseClassDataTypes(TEXT("SparseClassDataTypes"));
|
|
const FName FHeaderParserNames::NAME_IsConversionRoot(TEXT("IsConversionRoot"));
|
|
const FName FHeaderParserNames::NAME_BlueprintType(TEXT("BlueprintType"));
|
|
const FName FHeaderParserNames::NAME_AutoCollapseCategories(TEXT("AutoCollapseCategories"));
|
|
const FName FHeaderParserNames::NAME_HideFunctions(TEXT("HideFunctions"));
|
|
const FName FHeaderParserNames::NAME_AutoExpandCategories(TEXT("AutoExpandCategories"));
|
|
const FName FHeaderParserNames::NAME_PrioritizeCategories(TEXT("PrioritizeCategories"));
|
|
|
|
TArray<FString> FHeaderParser::PropertyCPPTypesRequiringUIRanges = { TEXT("float"), TEXT("double") };
|
|
TArray<FString> FHeaderParser::ReservedTypeNames = { TEXT("none") };
|
|
|
|
// Busy wait support for holes in the include graph issues
|
|
extern bool bGoWide;
|
|
extern std::atomic<bool> GSourcesConcurrent;
|
|
extern std::atomic<int> GSourcesToParse;
|
|
extern std::atomic<int> GSourcesParsing;
|
|
extern std::atomic<int> GSourcesCompleted;
|
|
extern std::atomic<int> GSourcesStalled;
|
|
|
|
/*-----------------------------------------------------------------------------
|
|
Utility functions.
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
namespace
|
|
{
|
|
bool ProbablyAMacro(const FStringView& Identifier)
|
|
{
|
|
if (Identifier.Len() == 0)
|
|
{
|
|
return false;
|
|
|
|
}
|
|
// Macros must start with a capitalized alphanumeric character or underscore
|
|
TCHAR FirstChar = Identifier[0];
|
|
if (FirstChar != TEXT('_') && (FirstChar < TEXT('A') || FirstChar > TEXT('Z')))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Test for known delegate and event macros.
|
|
if (Identifier.StartsWith(TEXT("DECLARE_MULTICAST_DELEGATE")) ||
|
|
Identifier.StartsWith(TEXT("DECLARE_DELEGATE")) ||
|
|
Identifier.StartsWith(TEXT("DECLARE_EVENT")))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Failing that, we'll guess about it being a macro based on it being a fully-capitalized identifier.
|
|
for (TCHAR Ch : Identifier.RightChop(1))
|
|
{
|
|
if (Ch != TEXT('_') && (Ch < TEXT('A') || Ch > TEXT('Z')) && (Ch < TEXT('0') || Ch > TEXT('9')))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Tests if an identifier looks like a macro which doesn't have a following open parenthesis.
|
|
*
|
|
* @param HeaderParser The parser to retrieve the next token.
|
|
* @param Token The token to test for being callable-macro-like.
|
|
*
|
|
* @return true if it looks like a non-callable macro, false otherwise.
|
|
*/
|
|
bool ProbablyAnUnknownObjectLikeMacro(FHeaderParser& HeaderParser, FToken Token)
|
|
{
|
|
// Non-identifiers are not macros
|
|
if (!Token.IsIdentifier())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Macros must start with a capitalized alphanumeric character or underscore
|
|
TCHAR FirstChar = Token.Value[0];
|
|
if (FirstChar != TEXT('_') && (FirstChar < TEXT('A') || FirstChar > TEXT('Z')))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// We'll guess about it being a macro based on it being fully-capitalized with at least one underscore.
|
|
int32 UnderscoreCount = 0;
|
|
for (TCHAR Ch : Token.Value.RightChop(1))
|
|
{
|
|
if (Ch == TEXT('_'))
|
|
{
|
|
++UnderscoreCount;
|
|
}
|
|
else if ((Ch < TEXT('A') || Ch > TEXT('Z')) && (Ch < TEXT('0') || Ch > TEXT('9')))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 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 false;
|
|
}
|
|
|
|
// Identifiers which end in _API are known
|
|
if (Token.Value.Len() > 4 && Token.Value.EndsWith(FStringView(TEXT("_API"), 4)))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Ignore certain known macros or identifiers that look like macros.
|
|
if (Token.IsValue(TEXT("FORCEINLINE_DEBUGGABLE"), ESearchCase::CaseSensitive) ||
|
|
Token.IsValue(TEXT("FORCEINLINE_STATS"), ESearchCase::CaseSensitive) ||
|
|
Token.IsValue(TEXT("SIZE_T"), ESearchCase::CaseSensitive))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Check if there's an open parenthesis following the token.
|
|
//
|
|
// Rather than ungetting the bracket token, we unget the original identifier token,
|
|
// then get it again, so we don't lose any comments which may exist between the token
|
|
// and the non-bracket.
|
|
FToken PossibleBracketToken;
|
|
HeaderParser.GetToken(PossibleBracketToken);
|
|
HeaderParser.UngetToken(Token);
|
|
HeaderParser.GetToken(Token);
|
|
|
|
return !PossibleBracketToken.IsSymbol(TEXT('('));
|
|
}
|
|
|
|
/**
|
|
* Parse and validate an array of identifiers (inside FUNC_NetRequest, FUNC_NetResponse)
|
|
* @param FuncInfo function info for the current function
|
|
* @param Identifiers identifiers inside the net service declaration
|
|
*/
|
|
void ParseNetServiceIdentifiers(FHeaderParser& HeaderParser, FFuncInfo& FuncInfo, const TArray<FString>& Identifiers)
|
|
{
|
|
static const auto& IdTag = TEXT("Id");
|
|
static const auto& ResponseIdTag = TEXT("ResponseId");
|
|
static const auto& JSBridgePriTag = TEXT("Priority");
|
|
|
|
for (const FString& Identifier : Identifiers)
|
|
{
|
|
const TCHAR* IdentifierPtr = *Identifier;
|
|
|
|
if (const TCHAR* Equals = FCString::Strchr(IdentifierPtr, TEXT('=')))
|
|
{
|
|
// It's a tag with an argument
|
|
|
|
if (FCString::Strnicmp(IdentifierPtr, IdTag, UE_ARRAY_COUNT(IdTag) - 1) == 0)
|
|
{
|
|
int32 TempInt = FCString::Atoi(Equals + 1);
|
|
if (TempInt <= 0 || TempInt > MAX_uint16)
|
|
{
|
|
HeaderParser.Throwf(TEXT("Invalid network identifier %s for function"), IdentifierPtr);
|
|
}
|
|
FuncInfo.RPCId = (uint16)TempInt;
|
|
}
|
|
else if (FCString::Strnicmp(IdentifierPtr, ResponseIdTag, UE_ARRAY_COUNT(ResponseIdTag) - 1) == 0 ||
|
|
FCString::Strnicmp(IdentifierPtr, JSBridgePriTag, UE_ARRAY_COUNT(JSBridgePriTag) - 1) == 0)
|
|
{
|
|
int32 TempInt = FCString::Atoi(Equals + 1);
|
|
if (TempInt <= 0 || TempInt > MAX_uint16)
|
|
{
|
|
HeaderParser.Throwf(TEXT("Invalid network identifier %s for function"), IdentifierPtr);
|
|
}
|
|
FuncInfo.RPCResponseId = (uint16)TempInt;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Assume it's an endpoint name
|
|
|
|
if (FuncInfo.EndpointName.Len())
|
|
{
|
|
HeaderParser.Throwf(TEXT("Function should not specify multiple endpoints - '%s' found but already using '%s'"), *Identifier);
|
|
}
|
|
|
|
FuncInfo.EndpointName = Identifier;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Processes a set of UFUNCTION or UDELEGATE specifiers into an FFuncInfo struct.
|
|
*
|
|
* @param FuncInfo - The FFuncInfo object to populate.
|
|
* @param Specifiers - The specifiers to process.
|
|
*/
|
|
void ProcessFunctionSpecifiers(FHeaderParser& HeaderParser, FFuncInfo& FuncInfo, const TArray<FPropertySpecifier>& Specifiers, TMap<FName, FString>& MetaData)
|
|
{
|
|
bool bSpecifiedUnreliable = false;
|
|
bool bSawPropertyAccessor = false;
|
|
|
|
for (const FPropertySpecifier& Specifier : Specifiers)
|
|
{
|
|
switch ((EFunctionSpecifier)Algo::FindSortedStringCaseInsensitive(*Specifier.Key, GFunctionSpecifierStrings))
|
|
{
|
|
default:
|
|
{
|
|
HeaderParser.Throwf(TEXT("Unknown function specifier '%s'"), *Specifier.Key);
|
|
}
|
|
break;
|
|
|
|
case EFunctionSpecifier::BlueprintNativeEvent:
|
|
{
|
|
if (FuncInfo.FunctionFlags & FUNC_Net)
|
|
{
|
|
HeaderParser.LogError(TEXT("BlueprintNativeEvent functions cannot be replicated!") );
|
|
}
|
|
else if ( (FuncInfo.FunctionFlags & FUNC_BlueprintEvent) && !(FuncInfo.FunctionFlags & FUNC_Native) )
|
|
{
|
|
// already a BlueprintImplementableEvent
|
|
HeaderParser.LogError(TEXT("A function cannot be both BlueprintNativeEvent and BlueprintImplementableEvent!") );
|
|
}
|
|
else if (bSawPropertyAccessor)
|
|
{
|
|
HeaderParser.LogError(TEXT("A function cannot be both BlueprintNativeEvent and a Blueprint Property accessor!"));
|
|
}
|
|
else if ( (FuncInfo.FunctionFlags & FUNC_Private) )
|
|
{
|
|
HeaderParser.LogError(TEXT("A Private function cannot be a BlueprintNativeEvent!") );
|
|
}
|
|
|
|
FuncInfo.FunctionFlags |= FUNC_Event;
|
|
FuncInfo.FunctionFlags |= FUNC_BlueprintEvent;
|
|
}
|
|
break;
|
|
|
|
case EFunctionSpecifier::BlueprintImplementableEvent:
|
|
{
|
|
if (FuncInfo.FunctionFlags & FUNC_Net)
|
|
{
|
|
HeaderParser.LogError(TEXT("BlueprintImplementableEvent functions cannot be replicated!") );
|
|
}
|
|
else if ( (FuncInfo.FunctionFlags & FUNC_BlueprintEvent) && (FuncInfo.FunctionFlags & FUNC_Native) )
|
|
{
|
|
// already a BlueprintNativeEvent
|
|
HeaderParser.LogError(TEXT("A function cannot be both BlueprintNativeEvent and BlueprintImplementableEvent!") );
|
|
}
|
|
else if (bSawPropertyAccessor)
|
|
{
|
|
HeaderParser.LogError(TEXT("A function cannot be both BlueprintImplementableEvent and a Blueprint Property accessor!"));
|
|
}
|
|
else if ( (FuncInfo.FunctionFlags & FUNC_Private) )
|
|
{
|
|
HeaderParser.LogError(TEXT("A Private function cannot be a BlueprintImplementableEvent!") );
|
|
}
|
|
|
|
FuncInfo.FunctionFlags |= FUNC_Event;
|
|
FuncInfo.FunctionFlags |= FUNC_BlueprintEvent;
|
|
FuncInfo.FunctionFlags &= ~FUNC_Native;
|
|
}
|
|
break;
|
|
|
|
case EFunctionSpecifier::Exec:
|
|
{
|
|
FuncInfo.FunctionFlags |= FUNC_Exec;
|
|
if( FuncInfo.FunctionFlags & FUNC_Net )
|
|
{
|
|
HeaderParser.LogError(TEXT("Exec functions cannot be replicated!") );
|
|
}
|
|
}
|
|
break;
|
|
|
|
case EFunctionSpecifier::SealedEvent:
|
|
{
|
|
FuncInfo.bSealedEvent = true;
|
|
}
|
|
break;
|
|
|
|
case EFunctionSpecifier::Server:
|
|
{
|
|
if ((FuncInfo.FunctionFlags & FUNC_BlueprintEvent) != 0)
|
|
{
|
|
HeaderParser.Throwf(TEXT("BlueprintImplementableEvent or BlueprintNativeEvent functions cannot be declared as Client or Server"));
|
|
}
|
|
|
|
FuncInfo.FunctionFlags |= FUNC_Net;
|
|
FuncInfo.FunctionFlags |= FUNC_NetServer;
|
|
|
|
if (Specifier.Values.Num())
|
|
{
|
|
FuncInfo.CppImplName = Specifier.Values[0];
|
|
}
|
|
|
|
if( FuncInfo.FunctionFlags & FUNC_Exec )
|
|
{
|
|
HeaderParser.LogError(TEXT("Exec functions cannot be replicated!") );
|
|
}
|
|
}
|
|
break;
|
|
|
|
case EFunctionSpecifier::Client:
|
|
{
|
|
if ((FuncInfo.FunctionFlags & FUNC_BlueprintEvent) != 0)
|
|
{
|
|
HeaderParser.Throwf(TEXT("BlueprintImplementableEvent or BlueprintNativeEvent functions cannot be declared as Client or Server"));
|
|
}
|
|
|
|
FuncInfo.FunctionFlags |= FUNC_Net;
|
|
FuncInfo.FunctionFlags |= FUNC_NetClient;
|
|
|
|
if (Specifier.Values.Num())
|
|
{
|
|
FuncInfo.CppImplName = Specifier.Values[0];
|
|
}
|
|
}
|
|
break;
|
|
|
|
case EFunctionSpecifier::NetMulticast:
|
|
{
|
|
if ((FuncInfo.FunctionFlags & FUNC_BlueprintEvent) != 0)
|
|
{
|
|
HeaderParser.Throwf(TEXT("BlueprintImplementableEvent or BlueprintNativeEvent functions cannot be declared as Multicast"));
|
|
}
|
|
|
|
FuncInfo.FunctionFlags |= FUNC_Net;
|
|
FuncInfo.FunctionFlags |= FUNC_NetMulticast;
|
|
}
|
|
break;
|
|
|
|
case EFunctionSpecifier::ServiceRequest:
|
|
{
|
|
if ((FuncInfo.FunctionFlags & FUNC_BlueprintEvent) != 0)
|
|
{
|
|
HeaderParser.Throwf(TEXT("BlueprintImplementableEvent or BlueprintNativeEvent functions cannot be declared as a ServiceRequest"));
|
|
}
|
|
|
|
FuncInfo.FunctionFlags |= FUNC_Net;
|
|
FuncInfo.FunctionFlags |= FUNC_NetReliable;
|
|
FuncInfo.FunctionFlags |= FUNC_NetRequest;
|
|
FuncInfo.FunctionExportFlags |= FUNCEXPORT_CustomThunk;
|
|
|
|
ParseNetServiceIdentifiers(HeaderParser, FuncInfo, Specifier.Values);
|
|
|
|
if (FuncInfo.EndpointName.Len() == 0)
|
|
{
|
|
HeaderParser.Throwf(TEXT("ServiceRequest needs to specify an endpoint name"));
|
|
}
|
|
}
|
|
break;
|
|
|
|
case EFunctionSpecifier::ServiceResponse:
|
|
{
|
|
if ((FuncInfo.FunctionFlags & FUNC_BlueprintEvent) != 0)
|
|
{
|
|
HeaderParser.Throwf(TEXT("BlueprintImplementableEvent or BlueprintNativeEvent functions cannot be declared as a ServiceResponse"));
|
|
}
|
|
|
|
FuncInfo.FunctionFlags |= FUNC_Net;
|
|
FuncInfo.FunctionFlags |= FUNC_NetReliable;
|
|
FuncInfo.FunctionFlags |= FUNC_NetResponse;
|
|
|
|
ParseNetServiceIdentifiers(HeaderParser, FuncInfo, Specifier.Values);
|
|
|
|
if (FuncInfo.EndpointName.Len() == 0)
|
|
{
|
|
HeaderParser.Throwf(TEXT("ServiceResponse needs to specify an endpoint name"));
|
|
}
|
|
}
|
|
break;
|
|
|
|
case EFunctionSpecifier::Reliable:
|
|
{
|
|
FuncInfo.FunctionFlags |= FUNC_NetReliable;
|
|
}
|
|
break;
|
|
|
|
case EFunctionSpecifier::Unreliable:
|
|
{
|
|
bSpecifiedUnreliable = true;
|
|
}
|
|
break;
|
|
|
|
case EFunctionSpecifier::CustomThunk:
|
|
{
|
|
FuncInfo.FunctionExportFlags |= FUNCEXPORT_CustomThunk;
|
|
}
|
|
break;
|
|
|
|
case EFunctionSpecifier::BlueprintCallable:
|
|
{
|
|
FuncInfo.FunctionFlags |= FUNC_BlueprintCallable;
|
|
}
|
|
break;
|
|
|
|
case EFunctionSpecifier::BlueprintGetter:
|
|
{
|
|
if (FuncInfo.FunctionFlags & FUNC_Event)
|
|
{
|
|
HeaderParser.LogError(TEXT("Function cannot be a blueprint event and a blueprint getter."));
|
|
}
|
|
|
|
bSawPropertyAccessor = true;
|
|
FuncInfo.FunctionFlags |= FUNC_BlueprintCallable;
|
|
FuncInfo.FunctionFlags |= FUNC_BlueprintPure;
|
|
MetaData.Add(NAME_BlueprintGetter);
|
|
}
|
|
break;
|
|
|
|
case EFunctionSpecifier::BlueprintSetter:
|
|
{
|
|
if (FuncInfo.FunctionFlags & FUNC_Event)
|
|
{
|
|
HeaderParser.LogError(TEXT("Function cannot be a blueprint event and a blueprint setter."));
|
|
}
|
|
|
|
bSawPropertyAccessor = true;
|
|
FuncInfo.FunctionFlags |= FUNC_BlueprintCallable;
|
|
MetaData.Add(NAME_BlueprintSetter);
|
|
}
|
|
break;
|
|
|
|
case EFunctionSpecifier::BlueprintPure:
|
|
{
|
|
bool bIsPure = true;
|
|
if (Specifier.Values.Num() == 1)
|
|
{
|
|
FString IsPureStr = Specifier.Values[0];
|
|
bIsPure = IsPureStr.ToBool();
|
|
}
|
|
|
|
// This function can be called, and is also pure.
|
|
FuncInfo.FunctionFlags |= FUNC_BlueprintCallable;
|
|
|
|
if (bIsPure)
|
|
{
|
|
FuncInfo.FunctionFlags |= FUNC_BlueprintPure;
|
|
}
|
|
else
|
|
{
|
|
FuncInfo.bForceBlueprintImpure = true;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case EFunctionSpecifier::BlueprintAuthorityOnly:
|
|
{
|
|
FuncInfo.FunctionFlags |= FUNC_BlueprintAuthorityOnly;
|
|
}
|
|
break;
|
|
|
|
case EFunctionSpecifier::BlueprintCosmetic:
|
|
{
|
|
FuncInfo.FunctionFlags |= FUNC_BlueprintCosmetic;
|
|
}
|
|
break;
|
|
|
|
case EFunctionSpecifier::WithValidation:
|
|
{
|
|
FuncInfo.FunctionFlags |= FUNC_NetValidate;
|
|
|
|
if (Specifier.Values.Num())
|
|
{
|
|
FuncInfo.CppValidationImplName = Specifier.Values[0];
|
|
}
|
|
}
|
|
break;
|
|
case EFunctionSpecifier::FieldNotify:
|
|
{
|
|
FuncInfo.bFieldNotify = true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (FuncInfo.FunctionFlags & FUNC_Net)
|
|
{
|
|
// Network replicated functions are always events
|
|
FuncInfo.FunctionFlags |= FUNC_Event;
|
|
|
|
check(!(FuncInfo.FunctionFlags & (FUNC_BlueprintEvent | FUNC_Exec)));
|
|
|
|
bool bIsNetService = !!(FuncInfo.FunctionFlags & (FUNC_NetRequest | FUNC_NetResponse));
|
|
bool bIsNetReliable = !!(FuncInfo.FunctionFlags & FUNC_NetReliable);
|
|
|
|
if (FuncInfo.FunctionFlags & FUNC_Static)
|
|
{
|
|
HeaderParser.LogError(TEXT("Static functions can't be replicated"));
|
|
}
|
|
|
|
if (!bIsNetReliable && !bSpecifiedUnreliable && !bIsNetService)
|
|
{
|
|
HeaderParser.LogError(TEXT("Replicated function: 'reliable' or 'unreliable' is required"));
|
|
}
|
|
|
|
if (bIsNetReliable && bSpecifiedUnreliable && !bIsNetService)
|
|
{
|
|
HeaderParser.LogError(TEXT("'reliable' and 'unreliable' are mutually exclusive"));
|
|
}
|
|
}
|
|
else if (FuncInfo.FunctionFlags & FUNC_NetReliable)
|
|
{
|
|
HeaderParser.LogError(TEXT("'reliable' specified without 'client' or 'server'"));
|
|
}
|
|
else if (bSpecifiedUnreliable)
|
|
{
|
|
HeaderParser.LogError(TEXT("'unreliable' specified without 'client' or 'server'"));
|
|
}
|
|
|
|
if (FuncInfo.bSealedEvent && !(FuncInfo.FunctionFlags & FUNC_Event))
|
|
{
|
|
HeaderParser.LogError(TEXT("SealedEvent may only be used on events"));
|
|
}
|
|
|
|
if (FuncInfo.bSealedEvent && FuncInfo.FunctionFlags & FUNC_BlueprintEvent)
|
|
{
|
|
HeaderParser.LogError(TEXT("SealedEvent cannot be used on Blueprint events"));
|
|
}
|
|
|
|
if (FuncInfo.bForceBlueprintImpure && (FuncInfo.FunctionFlags & FUNC_BlueprintPure) != 0)
|
|
{
|
|
HeaderParser.LogError(TEXT("BlueprintPure (or BlueprintPure=true) and BlueprintPure=false should not both appear on the same function, they are mutually exclusive"));
|
|
}
|
|
}
|
|
|
|
const TCHAR* GetHintText(FHeaderParser& HeaderParser, EVariableCategory VariableCategory)
|
|
{
|
|
switch (VariableCategory)
|
|
{
|
|
case EVariableCategory::ReplicatedParameter:
|
|
case EVariableCategory::RegularParameter:
|
|
return TEXT("Function parameter");
|
|
|
|
case EVariableCategory::Return:
|
|
return TEXT("Function return type");
|
|
|
|
case EVariableCategory::Member:
|
|
return TEXT("Member variable declaration");
|
|
|
|
default:
|
|
HeaderParser.Throwf(TEXT("Unknown variable category"));
|
|
}
|
|
|
|
// Unreachable
|
|
check(false);
|
|
return nullptr;
|
|
}
|
|
|
|
void SkipAlignasIfNecessary(FBaseParser& Parser)
|
|
{
|
|
if (Parser.MatchIdentifier(TEXT("alignas"), ESearchCase::CaseSensitive))
|
|
{
|
|
Parser.RequireSymbol(TEXT('('), TEXT("'alignas'"));
|
|
Parser.RequireAnyConstInt(TEXT("'alignas'"));
|
|
Parser.RequireSymbol(TEXT(')'), TEXT("'alignas'"));
|
|
}
|
|
}
|
|
|
|
void SkipDeprecatedMacroIfNecessary(FBaseParser& Parser)
|
|
{
|
|
FToken MacroToken;
|
|
if (!Parser.GetToken(MacroToken))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!MacroToken.IsIdentifier() ||
|
|
(!MacroToken.IsValue(TEXT("DEPRECATED"), ESearchCase::CaseSensitive) && !MacroToken.IsValue(TEXT("UE_DEPRECATED"), ESearchCase::CaseSensitive)))
|
|
{
|
|
Parser.UngetToken(MacroToken);
|
|
return;
|
|
}
|
|
|
|
auto ErrorMessageGetter = [&MacroToken]() { return FString::Printf(TEXT("%s macro"), *MacroToken.GetTokenValue()); };
|
|
|
|
Parser.RequireSymbol(TEXT('('), ErrorMessageGetter);
|
|
|
|
FToken Token;
|
|
if (Parser.GetToken(Token) && !Token.IsConstFloat())
|
|
{
|
|
Parser.Throwf(TEXT("Expected engine version in %s macro"), *MacroToken.GetTokenValue());
|
|
}
|
|
|
|
Parser.RequireSymbol(TEXT(','), ErrorMessageGetter);
|
|
if (Parser.GetToken(Token) && !Token.IsConstString())
|
|
{
|
|
Parser.Throwf(TEXT("Expected deprecation message in %s macro"), *MacroToken.GetTokenValue());
|
|
}
|
|
|
|
Parser.RequireSymbol(TEXT(')'), ErrorMessageGetter);
|
|
}
|
|
|
|
void SkipAlignasAndDeprecatedMacroIfNecessary(FBaseParser& Parser)
|
|
{
|
|
// alignas() can come before or after the deprecation macro.
|
|
// We can't have both, but the compiler will catch that anyway.
|
|
SkipAlignasIfNecessary(Parser);
|
|
SkipDeprecatedMacroIfNecessary(Parser);
|
|
SkipAlignasIfNecessary(Parser);
|
|
}
|
|
|
|
inline EPointerMemberBehavior ToPointerMemberBehavior(const FString& InString)
|
|
{
|
|
if (InString.Compare(TEXT("Disallow")) == 0)
|
|
{
|
|
return EPointerMemberBehavior::Disallow;
|
|
}
|
|
|
|
if (InString.Compare(TEXT("AllowSilently")) == 0)
|
|
{
|
|
return EPointerMemberBehavior::AllowSilently;
|
|
}
|
|
|
|
if (InString.Compare(TEXT("AllowAndLog")) == 0)
|
|
{
|
|
return EPointerMemberBehavior::AllowAndLog;
|
|
}
|
|
|
|
FUHTMessage(FString(TEXT("Configuration"))).Throwf(TEXT("Unrecognized native pointer member behavior: %s"), *InString);
|
|
return EPointerMemberBehavior::Disallow;
|
|
}
|
|
|
|
static const TCHAR* GLayoutMacroNames[] = {
|
|
TEXT("LAYOUT_ARRAY"),
|
|
TEXT("LAYOUT_ARRAY_EDITORONLY"),
|
|
TEXT("LAYOUT_BITFIELD"),
|
|
TEXT("LAYOUT_BITFIELD_EDITORONLY"),
|
|
TEXT("LAYOUT_FIELD"),
|
|
TEXT("LAYOUT_FIELD_EDITORONLY"),
|
|
TEXT("LAYOUT_FIELD_INITIALIZED"),
|
|
};
|
|
|
|
FCriticalSection GlobalDelegatesLock;
|
|
TMap<FString, FUnrealFunctionDefinitionInfo*> GlobalDelegates;
|
|
|
|
template <typename LambdaType>
|
|
bool BusyWait(LambdaType&& Lambda)
|
|
{
|
|
constexpr double TimeoutInSeconds = 5.0;
|
|
|
|
// Try it first
|
|
if (Lambda())
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// If we aren't doing concurrent processing, then there isn't any reason to continue, we will never make any progress.
|
|
if (!bGoWide)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Mark that we are stalled
|
|
ON_SCOPE_EXIT
|
|
{
|
|
--GSourcesStalled;
|
|
};
|
|
++GSourcesStalled;
|
|
|
|
int SourcesToParse = GSourcesToParse;
|
|
int SourcesParsing = GSourcesParsing;
|
|
int SourcesCompleted = GSourcesCompleted;
|
|
int SourcesStalled = GSourcesStalled;
|
|
double StartTime = FPlatformTime::Seconds();
|
|
for (;;)
|
|
{
|
|
FPlatformProcess::YieldCycles(10000);
|
|
if (Lambda())
|
|
{
|
|
return true;
|
|
}
|
|
|
|
int NewSourcesParsing = GSourcesParsing;
|
|
int NewSourcesCompleted = GSourcesCompleted;
|
|
int NewSourcesStalled = GSourcesStalled;
|
|
|
|
// If everything is stalled, then we have no hope
|
|
if (NewSourcesStalled + NewSourcesCompleted == SourcesToParse)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// If something has completed, then reset the timeout
|
|
double CurrentTime = FPlatformTime::Seconds();
|
|
if (NewSourcesCompleted != SourcesCompleted || NewSourcesParsing != SourcesParsing)
|
|
{
|
|
StartTime = CurrentTime;
|
|
}
|
|
|
|
// Otherwise, check for long timeout
|
|
else if (CurrentTime - StartTime > TimeoutInSeconds)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Remember state
|
|
SourcesParsing = NewSourcesParsing;
|
|
SourcesCompleted = NewSourcesCompleted;
|
|
SourcesStalled = NewSourcesStalled;
|
|
}
|
|
}
|
|
}
|
|
|
|
void AddEditInlineMetaData(TMap<FName, FString>& MetaData)
|
|
{
|
|
MetaData.Add(NAME_EditInline, TEXT("true"));
|
|
}
|
|
|
|
FUHTConfig::FUHTConfig()
|
|
{
|
|
// Read Ini options, GConfig must exist by this point
|
|
check(GConfig);
|
|
|
|
const FName TypeRedirectsKey(TEXT("TypeRedirects"));
|
|
const FName StructsWithNoPrefixKey(TEXT("StructsWithNoPrefix"));
|
|
const FName StructsWithTPrefixKey(TEXT("StructsWithTPrefix"));
|
|
const FName DelegateParameterCountStringsKey(TEXT("DelegateParameterCountStrings"));
|
|
const FName GeneratedCodeVersionKey(TEXT("GeneratedCodeVersion"));
|
|
const FName EngineNativePointerMemberBehaviorKey(TEXT("EngineNativePointerMemberBehavior"));
|
|
const FName EngineObjectPtrMemberBehaviorKey(TEXT("EngineObjectPtrMemberBehavior"));
|
|
const FName NonEngineNativePointerMemberBehaviorKey(TEXT("NonEngineNativePointerMemberBehavior"));
|
|
const FName NonEngineObjectPtrMemberBehaviorKey(TEXT("NonEngineObjectPtrMemberBehavior"));
|
|
const FName AutomaticSettersAndGettersKey(TEXT("AutomaticSettersAndGetters"));
|
|
|
|
FConfigSection* ConfigSection = GConfig->GetSectionPrivate(TEXT("UnrealHeaderTool"), false, true, GEngineIni);
|
|
if (ConfigSection)
|
|
{
|
|
for (FConfigSection::TIterator It(*ConfigSection); It; ++It)
|
|
{
|
|
if (It.Key() == TypeRedirectsKey)
|
|
{
|
|
FString OldType;
|
|
FString NewType;
|
|
|
|
FParse::Value(*It.Value().GetValue(), TEXT("OldType="), OldType);
|
|
FParse::Value(*It.Value().GetValue(), TEXT("NewType="), NewType);
|
|
|
|
TypeRedirectMap.Add(MoveTemp(OldType), MoveTemp(NewType));
|
|
}
|
|
else if (It.Key() == StructsWithNoPrefixKey)
|
|
{
|
|
StructsWithNoPrefix.Add(It.Value().GetValue());
|
|
}
|
|
else if (It.Key() == StructsWithTPrefixKey)
|
|
{
|
|
StructsWithTPrefix.Add(It.Value().GetValue());
|
|
}
|
|
else if (It.Key() == DelegateParameterCountStringsKey)
|
|
{
|
|
DelegateParameterCountStrings.Add(It.Value().GetValue());
|
|
}
|
|
else if (It.Key() == GeneratedCodeVersionKey)
|
|
{
|
|
DefaultGeneratedCodeVersion = ToGeneratedCodeVersion(It.Value().GetValue());
|
|
}
|
|
else if (It.Key() == EngineNativePointerMemberBehaviorKey)
|
|
{
|
|
EngineNativePointerMemberBehavior = ToPointerMemberBehavior(It.Value().GetValue());
|
|
}
|
|
else if (It.Key() == EngineObjectPtrMemberBehaviorKey)
|
|
{
|
|
EngineObjectPtrMemberBehavior = ToPointerMemberBehavior(It.Value().GetValue());
|
|
}
|
|
else if (It.Key() == NonEngineNativePointerMemberBehaviorKey)
|
|
{
|
|
NonEngineNativePointerMemberBehavior = ToPointerMemberBehavior(It.Value().GetValue());
|
|
}
|
|
else if (It.Key() == NonEngineObjectPtrMemberBehaviorKey)
|
|
{
|
|
NonEngineObjectPtrMemberBehavior = ToPointerMemberBehavior(It.Value().GetValue());
|
|
}
|
|
else if (It.Key() == AutomaticSettersAndGettersKey)
|
|
{
|
|
bAllowAutomaticSettersAndGetters = It.Value().GetValue().ToBool();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const FUHTConfig& FUHTConfig::Get()
|
|
{
|
|
static FUHTConfig UHTConfig;
|
|
return UHTConfig;
|
|
}
|
|
|
|
|
|
/////////////////////////////////////////////////////
|
|
// FHeaderParser
|
|
|
|
/*-----------------------------------------------------------------------------
|
|
Code emitting.
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
|
|
//
|
|
// Get a qualified class.
|
|
//
|
|
FUnrealClassDefinitionInfo* FHeaderParser::GetQualifiedClass(const TCHAR* Thing)
|
|
{
|
|
FToken Token;
|
|
if (GetIdentifier(Token))
|
|
{
|
|
RedirectTypeIdentifier(Token);
|
|
return FUnrealClassDefinitionInfo::FindScriptClassOrThrow(*this, FString(Token.Value));
|
|
}
|
|
else
|
|
{
|
|
Throwf(TEXT("%s: Missing class name"), Thing);
|
|
}
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------
|
|
Fields.
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
/**
|
|
* Find a function in the specified context. Starts with the specified scope, then iterates
|
|
* through the Outer chain until the field is found.
|
|
*
|
|
* @param InScope scope to start searching for the field in
|
|
* @param InIdentifier name of the field we're searching for
|
|
* @param bIncludeParents whether to allow searching in the scope of a parent struct
|
|
* @param Thing hint text that will be used in the error message if an error is encountered
|
|
*
|
|
* @return a pointer to a UField with a name matching InIdentifier, or NULL if it wasn't found
|
|
*/
|
|
FUnrealFunctionDefinitionInfo* FHeaderParser::FindFunction
|
|
(
|
|
|
|
const FUnrealStructDefinitionInfo& InScope,
|
|
const TCHAR* InIdentifier,
|
|
bool bIncludeParents,
|
|
const TCHAR* Thing
|
|
)
|
|
{
|
|
check(InIdentifier);
|
|
FName InName(InIdentifier, FNAME_Find);
|
|
if (InName != NAME_None)
|
|
{
|
|
for (const FUnrealStructDefinitionInfo* Scope = &InScope; Scope; Scope = UHTCast<FUnrealStructDefinitionInfo>(Scope->GetOuter()))
|
|
{
|
|
for (FUnrealPropertyDefinitionInfo* PropertyDef : TUHTFieldRange<FUnrealPropertyDefinitionInfo>(*Scope))
|
|
{
|
|
if (PropertyDef->GetFName() == InName)
|
|
{
|
|
if (Thing)
|
|
{
|
|
InScope.Throwf(TEXT("%s: expecting function or delegate, got a property"), Thing);
|
|
}
|
|
return nullptr;
|
|
}
|
|
}
|
|
for (FUnrealFunctionDefinitionInfo* FunctionDef : TUHTFieldRange<FUnrealFunctionDefinitionInfo>(*Scope))
|
|
{
|
|
if (FunctionDef->GetFName() == InName)
|
|
{
|
|
return FunctionDef;
|
|
}
|
|
}
|
|
|
|
if (!bIncludeParents)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
FUnrealPropertyDefinitionInfo* FHeaderParser::FindProperty(const FUnrealStructDefinitionInfo& InScope, const TCHAR* InIdentifier, bool bIncludeParents, const TCHAR* Thing)
|
|
{
|
|
check(InIdentifier);
|
|
FName InName(InIdentifier, FNAME_Find);
|
|
if (InName != NAME_None)
|
|
{
|
|
for (const FUnrealStructDefinitionInfo* Scope = &InScope; Scope; Scope = UHTCast<FUnrealStructDefinitionInfo>(Scope->GetOuter()))
|
|
{
|
|
for (FUnrealFunctionDefinitionInfo* FunctionDef : TUHTFieldRange<FUnrealFunctionDefinitionInfo>(*Scope))
|
|
{
|
|
if (FunctionDef->GetFName() == InName)
|
|
{
|
|
if (Thing)
|
|
{
|
|
InScope.Throwf(TEXT("%s: expecting a property, got a function or delegate"), Thing);
|
|
}
|
|
return nullptr;
|
|
}
|
|
}
|
|
for (FUnrealPropertyDefinitionInfo* PropertyDef : TUHTFieldRange<FUnrealPropertyDefinitionInfo>(*Scope))
|
|
{
|
|
if (PropertyDef->GetFName() == InName)
|
|
{
|
|
return PropertyDef;
|
|
}
|
|
}
|
|
|
|
if (!bIncludeParents)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
/**
|
|
* Adds source file's include path to given metadata.
|
|
*
|
|
* @param Type Type for which to add include path.
|
|
* @param MetaData Meta data to fill the information.
|
|
*/
|
|
void AddIncludePathToMetadata(FUnrealFieldDefinitionInfo& FieldDef, TMap<FName, FString> &MetaData)
|
|
{
|
|
// Add metadata for the include path.
|
|
MetaData.Add(NAME_IncludePath, FieldDef.GetUnrealSourceFile().GetIncludePath());
|
|
}
|
|
|
|
/**
|
|
* Adds module's relative path from given file.
|
|
*
|
|
* @param SourceFile Given source file.
|
|
* @param MetaData Meta data to fill the information.
|
|
*/
|
|
void AddModuleRelativePathToMetadata(FUnrealSourceFile& SourceFile, TMap<FName, FString> &MetaData)
|
|
{
|
|
MetaData.Add(NAME_ModuleRelativePath, SourceFile.GetModuleRelativePath());
|
|
}
|
|
|
|
/**
|
|
* Adds module's relative path to given metadata.
|
|
*
|
|
* @param Type Type for which to add module's relative path.
|
|
* @param MetaData Meta data to fill the information.
|
|
*/
|
|
void AddModuleRelativePathToMetadata(FUnrealTypeDefinitionInfo& Type, TMap<FName, FString> &MetaData)
|
|
{
|
|
// Don't add module relative paths to functions.
|
|
if (Type.AsFunction() == nullptr && Type.HasSource())
|
|
{
|
|
MetaData.Add(NAME_ModuleRelativePath, Type.GetUnrealSourceFile().GetModuleRelativePath());
|
|
}
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------
|
|
Variables.
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
//
|
|
// Compile an enumeration definition.
|
|
//
|
|
FUnrealEnumDefinitionInfo& FHeaderParser::CompileEnum()
|
|
{
|
|
TSharedPtr<FFileScope> Scope = SourceFile.GetScope();
|
|
|
|
CheckAllow( TEXT("'Enum'"), ENestAllowFlags::TypeDecl );
|
|
|
|
// Get the enum specifier list
|
|
FToken EnumToken;
|
|
FMetaData EnumMetaData;
|
|
TArray<FPropertySpecifier> SpecifiersFound;
|
|
ReadSpecifierSetInsideMacro(SpecifiersFound, TEXT("Enum"), EnumMetaData);
|
|
|
|
// Check enum type. This can be global 'enum', 'namespace' or 'enum class' enums.
|
|
bool bReadEnumName = false;
|
|
UEnum::ECppForm CppForm = UEnum::ECppForm::Regular;
|
|
EEnumFlags Flags = EEnumFlags::None;
|
|
if (!GetIdentifier(EnumToken))
|
|
{
|
|
Throwf(TEXT("Missing identifier after UENUM()") );
|
|
}
|
|
|
|
if (EnumToken.IsValue(TEXT("namespace"), ESearchCase::CaseSensitive))
|
|
{
|
|
CppForm = UEnum::ECppForm::Namespaced;
|
|
|
|
SkipDeprecatedMacroIfNecessary(*this);
|
|
|
|
bReadEnumName = GetIdentifier(EnumToken);
|
|
}
|
|
else if (EnumToken.IsValue(TEXT("enum"), ESearchCase::CaseSensitive))
|
|
{
|
|
if (!GetIdentifier(EnumToken))
|
|
{
|
|
Throwf(TEXT("Missing identifier after enum") );
|
|
}
|
|
|
|
if (EnumToken.IsValue(TEXT("class"), ESearchCase::CaseSensitive) || EnumToken.IsValue(TEXT("struct"), ESearchCase::CaseSensitive))
|
|
{
|
|
CppForm = UEnum::ECppForm::EnumClass;
|
|
}
|
|
else
|
|
{
|
|
// Put whatever token we found back so that we can correctly skip below
|
|
UngetToken(EnumToken);
|
|
|
|
CppForm = UEnum::ECppForm::Regular;
|
|
}
|
|
|
|
SkipAlignasAndDeprecatedMacroIfNecessary(*this);
|
|
|
|
bReadEnumName = GetIdentifier(EnumToken);
|
|
}
|
|
else
|
|
{
|
|
Throwf(TEXT("UENUM() should be followed by \'enum\' or \'namespace\' keywords.") );
|
|
}
|
|
|
|
// Get enumeration name.
|
|
if (!bReadEnumName)
|
|
{
|
|
Throwf(TEXT("Missing enumeration name") );
|
|
}
|
|
|
|
FTokenValue EnumIdentifier = EnumToken.GetTokenValue();
|
|
ParseFieldMetaData(EnumMetaData, *EnumIdentifier);
|
|
|
|
// Create enum definition.
|
|
FUnrealEnumDefinitionInfo& EnumDef = GTypeDefinitionInfoMap.FindByNameChecked<FUnrealEnumDefinitionInfo>(*EnumIdentifier);
|
|
|
|
Scope->AddType(EnumDef);
|
|
|
|
if (CppForm != EnumDef.GetCppForm())
|
|
{
|
|
Throwf(TEXT("ICE: Mismatch of enum CPP form between pre-parser and parser"));
|
|
}
|
|
|
|
for (const FPropertySpecifier& Specifier : SpecifiersFound)
|
|
{
|
|
switch ((EEnumSpecifier)Algo::FindSortedStringCaseInsensitive(*Specifier.Key, GEnumSpecifierStrings))
|
|
{
|
|
default:
|
|
Throwf(TEXT("Unknown enum specifier '%s'"), *Specifier.Key);
|
|
|
|
case EEnumSpecifier::Flags:
|
|
Flags |= EEnumFlags::Flags;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ((GetCurrentCompilerDirective() & ECompilerDirective::WithEditorOnlyData) != 0)
|
|
{
|
|
EnumDef.MakeEditorOnly();
|
|
}
|
|
|
|
// Validate the metadata for the enum
|
|
EnumDef.ValidateMetaDataFormat(EnumMetaData);
|
|
|
|
// Read base for enum class
|
|
EUnderlyingEnumType UnderlyingType = EUnderlyingEnumType::uint8;
|
|
if (CppForm == UEnum::ECppForm::EnumClass)
|
|
{
|
|
UnderlyingType = ParseUnderlyingEnumType();
|
|
if (UnderlyingType != EnumDef.GetUnderlyingType())
|
|
{
|
|
Throwf(TEXT("ICE: Mismatch of underlying enum type between pre-parser and parser"));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (EnumHasAnyFlags(Flags, EEnumFlags::Flags))
|
|
{
|
|
Throwf(TEXT("The 'Flags' specifier can only be used on enum classes"));
|
|
}
|
|
}
|
|
|
|
if (UnderlyingType != EUnderlyingEnumType::uint8 && EnumMetaData.Contains(FHeaderParserNames::NAME_BlueprintType))
|
|
{
|
|
Throwf(TEXT("Invalid BlueprintType enum base - currently only uint8 supported"));
|
|
}
|
|
|
|
EnumDef.GetDefinitionRange().Start = &Input[InputPos];
|
|
|
|
// Get opening brace.
|
|
RequireSymbol( TEXT('{'), TEXT("'Enum'") );
|
|
|
|
switch (CppForm)
|
|
{
|
|
case UEnum::ECppForm::Namespaced:
|
|
{
|
|
// Now handle the inner true enum portion
|
|
RequireIdentifier(TEXT("enum"), ESearchCase::CaseSensitive, TEXT("'Enum'"));
|
|
|
|
SkipAlignasAndDeprecatedMacroIfNecessary(*this);
|
|
|
|
FToken InnerEnumToken;
|
|
if (!GetIdentifier(InnerEnumToken))
|
|
{
|
|
Throwf(TEXT("Missing enumeration name") );
|
|
}
|
|
|
|
EnumDef.SetCppType(FString::Printf(TEXT("%s::%s"), *EnumIdentifier, *InnerEnumToken.GetTokenValue()));
|
|
|
|
RequireSymbol( TEXT('{'), TEXT("'Enum'") );
|
|
}
|
|
break;
|
|
|
|
case UEnum::ECppForm::Regular:
|
|
case UEnum::ECppForm::EnumClass:
|
|
{
|
|
EnumDef.SetCppType(*EnumIdentifier);
|
|
}
|
|
break;
|
|
}
|
|
|
|
// List of all metadata generated for this enum
|
|
TMap<FName,FString> EnumValueMetaData = EnumMetaData;
|
|
|
|
AddModuleRelativePathToMetadata(EnumDef, EnumValueMetaData);
|
|
AddFormattedPrevCommentAsTooltipMetaData(EnumValueMetaData);
|
|
|
|
// Parse all enums tags.
|
|
FToken TagToken;
|
|
FMetaData TagMetaData;
|
|
TArray<TMap<FName, FString>> EntryMetaData;
|
|
|
|
bool bHasUnparsedValue = false;
|
|
TArray<TPair<FName, int64>> EnumNames;
|
|
int64 CurrentEnumValue = 0;
|
|
while (GetIdentifier(TagToken))
|
|
{
|
|
FTokenValue TagIdentifier = TagToken.GetTokenValue();
|
|
AddFormattedPrevCommentAsTooltipMetaData(TagMetaData);
|
|
|
|
SkipDeprecatedMacroIfNecessary(*this);
|
|
|
|
// Try to read an optional explicit enum value specification
|
|
if (MatchSymbol(TEXT('=')))
|
|
{
|
|
FToken InitToken;
|
|
if (!GetToken(InitToken))
|
|
{
|
|
Throwf(TEXT("UENUM: missing enumerator initializer"));
|
|
}
|
|
|
|
int64 NewEnumValue = -1;
|
|
if (!InitToken.GetConstInt64(NewEnumValue))
|
|
{
|
|
// We didn't parse a literal, so set an invalid value
|
|
NewEnumValue = -1;
|
|
bHasUnparsedValue = true;
|
|
}
|
|
|
|
// Skip tokens until we encounter a comma, a closing brace or a UMETA declaration
|
|
for (;;)
|
|
{
|
|
if (!GetToken(InitToken))
|
|
{
|
|
Throwf(TEXT("Enumerator: end of file encountered while parsing the initializer"));
|
|
}
|
|
|
|
if (InitToken.IsSymbol(TEXT(',')) || InitToken.IsSymbol(TEXT('}')) || InitToken.IsIdentifier(TEXT("UMETA"), ESearchCase::IgnoreCase))
|
|
{
|
|
UngetToken(InitToken);
|
|
break;
|
|
}
|
|
|
|
// There are tokens after the initializer so it's not a standalone literal,
|
|
// so set it to an invalid value.
|
|
NewEnumValue = -1;
|
|
bHasUnparsedValue = true;
|
|
}
|
|
|
|
CurrentEnumValue = NewEnumValue;
|
|
}
|
|
|
|
FName NewTag;
|
|
switch (CppForm)
|
|
{
|
|
case UEnum::ECppForm::Namespaced:
|
|
case UEnum::ECppForm::EnumClass:
|
|
{
|
|
NewTag = FName(*FString::Printf(TEXT("%s::%s"), *EnumIdentifier, *TagIdentifier), FNAME_Add);
|
|
}
|
|
break;
|
|
|
|
case UEnum::ECppForm::Regular:
|
|
{
|
|
NewTag = FName(TagToken.Value, FNAME_Add);
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Save the new tag
|
|
EnumNames.Emplace(NewTag, CurrentEnumValue);
|
|
|
|
// Autoincrement the current enumeration value
|
|
if (CurrentEnumValue != -1)
|
|
{
|
|
++CurrentEnumValue;
|
|
}
|
|
|
|
TagMetaData.Add(NAME_Name, NewTag.ToString());
|
|
EntryMetaData.Add(TagMetaData);
|
|
|
|
// check for metadata on this enum value
|
|
ParseFieldMetaData(TagMetaData, *TagIdentifier);
|
|
if (TagMetaData.Num() > 0)
|
|
{
|
|
// special case for enum value metadata - we need to prepend the key name with the enum value name
|
|
const FString TokenString = *TagIdentifier;
|
|
for (const auto& MetaData : TagMetaData)
|
|
{
|
|
FString KeyString = TokenString + TEXT(".") + MetaData.Key.ToString();
|
|
EnumValueMetaData.Emplace(*KeyString, MetaData.Value);
|
|
}
|
|
|
|
// now clear the metadata because we're going to reuse this token for parsing the next enum value
|
|
TagMetaData.Empty();
|
|
}
|
|
|
|
if (!MatchSymbol(TEXT(',')))
|
|
{
|
|
FToken ClosingBrace;
|
|
if (!GetToken(ClosingBrace))
|
|
{
|
|
Throwf(TEXT("UENUM: end of file encountered"));
|
|
}
|
|
|
|
if (ClosingBrace.IsSymbol(TEXT('}')))
|
|
{
|
|
UngetToken(ClosingBrace);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Trailing brace and semicolon for the enum
|
|
RequireSymbol( TEXT('}'), TEXT("'Enum'") );
|
|
MatchSemi();
|
|
|
|
if (CppForm == UEnum::ECppForm::Namespaced)
|
|
{
|
|
// Trailing brace for the namespace.
|
|
RequireSymbol( TEXT('}'), TEXT("'Enum'") );
|
|
}
|
|
|
|
EnumDef.GetDefinitionRange().End = &Input[InputPos];
|
|
|
|
// Register the list of enum names.
|
|
if (!EnumDef.SetEnums(EnumNames, CppForm, Flags))
|
|
{
|
|
//@TODO - Two issues with this, first the SetEnums always returns true. That was the case even with the original code.
|
|
// Second issue is that the code to generate the max string doesn't work in many cases (i.e. EPixelFormat)
|
|
const FName MaxEnumItem = *(EnumDef.GenerateEnumPrefix() + TEXT("_MAX"));
|
|
const int32 MaxEnumItemIndex = EnumDef.GetIndexByName(MaxEnumItem);
|
|
if (MaxEnumItemIndex != INDEX_NONE)
|
|
{
|
|
Throwf(TEXT("Illegal enumeration tag specified. Conflicts with auto-generated tag '%s'"), *MaxEnumItem.ToString());
|
|
}
|
|
|
|
Throwf(TEXT("Unable to generate enum MAX entry '%s' due to name collision"), *MaxEnumItem.ToString());
|
|
}
|
|
|
|
CheckDocumentationPolicyForEnum(EnumDef, EnumValueMetaData, EntryMetaData);
|
|
|
|
if (!EnumDef.IsValidEnumValue(0) && EnumMetaData.Contains(FHeaderParserNames::NAME_BlueprintType) && !bHasUnparsedValue)
|
|
{
|
|
EnumDef.LogWarning(TEXT("'%s' does not have a 0 entry! (This is a problem when the enum is initalized by default)"), *EnumDef.GetName());
|
|
}
|
|
|
|
// Add the metadata gathered for the enum to the package
|
|
if (EnumValueMetaData.Num() > 0)
|
|
{
|
|
EnumDef.AddMetaData(MoveTemp(EnumValueMetaData));
|
|
}
|
|
return EnumDef;
|
|
}
|
|
|
|
/**
|
|
* Checks if a string is made up of all the same character.
|
|
*
|
|
* @param Str The string to check for all
|
|
* @param Ch The character to check for
|
|
*
|
|
* @return True if the string is made up only of Ch characters.
|
|
*/
|
|
bool IsAllSameChar(const TCHAR* Str, TCHAR Ch)
|
|
{
|
|
check(Str);
|
|
|
|
while (TCHAR StrCh = *Str++)
|
|
{
|
|
if (StrCh != Ch)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param Input An input string, expected to be a script comment.
|
|
* @return The input string, reformatted in such a way as to be appropriate for use as a tooltip.
|
|
*/
|
|
FString FHeaderParser::FormatCommentForToolTip(const FString& Input)
|
|
{
|
|
// Return an empty string if there are no alpha-numeric characters or a Unicode characters above 0xFF
|
|
// (which would be the case for pure CJK comments) in the input string.
|
|
bool bFoundAlphaNumericChar = false;
|
|
for ( int32 i = 0 ; i < Input.Len() ; ++i )
|
|
{
|
|
if ( FChar::IsAlnum(Input[i]) || (Input[i] > 0xFF) )
|
|
{
|
|
bFoundAlphaNumericChar = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( !bFoundAlphaNumericChar )
|
|
{
|
|
return FString();
|
|
}
|
|
|
|
FString Result(Input);
|
|
|
|
// Sweep out comments marked to be ignored.
|
|
{
|
|
int32 CommentStart, CommentEnd;
|
|
// Block comments go first
|
|
for (CommentStart = Result.Find(TEXT("/*~"), ESearchCase::CaseSensitive); CommentStart != INDEX_NONE; CommentStart = Result.Find(TEXT("/*~"), ESearchCase::CaseSensitive))
|
|
{
|
|
CommentEnd = Result.Find(TEXT("*/"), ESearchCase::CaseSensitive, ESearchDir::FromStart, CommentStart);
|
|
if (CommentEnd != INDEX_NONE)
|
|
{
|
|
Result.RemoveAt(CommentStart, (CommentEnd + 2) - CommentStart, false);
|
|
}
|
|
else
|
|
{
|
|
// This looks like an error - an unclosed block comment.
|
|
break;
|
|
}
|
|
}
|
|
// Leftover line comments go next
|
|
for (CommentStart = Result.Find(TEXT("//~"), ESearchCase::CaseSensitive); CommentStart != INDEX_NONE; CommentStart = Result.Find(TEXT("//~"), ESearchCase::CaseSensitive))
|
|
{
|
|
CommentEnd = Result.Find(TEXT("\n"), ESearchCase::CaseSensitive, ESearchDir::FromStart, CommentStart);
|
|
if (CommentEnd != INDEX_NONE)
|
|
{
|
|
Result.RemoveAt(CommentStart, (CommentEnd + 1) - CommentStart, false);
|
|
}
|
|
else
|
|
{
|
|
Result.RemoveAt(CommentStart, Result.Len() - CommentStart, false);
|
|
break;
|
|
}
|
|
}
|
|
// Finish by shrinking if anything was removed, since we deferred this during the search.
|
|
Result.Shrink();
|
|
}
|
|
|
|
// Check for known commenting styles.
|
|
const bool bJavaDocStyle = Result.Contains(TEXT("/**"), ESearchCase::CaseSensitive);
|
|
const bool bCStyle = Result.Contains(TEXT("/*"), ESearchCase::CaseSensitive);
|
|
const bool bCPPStyle = Result.StartsWith(TEXT("//"), ESearchCase::CaseSensitive);
|
|
|
|
if ( bJavaDocStyle || bCStyle)
|
|
{
|
|
// Remove beginning and end markers.
|
|
if (bJavaDocStyle)
|
|
{
|
|
Result.ReplaceInline(TEXT("/**"), TEXT(""), ESearchCase::CaseSensitive);
|
|
}
|
|
if (bCStyle)
|
|
{
|
|
Result.ReplaceInline(TEXT("/*"), TEXT(""), ESearchCase::CaseSensitive);
|
|
}
|
|
Result.ReplaceInline(TEXT("*/"), TEXT(""), ESearchCase::CaseSensitive);
|
|
}
|
|
|
|
if ( bCPPStyle )
|
|
{
|
|
// Remove c++-style comment markers. Also handle javadoc-style comments
|
|
Result.ReplaceInline(TEXT("///"), TEXT(""), ESearchCase::CaseSensitive);
|
|
Result.ReplaceInline(TEXT("//"), TEXT(""), ESearchCase::CaseSensitive);
|
|
|
|
// Parser strips cpptext and replaces it with "// (cpptext)" -- prevent
|
|
// this from being treated as a comment on variables declared below the
|
|
// cpptext section
|
|
Result.ReplaceInline(TEXT("(cpptext)"), TEXT(""));
|
|
}
|
|
|
|
// Get rid of carriage return or tab characters, which mess up tooltips.
|
|
Result.ReplaceInline(TEXT( "\r" ), TEXT( "" ), ESearchCase::CaseSensitive);
|
|
|
|
//wx widgets has a hard coded tab size of 8
|
|
{
|
|
const int32 SpacesPerTab = 8;
|
|
Result.ConvertTabsToSpacesInline(SpacesPerTab);
|
|
}
|
|
|
|
// get rid of uniform leading whitespace and all trailing whitespace, on each line
|
|
TArray<FString> Lines;
|
|
Result.ParseIntoArray(Lines, TEXT("\n"), false);
|
|
|
|
for (FString& Line : Lines)
|
|
{
|
|
// Remove trailing whitespace
|
|
Line.TrimEndInline();
|
|
|
|
// Remove leading "*" and "* " in javadoc comments.
|
|
if (bJavaDocStyle)
|
|
{
|
|
// Find first non-whitespace character
|
|
int32 Pos = 0;
|
|
while (Pos < Line.Len() && FChar::IsWhitespace(Line[Pos]))
|
|
{
|
|
++Pos;
|
|
}
|
|
|
|
// Is it a *?
|
|
if (Pos < Line.Len() && Line[Pos] == '*')
|
|
{
|
|
// Eat next space as well
|
|
if (Pos+1 < Line.Len() && FChar::IsWhitespace(Line[Pos+1]))
|
|
{
|
|
++Pos;
|
|
}
|
|
|
|
Line.RightChopInline(Pos + 1, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
auto IsWhitespaceOrLineSeparator = [](const FString& Line)
|
|
{
|
|
int32 LineLength = Line.Len();
|
|
int32 WhitespaceCount = 0;
|
|
while (WhitespaceCount < LineLength && FChar::IsWhitespace(Line[WhitespaceCount]))
|
|
{
|
|
++WhitespaceCount;
|
|
}
|
|
|
|
if (WhitespaceCount == LineLength)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
const TCHAR* Str = (*Line) + WhitespaceCount;
|
|
return IsAllSameChar(Str, TEXT('-')) || IsAllSameChar(Str, TEXT('=')) || IsAllSameChar(Str, TEXT('*'));
|
|
};
|
|
|
|
// Find first meaningful line
|
|
int32 FirstIndex = 0;
|
|
for (const FString& Line : Lines)
|
|
{
|
|
if (!IsWhitespaceOrLineSeparator(Line))
|
|
{
|
|
break;
|
|
}
|
|
|
|
++FirstIndex;
|
|
}
|
|
|
|
int32 LastIndex = Lines.Num();
|
|
while (LastIndex != FirstIndex)
|
|
{
|
|
const FString& Line = Lines[LastIndex - 1];
|
|
|
|
if (!IsWhitespaceOrLineSeparator(Line))
|
|
{
|
|
break;
|
|
}
|
|
|
|
--LastIndex;
|
|
}
|
|
|
|
Result.Reset();
|
|
|
|
if (FirstIndex != LastIndex)
|
|
{
|
|
FString& FirstLine = Lines[FirstIndex];
|
|
|
|
// Figure out how much whitespace is on the first line
|
|
int32 MaxNumWhitespaceToRemove;
|
|
for (MaxNumWhitespaceToRemove = 0; MaxNumWhitespaceToRemove < FirstLine.Len(); MaxNumWhitespaceToRemove++)
|
|
{
|
|
if (!FChar::IsLinebreak(FirstLine[MaxNumWhitespaceToRemove]) && !FChar::IsWhitespace(FirstLine[MaxNumWhitespaceToRemove]))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (int32 Index = FirstIndex; Index != LastIndex; ++Index)
|
|
{
|
|
FString& Line = Lines[Index];
|
|
|
|
int32 TemporaryMaxWhitespace = MaxNumWhitespaceToRemove;
|
|
|
|
// Allow eating an extra tab on subsequent lines if it's present
|
|
if ((Index > FirstIndex) && (Line.Len() > 0) && (Line[0] == '\t'))
|
|
{
|
|
TemporaryMaxWhitespace++;
|
|
}
|
|
|
|
// Advance past whitespace
|
|
int32 Pos = 0;
|
|
while (Pos < TemporaryMaxWhitespace && Pos < Line.Len() && FChar::IsWhitespace(Line[Pos]))
|
|
{
|
|
++Pos;
|
|
}
|
|
|
|
if (Pos > 0)
|
|
{
|
|
Line.RightChopInline(Pos, false);
|
|
}
|
|
|
|
if (Index > FirstIndex)
|
|
{
|
|
Result += TEXT("\n");
|
|
}
|
|
|
|
if (Line.Len() && !IsAllSameChar(*Line, TEXT('=')))
|
|
{
|
|
Result += Line;
|
|
}
|
|
}
|
|
}
|
|
|
|
//@TODO: UCREMOVAL: Really want to trim an arbitrary number of newlines above and below, but keep multiple newlines internally
|
|
// Make sure it doesn't start with a newline
|
|
if (!Result.IsEmpty() && FChar::IsLinebreak(Result[0]))
|
|
{
|
|
Result.RightChopInline(1, false);
|
|
}
|
|
|
|
// Make sure it doesn't end with a dead newline
|
|
if (!Result.IsEmpty() && FChar::IsLinebreak(Result[Result.Len() - 1]))
|
|
{
|
|
Result.LeftInline(Result.Len() - 1, false);
|
|
}
|
|
|
|
// Done.
|
|
return Result;
|
|
}
|
|
|
|
TMap<FName, FString> FHeaderParser::GetParameterToolTipsFromFunctionComment(const FString& Input)
|
|
{
|
|
SCOPE_SECONDS_COUNTER_UHT(DocumentationPolicy);
|
|
|
|
TMap<FName, FString> Map;
|
|
if (Input.IsEmpty())
|
|
{
|
|
return Map;
|
|
}
|
|
|
|
TArray<FString> Params;
|
|
static const auto& ParamTag = TEXT("@param");
|
|
static const auto& ReturnTag = TEXT("@return");
|
|
static const auto& ReturnParamPrefix = TEXT("ReturnValue ");
|
|
|
|
/**
|
|
* Search for @param / @return followed by a section until a line break.
|
|
* For example: "@param Test MyTest Variable" becomes "Test", "MyTest Variable"
|
|
* These pairs are then later split and stored as the parameter tooltips.
|
|
* Once we don't find either @param or @return we break from the loop.
|
|
*/
|
|
int32 Offset = 0;
|
|
while (Offset < Input.Len())
|
|
{
|
|
const TCHAR* ParamPrefix = TEXT("");
|
|
int32 ParamStart = Input.Find(ParamTag, ESearchCase::CaseSensitive, ESearchDir::FromStart, Offset);
|
|
if(ParamStart != INDEX_NONE)
|
|
{
|
|
ParamStart = ParamStart + UE_ARRAY_COUNT(ParamTag);
|
|
Offset = ParamStart;
|
|
}
|
|
else
|
|
{
|
|
ParamStart = Input.Find(ReturnTag, ESearchCase::CaseSensitive, ESearchDir::FromStart, Offset);
|
|
if (ParamStart != INDEX_NONE)
|
|
{
|
|
ParamStart = ParamStart + UE_ARRAY_COUNT(ReturnTag);
|
|
Offset = ParamStart;
|
|
ParamPrefix = ReturnParamPrefix;
|
|
}
|
|
else
|
|
{
|
|
// no @param, no @return?
|
|
break;
|
|
}
|
|
}
|
|
|
|
int32 ParamEnd = Input.Find(TEXT("\n"), ESearchCase::CaseSensitive, ESearchDir::FromStart, ParamStart);
|
|
if (ParamEnd == INDEX_NONE)
|
|
{
|
|
ParamEnd = Input.Len();
|
|
}
|
|
Offset = ParamEnd;
|
|
|
|
Params.Add(ParamPrefix + Input.Mid(ParamStart, ParamEnd - ParamStart - 1));
|
|
}
|
|
|
|
for (FString& Param : Params)
|
|
{
|
|
Param.ConvertTabsToSpacesInline(4);
|
|
Param.TrimStartAndEndInline();
|
|
|
|
int32 FirstSpaceIndex = -1;
|
|
if (!Param.FindChar(TEXT(' '), FirstSpaceIndex))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
FString ParamToolTip = Param.Mid(FirstSpaceIndex + 1);
|
|
ParamToolTip.TrimStartInline();
|
|
|
|
Param.LeftInline(FirstSpaceIndex);
|
|
|
|
Map.Add(*Param, MoveTemp(ParamToolTip));
|
|
}
|
|
|
|
return Map;
|
|
}
|
|
|
|
|
|
void FHeaderParser::AddFormattedPrevCommentAsTooltipMetaData(TMap<FName, FString>& MetaData)
|
|
{
|
|
// Don't add a tooltip if one already exists.
|
|
if (MetaData.Find(NAME_ToolTip))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Add the comment if it is not empty
|
|
if (!PrevComment.IsEmpty())
|
|
{
|
|
MetaData.Add(NAME_Comment, *PrevComment);
|
|
}
|
|
|
|
// Don't add a tooltip if the comment is empty after formatting.
|
|
FString FormattedComment = FormatCommentForToolTip(PrevComment);
|
|
if (!FormattedComment.Len())
|
|
{
|
|
return;
|
|
}
|
|
|
|
MetaData.Add(NAME_ToolTip, *FormattedComment);
|
|
|
|
// We've already used this comment as a tooltip, so clear it so that it doesn't get used again
|
|
PrevComment.Empty();
|
|
}
|
|
|
|
static const TCHAR* GetAccessSpecifierName(EAccessSpecifier AccessSpecifier)
|
|
{
|
|
switch (AccessSpecifier)
|
|
{
|
|
case ACCESS_Public:
|
|
return TEXT("public");
|
|
case ACCESS_Protected:
|
|
return TEXT("protected");
|
|
case ACCESS_Private:
|
|
return TEXT("private");
|
|
default:
|
|
check(0);
|
|
}
|
|
return TEXT("");
|
|
}
|
|
|
|
// Tries to parse the token as an access protection specifier (public:, protected:, or private:)
|
|
EAccessSpecifier FHeaderParser::ParseAccessProtectionSpecifier(const FToken& Token)
|
|
{
|
|
if (!Token.IsIdentifier())
|
|
{
|
|
return ACCESS_NotAnAccessSpecifier;
|
|
}
|
|
|
|
for (EAccessSpecifier Test = EAccessSpecifier(ACCESS_NotAnAccessSpecifier + 1); Test != ACCESS_Num; Test = EAccessSpecifier(Test + 1))
|
|
{
|
|
if (Token.IsValue(GetAccessSpecifierName(Test), ESearchCase::CaseSensitive))
|
|
{
|
|
auto ErrorMessageGetter = [&Token]() { return FString::Printf(TEXT("after %s"), *Token.GetTokenValue()); };
|
|
|
|
// Consume the colon after the specifier
|
|
RequireSymbol(TEXT(':'), ErrorMessageGetter);
|
|
return Test;
|
|
}
|
|
}
|
|
return ACCESS_NotAnAccessSpecifier;
|
|
}
|
|
|
|
|
|
/**
|
|
* Compile a struct definition.
|
|
*/
|
|
FUnrealScriptStructDefinitionInfo& FHeaderParser::CompileStructDeclaration()
|
|
{
|
|
int32 DeclInputLine = InputLine;
|
|
TSharedPtr<FFileScope> Scope = SourceFile.GetScope();
|
|
|
|
// Make sure structs can be declared here.
|
|
CheckAllow( TEXT("'struct'"), ENestAllowFlags::TypeDecl );
|
|
|
|
bool IsNative = false;
|
|
bool IsExport = false;
|
|
bool IsTransient = false;
|
|
TMap<FName, FString> MetaData;
|
|
|
|
// Get the struct specifier list
|
|
TArray<FPropertySpecifier> SpecifiersFound;
|
|
ReadSpecifierSetInsideMacro(SpecifiersFound, TEXT("Struct"), MetaData);
|
|
|
|
// Consume the struct keyword
|
|
RequireIdentifier(TEXT("struct"), ESearchCase::CaseSensitive, TEXT("Struct declaration specifier"));
|
|
|
|
// The struct name as parsed in script and stripped of it's prefix
|
|
FString StructNameInScript;
|
|
|
|
// The struct name stripped of it's prefix
|
|
FString StructNameStripped;
|
|
|
|
// The required API module for this struct, if any
|
|
FString RequiredAPIMacroIfPresent;
|
|
|
|
SkipAlignasAndDeprecatedMacroIfNecessary(*this);
|
|
|
|
// Read the struct name
|
|
ParseNameWithPotentialAPIMacroPrefix(/*out*/ StructNameInScript, /*out*/ RequiredAPIMacroIfPresent, TEXT("struct"));
|
|
|
|
StructNameStripped = GetClassNameWithPrefixRemoved(StructNameInScript);
|
|
|
|
// Effective struct name
|
|
const FString EffectiveStructName = *StructNameStripped;
|
|
|
|
// Locate the structure
|
|
FUnrealScriptStructDefinitionInfo& StructDef = GTypeDefinitionInfoMap.FindByNameChecked<FUnrealScriptStructDefinitionInfo>(*StructNameStripped);
|
|
|
|
if (StructDef.HasAnyStructFlags(STRUCT_Immutable))
|
|
{
|
|
if (!FPaths::IsSamePath(Filename, GUObjectDef->GetUnrealSourceFile().GetFilename()))
|
|
{
|
|
LogError(TEXT("Immutable is being phased out in favor of SerializeNative, and is only legal on the mirror structs declared in UObject"));
|
|
}
|
|
}
|
|
|
|
// Parse the inheritance list
|
|
ParseInheritance(TEXT("struct"), [](const TCHAR* StructName, bool bIsSuperClass) {}); // Eat the results, already been parsed
|
|
|
|
// if we have a base struct, propagate inherited struct flags now
|
|
if (!StructDef.GetSuperStructInfo().Name.IsEmpty())
|
|
{
|
|
FUnrealScriptStructDefinitionInfo* BaseStructDef = nullptr;
|
|
|
|
const FString& ParentStructNameInScript = StructDef.GetSuperStructInfo().Name;
|
|
FString ParentStructNameStripped;
|
|
bool bOverrideParentStructName = false;
|
|
|
|
if (!UHTConfig.StructsWithNoPrefix.Contains(ParentStructNameInScript))
|
|
{
|
|
bOverrideParentStructName = true;
|
|
ParentStructNameStripped = GetClassNameWithPrefixRemoved(ParentStructNameInScript);
|
|
}
|
|
|
|
// If we're expecting a prefix, first try finding the correct field with the stripped struct name
|
|
if (bOverrideParentStructName)
|
|
{
|
|
BaseStructDef = UHTCast<FUnrealScriptStructDefinitionInfo>(Scope->FindTypeByName(*ParentStructNameStripped));
|
|
}
|
|
|
|
// If it wasn't found, try to find the literal name given
|
|
if (BaseStructDef == nullptr)
|
|
{
|
|
BaseStructDef = UHTCast<FUnrealScriptStructDefinitionInfo>(Scope->FindTypeByName(*ParentStructNameInScript));
|
|
}
|
|
|
|
// Try to locate globally //@TODO: UCREMOVAL: This seems extreme
|
|
if (BaseStructDef == nullptr)
|
|
{
|
|
if (bOverrideParentStructName)
|
|
{
|
|
BaseStructDef = GTypeDefinitionInfoMap.FindByName<FUnrealScriptStructDefinitionInfo>(*ParentStructNameStripped);
|
|
}
|
|
|
|
if (BaseStructDef == nullptr)
|
|
{
|
|
BaseStructDef = GTypeDefinitionInfoMap.FindByName<FUnrealScriptStructDefinitionInfo>(*ParentStructNameInScript);
|
|
}
|
|
}
|
|
|
|
// If the struct still wasn't found, throw an error
|
|
if (BaseStructDef == nullptr)
|
|
{
|
|
Throwf(TEXT("'struct': Can't find struct '%s'"), *ParentStructNameInScript);
|
|
}
|
|
|
|
// If the struct was found, confirm it adheres to the correct syntax. This should always fail if we were expecting an override that was not found.
|
|
if (bOverrideParentStructName)
|
|
{
|
|
const TCHAR* PrefixCPP = UHTConfig.StructsWithTPrefix.Contains(ParentStructNameStripped) ? TEXT("T") : BaseStructDef->GetPrefixCPP();
|
|
if (ParentStructNameInScript != FString::Printf(TEXT("%s%s"), PrefixCPP, *ParentStructNameStripped))
|
|
{
|
|
Throwf(TEXT("Parent Struct '%s' is missing a valid Unreal prefix, expecting '%s'"), *ParentStructNameInScript, *FString::Printf(TEXT("%s%s"), PrefixCPP, *BaseStructDef->GetName()));
|
|
}
|
|
}
|
|
|
|
StructDef.GetSuperStructInfo().Struct = BaseStructDef->AsStruct();
|
|
StructDef.SetStructFlags(EStructFlags(BaseStructDef->GetStructFlags() & STRUCT_Inherit));
|
|
if (StructDef.GetScriptStructSafe() != nullptr && BaseStructDef->GetScriptStructSafe())
|
|
{
|
|
StructDef.GetScriptStructSafe()->SetSuperStruct(BaseStructDef->GetScriptStructSafe());
|
|
}
|
|
}
|
|
|
|
Scope->AddType(StructDef);
|
|
|
|
AddModuleRelativePathToMetadata(StructDef, MetaData);
|
|
|
|
// Check to make sure the syntactic native prefix was set-up correctly.
|
|
// If this check results in a false positive, it will be flagged as an identifier failure.
|
|
FString DeclaredPrefix = GetClassPrefix( StructNameInScript );
|
|
if( DeclaredPrefix == StructDef.GetPrefixCPP() || DeclaredPrefix == TEXT("T") )
|
|
{
|
|
// Found a prefix, do a basic check to see if it's valid
|
|
const TCHAR* ExpectedPrefixCPP = UHTConfig.StructsWithTPrefix.Contains(StructNameStripped) ? TEXT("T") : StructDef.GetPrefixCPP();
|
|
FString ExpectedStructName = FString::Printf(TEXT("%s%s"), ExpectedPrefixCPP, *StructNameStripped);
|
|
if (StructNameInScript != ExpectedStructName)
|
|
{
|
|
Throwf(TEXT("Struct '%s' has an invalid Unreal prefix, expecting '%s'"), *StructNameInScript, *ExpectedStructName);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
const TCHAR* ExpectedPrefixCPP = UHTConfig.StructsWithTPrefix.Contains(StructNameInScript) ? TEXT("T") : StructDef.GetPrefixCPP();
|
|
FString ExpectedStructName = FString::Printf(TEXT("%s%s"), ExpectedPrefixCPP, *StructNameInScript);
|
|
Throwf(TEXT("Struct '%s' is missing a valid Unreal prefix, expecting '%s'"), *StructNameInScript, *ExpectedStructName);
|
|
}
|
|
|
|
AddFormattedPrevCommentAsTooltipMetaData(MetaData);
|
|
|
|
// Register the metadata
|
|
FUHTMetaData::RemapAndAddMetaData(StructDef, MoveTemp(MetaData));
|
|
|
|
StructDef.GetDefinitionRange().Start = &Input[InputPos];
|
|
|
|
// Get opening brace.
|
|
RequireSymbol( TEXT('{'), TEXT("'struct'") );
|
|
|
|
// Members of structs have a default public access level in c++
|
|
// Assume that, but restore the parser state once we finish parsing this struct
|
|
TGuardValue<EAccessSpecifier> HoldFromClass(CurrentAccessSpecifier, ACCESS_Public);
|
|
|
|
int32 SavedLineNumber = InputLine;
|
|
|
|
// Clear comment before parsing body of the struct.
|
|
|
|
|
|
// Parse all struct variables.
|
|
FToken Token;
|
|
while (1)
|
|
{
|
|
ClearComment();
|
|
GetToken( Token );
|
|
int StartingLine = InputLine;
|
|
|
|
if (EAccessSpecifier AccessSpecifier = ParseAccessProtectionSpecifier(Token))
|
|
{
|
|
CurrentAccessSpecifier = AccessSpecifier;
|
|
}
|
|
else if (Token.IsIdentifier(TEXT("UPROPERTY"), ESearchCase::CaseSensitive))
|
|
{
|
|
CompileVariableDeclaration(StructDef);
|
|
}
|
|
else if (Token.IsIdentifier(TEXT("UFUNCTION"), ESearchCase::CaseSensitive))
|
|
{
|
|
Throwf(TEXT("USTRUCTs cannot contain UFUNCTIONs."));
|
|
}
|
|
else if (Token.IsIdentifier(TEXT("RIGVM_METHOD"), ESearchCase::CaseSensitive))
|
|
{
|
|
CompileRigVMMethodDeclaration(StructDef);
|
|
}
|
|
else if (Token.IsIdentifier(TEXT("GENERATED_USTRUCT_BODY"), ESearchCase::CaseSensitive) || Token.IsIdentifier(TEXT("GENERATED_BODY"), ESearchCase::CaseSensitive))
|
|
{
|
|
// Match 'GENERATED_USTRUCT_BODY' '(' [StructName] ')' or 'GENERATED_BODY' '(' [StructName] ')'
|
|
if (CurrentAccessSpecifier != ACCESS_Public)
|
|
{
|
|
Throwf(TEXT("%s must be in the public scope of '%s', not private or protected."), *Token.GetTokenValue(), *StructNameInScript);
|
|
}
|
|
|
|
if (StructDef.GetMacroDeclaredLineNumber() != INDEX_NONE)
|
|
{
|
|
Throwf(TEXT("Multiple %s declarations found in '%s'"), *Token.GetTokenValue(), *StructNameInScript);
|
|
}
|
|
|
|
StructDef.SetMacroDeclaredLineNumber(InputLine);
|
|
RequireSymbol(TEXT('('), TEXT("'struct'"));
|
|
|
|
CompileVersionDeclaration(StructDef);
|
|
|
|
RequireSymbol(TEXT(')'), TEXT("'struct'"));
|
|
|
|
// Eat a semicolon if present (not required)
|
|
SafeMatchSymbol(TEXT(';'));
|
|
}
|
|
else if ( Token.IsSymbol(TEXT('#')) && MatchIdentifier(TEXT("ifdef"), ESearchCase::CaseSensitive) )
|
|
{
|
|
PushCompilerDirective(ECompilerDirective::Insignificant);
|
|
}
|
|
else if ( Token.IsSymbol(TEXT('#')) && MatchIdentifier(TEXT("ifndef"), ESearchCase::CaseSensitive) )
|
|
{
|
|
PushCompilerDirective(ECompilerDirective::Insignificant);
|
|
}
|
|
else if (Token.IsSymbol(TEXT('#')) && MatchIdentifier(TEXT("endif"), ESearchCase::CaseSensitive))
|
|
{
|
|
PopCompilerDirective();
|
|
// Do nothing and hope that the if code below worked out OK earlier
|
|
|
|
// skip it and skip over the text, it is not recorded or processed
|
|
if (StartingLine == InputLine)
|
|
{
|
|
TCHAR c;
|
|
while (!IsEOL(c = GetChar()))
|
|
{
|
|
}
|
|
ClearComment();
|
|
}
|
|
}
|
|
else if ( Token.IsSymbol(TEXT('#')) && MatchIdentifier(TEXT("if"), ESearchCase::CaseSensitive) )
|
|
{
|
|
//@TODO: This parsing should be combined with CompileDirective and probably happen much much higher up!
|
|
bool bInvertConditional = MatchSymbol(TEXT('!'));
|
|
bool bConsumeAsCppText = false;
|
|
|
|
if (MatchIdentifier(TEXT("WITH_EDITORONLY_DATA"), ESearchCase::CaseSensitive) )
|
|
{
|
|
if (bInvertConditional)
|
|
{
|
|
Throwf(TEXT("Cannot use !WITH_EDITORONLY_DATA"));
|
|
}
|
|
|
|
PushCompilerDirective(ECompilerDirective::WithEditorOnlyData);
|
|
}
|
|
else if (MatchIdentifier(TEXT("WITH_EDITOR"), ESearchCase::CaseSensitive) )
|
|
{
|
|
if (bInvertConditional)
|
|
{
|
|
Throwf(TEXT("Cannot use !WITH_EDITOR"));
|
|
}
|
|
PushCompilerDirective(ECompilerDirective::WithEditor);
|
|
}
|
|
else if (MatchIdentifier(TEXT("CPP"), ESearchCase::CaseSensitive) || MatchConstInt(TEXT("0")) || MatchConstInt(TEXT("1")) || MatchIdentifier(TEXT("WITH_HOT_RELOAD"), ESearchCase::CaseSensitive) || MatchIdentifier(TEXT("WITH_HOT_RELOAD_CTORS"), ESearchCase::CaseSensitive))
|
|
{
|
|
bConsumeAsCppText = !bInvertConditional;
|
|
PushCompilerDirective(ECompilerDirective::Insignificant);
|
|
}
|
|
else
|
|
{
|
|
Throwf(TEXT("'struct': Unsupported preprocessor directive inside a struct.") );
|
|
}
|
|
|
|
if (bConsumeAsCppText)
|
|
{
|
|
// Skip over the text, it is not recorded or processed
|
|
int32 nest = 1;
|
|
while (nest > 0)
|
|
{
|
|
TCHAR ch = GetChar(1);
|
|
|
|
if ( ch==0 )
|
|
{
|
|
Throwf(TEXT("Unexpected end of struct definition %s"), *StructDef.GetName());
|
|
}
|
|
else if ( ch=='{' || (ch=='#' && (PeekIdentifier(TEXT("if"), ESearchCase::CaseSensitive) || PeekIdentifier(TEXT("ifdef"), ESearchCase::CaseSensitive))) )
|
|
{
|
|
nest++;
|
|
}
|
|
else if ( ch=='}' || (ch=='#' && PeekIdentifier(TEXT("endif"), ESearchCase::CaseSensitive)) )
|
|
{
|
|
nest--;
|
|
}
|
|
|
|
if (nest==0)
|
|
{
|
|
RequireIdentifier(TEXT("endif"), ESearchCase::CaseSensitive, TEXT("'if'"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (Token.IsSymbol(TEXT('#')) && MatchIdentifier(TEXT("pragma"), ESearchCase::CaseSensitive))
|
|
{
|
|
// skip it and skip over the text, it is not recorded or processed
|
|
TCHAR c;
|
|
while (!IsEOL(c = GetChar()))
|
|
{
|
|
}
|
|
}
|
|
else if (ProbablyAnUnknownObjectLikeMacro(*this, Token))
|
|
{
|
|
// skip it
|
|
}
|
|
else
|
|
{
|
|
if (!Token.IsSymbol( TEXT('}')))
|
|
{
|
|
// Skip declaration will destroy data in Token, so cache off the identifier in case we need to provfide an error
|
|
FToken FirstToken = Token;
|
|
if (!SkipDeclaration(Token))
|
|
{
|
|
Throwf(TEXT("'struct': Unexpected '%s'"), *FirstToken.GetTokenValue());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MatchSemi();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
StructDef.GetDefinitionRange().End = &Input[InputPos];
|
|
|
|
// Validation
|
|
bool bStructBodyFound = StructDef.GetMacroDeclaredLineNumber() != INDEX_NONE;
|
|
if (!bStructBodyFound && StructDef.HasAnyStructFlags(STRUCT_Native))
|
|
{
|
|
// Roll the line number back to the start of the struct body and error out
|
|
InputLine = SavedLineNumber;
|
|
Throwf(TEXT("Expected a GENERATED_BODY() at the start of struct"));
|
|
}
|
|
|
|
// check if the struct is marked as deprecated and does not implement the upgrade path
|
|
if(StructDef.HasMetaData(TEXT("Deprecated")))
|
|
{
|
|
const FRigVMStructInfo& StructRigVMInfo = StructDef.GetRigVMInfo();
|
|
if(!StructRigVMInfo.bHasGetUpgradeInfoMethod && !StructRigVMInfo.Methods.IsEmpty())
|
|
{
|
|
LogError(TEXT(
|
|
"RigVMStruct '%s' is marked as deprecated but is missing GetUpgradeInfo method.\n"
|
|
"Please implement a method like below:\n\n"
|
|
"RIGVM_METHOD()\n"
|
|
"virtual FRigVMStructUpgradeInfo GetUpgradeInfo() const override;"),
|
|
*StructDef.GetNameCPP());
|
|
}
|
|
}
|
|
|
|
// Validate sparse class data
|
|
CheckSparseClassData(StructDef);
|
|
return StructDef;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------
|
|
Retry management.
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
/**
|
|
* Remember the current compilation points, both in the source being
|
|
* compiled and the object code being emitted.
|
|
*
|
|
* @param Retry [out] filled in with current compiler position information
|
|
*/
|
|
void FHeaderParser::InitScriptLocation( FScriptLocation& Retry )
|
|
{
|
|
Retry.Input = Input;
|
|
Retry.InputPos = InputPos;
|
|
Retry.InputLine = InputLine;
|
|
}
|
|
|
|
/**
|
|
* Return to a previously-saved retry point.
|
|
*
|
|
* @param Retry the point to return to
|
|
* @param Binary whether to modify the compiled bytecode
|
|
* @param bText whether to modify the compiler's current location in the text
|
|
*/
|
|
void FHeaderParser::ReturnToLocation(const FScriptLocation& Retry, bool Binary, bool bText)
|
|
{
|
|
if (bText)
|
|
{
|
|
Input = Retry.Input;
|
|
InputPos = Retry.InputPos;
|
|
InputLine = Retry.InputLine;
|
|
}
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------
|
|
Nest information.
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
//
|
|
// Return the name for a nest type.
|
|
//
|
|
const TCHAR *FHeaderParser::NestTypeName( ENestType NestType )
|
|
{
|
|
switch( NestType )
|
|
{
|
|
case ENestType::GlobalScope:
|
|
return TEXT("Global Scope");
|
|
case ENestType::Class:
|
|
return TEXT("Class");
|
|
case ENestType::NativeInterface:
|
|
case ENestType::Interface:
|
|
return TEXT("Interface");
|
|
case ENestType::FunctionDeclaration:
|
|
return TEXT("Function");
|
|
default:
|
|
check(false);
|
|
return TEXT("Unknown");
|
|
}
|
|
}
|
|
|
|
// Checks to see if a particular kind of command is allowed on this nesting level.
|
|
bool FHeaderParser::IsAllowedInThisNesting(ENestAllowFlags AllowFlags)
|
|
{
|
|
return (TopNest->Allow & AllowFlags) != ENestAllowFlags::None;
|
|
}
|
|
|
|
//
|
|
// Make sure that a particular kind of command is allowed on this nesting level.
|
|
// If it's not, issues a compiler error referring to the token and the current
|
|
// nesting level.
|
|
//
|
|
void FHeaderParser::CheckAllow( const TCHAR* Thing, ENestAllowFlags AllowFlags )
|
|
{
|
|
if (!IsAllowedInThisNesting(AllowFlags))
|
|
{
|
|
if (TopNest->NestType == ENestType::GlobalScope)
|
|
{
|
|
Throwf(TEXT("%s is not allowed before the Class definition"), Thing );
|
|
}
|
|
else
|
|
{
|
|
Throwf(TEXT("%s is not allowed here"), Thing );
|
|
}
|
|
}
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------
|
|
Nest management.
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
void FHeaderParser::PushNest(ENestType NestType, FUnrealStructDefinitionInfo* InNodeDef, FUnrealSourceFile* InSourceFile)
|
|
{
|
|
// Update pointer to top nesting level.
|
|
TopNest = &Nest[NestLevel++];
|
|
TopNest->SetScope(NestType == ENestType::GlobalScope ? &InSourceFile->GetScope().Get() : &InNodeDef->GetScope().Get());
|
|
TopNest->NestType = NestType;
|
|
|
|
// Prevent overnesting.
|
|
if (NestLevel >= MAX_NEST_LEVELS)
|
|
{
|
|
Throwf(TEXT("Maximum nesting limit exceeded"));
|
|
}
|
|
|
|
// Inherit info from stack node above us.
|
|
if (NestLevel > 1 && NestType == ENestType::GlobalScope)
|
|
{
|
|
// Use the existing stack node.
|
|
TopNest->SetScope(TopNest[-1].GetScope());
|
|
}
|
|
|
|
// NestType specific logic.
|
|
switch (NestType)
|
|
{
|
|
case ENestType::GlobalScope:
|
|
TopNest->Allow = ENestAllowFlags::Class | ENestAllowFlags::TypeDecl | ENestAllowFlags::ImplicitDelegateDecl;
|
|
break;
|
|
|
|
case ENestType::Class:
|
|
TopNest->Allow = ENestAllowFlags::VarDecl | ENestAllowFlags::Function | ENestAllowFlags::ImplicitDelegateDecl;
|
|
break;
|
|
|
|
case ENestType::NativeInterface:
|
|
case ENestType::Interface:
|
|
TopNest->Allow = ENestAllowFlags::Function;
|
|
break;
|
|
|
|
case ENestType::FunctionDeclaration:
|
|
TopNest->Allow = ENestAllowFlags::VarDecl;
|
|
|
|
break;
|
|
|
|
default:
|
|
Throwf(TEXT("Internal error in PushNest, type %i"), (uint8)NestType);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decrease the nesting level and handle any errors that result.
|
|
*
|
|
* @param NestType nesting type of the current node
|
|
* @param Descr text to use in error message if any errors are encountered
|
|
*/
|
|
void FHeaderParser::PopNest(ENestType NestType, const TCHAR* Descr)
|
|
{
|
|
// Validate the nesting state.
|
|
if (NestLevel <= 0)
|
|
{
|
|
Throwf(TEXT("Unexpected '%s' at global scope"), Descr, NestTypeName(NestType));
|
|
}
|
|
else if (TopNest->NestType != NestType)
|
|
{
|
|
Throwf(TEXT("Unexpected end of %s in '%s' block"), Descr, NestTypeName(TopNest->NestType));
|
|
}
|
|
|
|
if (NestType != ENestType::GlobalScope && NestType != ENestType::Class && NestType != ENestType::Interface && NestType != ENestType::NativeInterface && NestType != ENestType::FunctionDeclaration)
|
|
{
|
|
Throwf(TEXT("Bad first pass NestType %i"), (uint8)NestType);
|
|
}
|
|
|
|
// Pop the nesting level.
|
|
NestType = TopNest->NestType;
|
|
NestLevel--;
|
|
if (NestLevel == 0)
|
|
{
|
|
TopNest = nullptr;
|
|
}
|
|
else
|
|
{
|
|
TopNest--;
|
|
check(TopNest >= Nest);
|
|
|
|
}
|
|
}
|
|
|
|
void FHeaderParser::FixupDelegateProperties(FUnrealStructDefinitionInfo& StructDef, FScope& Scope, TMap<FName, FUnrealFunctionDefinitionInfo*>& DelegateCache )
|
|
{
|
|
for (TSharedRef<FUnrealPropertyDefinitionInfo> PropertyDef : StructDef.GetProperties())
|
|
{
|
|
FPropertyBase& PropertyBase = PropertyDef->GetPropertyBase();
|
|
|
|
// Ignore any containers types but static arrays
|
|
if (PropertyBase.ArrayType != EArrayType::None && PropertyBase.ArrayType != EArrayType::Static)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// At this point, we can check to see if we are a delegate or multicast delegate directly off the property definition
|
|
bool bIsDelegate = PropertyBase.Type == CPT_Delegate;
|
|
bool bIsMulticastDelegate = PropertyBase.Type == CPT_MulticastDelegate;
|
|
if (!bIsDelegate && !bIsMulticastDelegate)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// this FDelegateProperty corresponds to an actual delegate variable (i.e. delegate<SomeDelegate> Foo); we need to lookup the token data for
|
|
// this property and verify that the delegate property's "type" is an actual delegate function
|
|
FPropertyBase& DelegatePropertyBase = PropertyDef->GetPropertyBase();
|
|
|
|
// attempt to find the delegate function in the map of functions we've already found
|
|
FUnrealFunctionDefinitionInfo* SourceDelegateFunctionDef = DelegateCache.FindRef(DelegatePropertyBase.DelegateName);
|
|
if (SourceDelegateFunctionDef == nullptr)
|
|
{
|
|
FString NameOfDelegateFunction = DelegatePropertyBase.DelegateName.ToString() + FString( HEADER_GENERATED_DELEGATE_SIGNATURE_SUFFIX );
|
|
if ( !NameOfDelegateFunction.Contains(TEXT(".")) )
|
|
{
|
|
// an unqualified delegate function name - search for a delegate function by this name within the current scope
|
|
if (FUnrealFieldDefinitionInfo* FoundDef = Scope.FindTypeByName(*NameOfDelegateFunction))
|
|
{
|
|
SourceDelegateFunctionDef = FoundDef->AsFunction();
|
|
}
|
|
if (SourceDelegateFunctionDef == nullptr && DelegatePropertyBase.DelegateSignatureOwnerClassDef != nullptr)
|
|
{
|
|
SourceDelegateFunctionDef = FindFunction(*DelegatePropertyBase.DelegateSignatureOwnerClassDef, *NameOfDelegateFunction, true, nullptr);
|
|
}
|
|
if (SourceDelegateFunctionDef == nullptr)
|
|
{
|
|
FScopeLock Lock(&GlobalDelegatesLock);
|
|
SourceDelegateFunctionDef = GlobalDelegates.FindRef(NameOfDelegateFunction);
|
|
}
|
|
if (SourceDelegateFunctionDef == nullptr)
|
|
{
|
|
// convert this into a fully qualified path name for the error message.
|
|
NameOfDelegateFunction = Scope.GetName().ToString() + TEXT(".") + NameOfDelegateFunction;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
FString DelegateClassName, DelegateName;
|
|
NameOfDelegateFunction.Split(TEXT("."), &DelegateClassName, &DelegateName);
|
|
|
|
// verify that we got a valid string for the class name
|
|
if ( DelegateClassName.Len() == 0 )
|
|
{
|
|
UngetToken(PropertyDef->GetLineNumber(), PropertyDef->GetParsePosition());
|
|
Throwf(TEXT("Invalid scope specified in delegate property function reference: '%s'"), *NameOfDelegateFunction);
|
|
}
|
|
|
|
// verify that we got a valid string for the name of the function
|
|
if ( DelegateName.Len() == 0 )
|
|
{
|
|
UngetToken(PropertyDef->GetLineNumber(), PropertyDef->GetParsePosition());
|
|
Throwf(TEXT("Invalid delegate name specified in delegate property function reference '%s'"), *NameOfDelegateFunction);
|
|
}
|
|
|
|
// make sure that the class that contains the delegate can be referenced here
|
|
FUnrealClassDefinitionInfo* DelegateOwnerClassDef = FUnrealClassDefinitionInfo::FindScriptClassOrThrow(*this, DelegateClassName);
|
|
if (DelegateOwnerClassDef->GetScope()->FindTypeByName(*DelegateName) != nullptr)
|
|
{
|
|
Throwf(TEXT("Inaccessible type: '%s'"), *DelegateOwnerClassDef->GetPathName());
|
|
}
|
|
SourceDelegateFunctionDef = FindFunction(*DelegateOwnerClassDef, *DelegateName, false, nullptr);
|
|
}
|
|
|
|
if (SourceDelegateFunctionDef == NULL )
|
|
{
|
|
UngetToken(PropertyDef->GetLineNumber(), PropertyDef->GetParsePosition());
|
|
Throwf(TEXT("Failed to find delegate function '%s'"), *NameOfDelegateFunction);
|
|
}
|
|
else if (!SourceDelegateFunctionDef->HasAnyFunctionFlags(FUNC_Delegate))
|
|
{
|
|
UngetToken(PropertyDef->GetLineNumber(), PropertyDef->GetParsePosition());
|
|
Throwf(TEXT("Only delegate functions can be used as the type for a delegate property; '%s' is not a delegate."), *NameOfDelegateFunction);
|
|
}
|
|
}
|
|
|
|
// successfully found the delegate function that this delegate property corresponds to
|
|
|
|
// save this into the delegate cache for faster lookup later
|
|
DelegateCache.Add(DelegatePropertyBase.DelegateName, SourceDelegateFunctionDef);
|
|
|
|
// bind it to the delegate property
|
|
if (PropertyBase.Type == CPT_Delegate)
|
|
{
|
|
if( !SourceDelegateFunctionDef->HasAnyFunctionFlags( FUNC_MulticastDelegate ) )
|
|
{
|
|
PropertyDef->SetDelegateFunctionSignature(*SourceDelegateFunctionDef);
|
|
}
|
|
else
|
|
{
|
|
Throwf(TEXT("Unable to declare a single-cast delegate property for a multi-cast delegate type '%s'. Either add a 'multicast' qualifier to the property or change the delegate type to be single-cast as well."), *SourceDelegateFunctionDef->GetName());
|
|
}
|
|
}
|
|
else if (PropertyBase.Type == CPT_MulticastDelegate)
|
|
{
|
|
if (SourceDelegateFunctionDef->HasAnyFunctionFlags( FUNC_MulticastDelegate ))
|
|
{
|
|
PropertyDef->SetDelegateFunctionSignature(*SourceDelegateFunctionDef);
|
|
}
|
|
else
|
|
{
|
|
Throwf(TEXT("Unable to declare a multi-cast delegate property for a single-cast delegate type '%s'. Either remove the 'multicast' qualifier from the property or change the delegate type to be 'multicast' as well."), *SourceDelegateFunctionDef->GetName());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Functions might have their own delegate properties which need to be validated
|
|
for (TSharedRef<FUnrealFunctionDefinitionInfo> Function : StructDef.GetFunctions())
|
|
{
|
|
FixupDelegateProperties(*Function, Scope, DelegateCache);
|
|
}
|
|
|
|
CheckDocumentationPolicyForStruct(StructDef);
|
|
|
|
ParseRigVMMethodParameters(StructDef);
|
|
}
|
|
|
|
void FHeaderParser::CheckSparseClassData(const FUnrealStructDefinitionInfo& StructDef)
|
|
{
|
|
// we're looking for classes that have sparse class data structures
|
|
const FUnrealClassDefinitionInfo* ClassDef = UHTCast<FUnrealClassDefinitionInfo>(StructDef);
|
|
|
|
// make sure we don't try to have sparse class data inside of a struct instead of a class
|
|
if (StructDef.HasMetaData(FHeaderParserNames::NAME_SparseClassDataTypes))
|
|
{
|
|
if (ClassDef == nullptr)
|
|
{
|
|
StructDef.Throwf(TEXT("%s contains sparse class data but is not a class."), *StructDef.GetName());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return;
|
|
}
|
|
|
|
TArray<FString> SparseClassDataTypes;
|
|
ClassDef->GetSparseClassDataTypes(SparseClassDataTypes);
|
|
|
|
// for now we only support one sparse class data structure per class
|
|
if (SparseClassDataTypes.Num() > 1)
|
|
{
|
|
StructDef.Throwf(TEXT("Class %s contains multiple sparse class data types."), *ClassDef->GetName());
|
|
return;
|
|
}
|
|
if (SparseClassDataTypes.Num() == 0)
|
|
{
|
|
StructDef.Throwf(TEXT("Class %s has sparse class metadata but does not specify a type."), *ClassDef->GetName());
|
|
return;
|
|
}
|
|
|
|
for (const FString& SparseClassDataTypeName : SparseClassDataTypes)
|
|
{
|
|
FUnrealScriptStructDefinitionInfo* SparseClassDataStructDef = GTypeDefinitionInfoMap.FindByName<FUnrealScriptStructDefinitionInfo>(*SparseClassDataTypeName);
|
|
|
|
// make sure the sparse class data struct actually exists
|
|
if (!SparseClassDataStructDef)
|
|
{
|
|
StructDef.Throwf(TEXT("Unable to find sparse data type %s for class %s."), *SparseClassDataTypeName, *ClassDef->GetName());
|
|
return;
|
|
}
|
|
|
|
// check the data struct for invalid properties
|
|
for (FUnrealPropertyDefinitionInfo* PropertyDef : TUHTFieldRange<FUnrealPropertyDefinitionInfo>(*SparseClassDataStructDef))
|
|
{
|
|
if (PropertyDef->HasAnyPropertyFlags(CPF_BlueprintAssignable))
|
|
{
|
|
StructDef.Throwf(TEXT("Sparse class data types can not contain blueprint assignable delegates. Type '%s' Delegate '%s'"), *SparseClassDataStructDef->GetName(), *PropertyDef->GetName());
|
|
}
|
|
|
|
// all sparse properties should have EditDefaultsOnly
|
|
if (!PropertyDef->HasAllPropertyFlags(CPF_Edit | CPF_DisableEditOnInstance))
|
|
{
|
|
StructDef.Throwf(TEXT("Sparse class data types must be VisibleDefaultsOnly or EditDefaultsOnly. Type '%s' Property '%s'"), *SparseClassDataStructDef->GetName(), *PropertyDef->GetName());
|
|
}
|
|
|
|
// no sparse properties should have BlueprintReadWrite
|
|
if (PropertyDef->HasAllPropertyFlags(CPF_BlueprintVisible) && !PropertyDef->HasAllPropertyFlags(CPF_BlueprintReadOnly))
|
|
{
|
|
StructDef.Throwf(TEXT("Sparse class data types must not be BlueprintReadWrite. Type '%s' Property '%s'"), *SparseClassDataStructDef->GetName(), *PropertyDef->GetName());
|
|
}
|
|
}
|
|
|
|
// if the class's parent has a sparse class data struct then the current class must also use the same struct or one that inherits from it
|
|
|
|
const FUnrealClassDefinitionInfo* ParentClassDef = ClassDef->GetSuperClass();
|
|
TArray<FString> ParentSparseClassDataTypeNames;
|
|
ClassDef->GetSuperClass()->GetSparseClassDataTypes(ParentSparseClassDataTypeNames);
|
|
for (FString& ParentSparseClassDataTypeName : ParentSparseClassDataTypeNames)
|
|
{
|
|
if (FUnrealScriptStructDefinitionInfo* ParentSparseClassDataStructDef = GTypeDefinitionInfoMap.FindByName<FUnrealScriptStructDefinitionInfo>(*ParentSparseClassDataTypeName))
|
|
{
|
|
if (!SparseClassDataStructDef->IsChildOf(*ParentSparseClassDataStructDef))
|
|
{
|
|
StructDef.Throwf(TEXT("Class %s is a child of %s but its sparse class data struct, %s, does not inherit from %s."), *ClassDef->GetName(), *ParentClassDef->GetName(), *SparseClassDataStructDef->GetName(), *ParentSparseClassDataStructDef->GetName());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FHeaderParser::ValidateClassFlags(const FUnrealClassDefinitionInfo& ToValidate)
|
|
{
|
|
if (ToValidate.HasAnyClassFlags(CLASS_NeedsDeferredDependencyLoading) && !ToValidate.IsChildOf(*GUClassDef))
|
|
{
|
|
// CLASS_NeedsDeferredDependencyLoading can only be set on classes derived from UClass
|
|
ToValidate.Throwf(TEXT("NeedsDeferredDependencyLoading is set on %s but the flag can only be used with classes derived from UClass."), *ToValidate.GetName());
|
|
}
|
|
}
|
|
|
|
void FHeaderParser::VerifyBlueprintPropertyGetter(FUnrealPropertyDefinitionInfo& PropertyDef, FUnrealFunctionDefinitionInfo* TargetFuncDef)
|
|
{
|
|
check(TargetFuncDef);
|
|
|
|
const TArray<TSharedRef<FUnrealPropertyDefinitionInfo>>& Properties = TargetFuncDef->GetProperties();
|
|
FUnrealPropertyDefinitionInfo* ReturnPropDef = TargetFuncDef->GetReturn();
|
|
if (Properties.Num() > 1 || (Properties.Num() == 1 && ReturnPropDef == nullptr))
|
|
{
|
|
LogError(TEXT("Blueprint Property getter function %s must not have parameters."), *TargetFuncDef->GetName());
|
|
}
|
|
|
|
if (ReturnPropDef == nullptr || !PropertyDef.SameType(*ReturnPropDef))
|
|
{
|
|
FString ExtendedCPPType;
|
|
FString CPPType = PropertyDef.GetCPPType(&ExtendedCPPType);
|
|
LogError(TEXT("Blueprint Property getter function %s must have return value of type %s%s."), *TargetFuncDef->GetName(), *CPPType, *ExtendedCPPType);
|
|
}
|
|
|
|
if (TargetFuncDef->HasAnyFunctionFlags(FUNC_Event))
|
|
{
|
|
LogError(TEXT("Blueprint Property setter function cannot be a blueprint event."));
|
|
}
|
|
else if (!TargetFuncDef->HasAnyFunctionFlags(FUNC_BlueprintPure))
|
|
{
|
|
LogError(TEXT("Blueprint Property getter function must be pure."));
|
|
}
|
|
}
|
|
|
|
void FHeaderParser::VerifyBlueprintPropertySetter(FUnrealPropertyDefinitionInfo& PropertyDef, FUnrealFunctionDefinitionInfo* TargetFuncDef)
|
|
{
|
|
check(TargetFuncDef);
|
|
FUnrealPropertyDefinitionInfo* ReturnPropDef = TargetFuncDef->GetReturn();
|
|
|
|
if (ReturnPropDef)
|
|
{
|
|
LogError(TEXT("Blueprint Property setter function %s must not have a return value."), *TargetFuncDef->GetName());
|
|
}
|
|
else
|
|
{
|
|
const TArray<TSharedRef<FUnrealPropertyDefinitionInfo>>& Properties = TargetFuncDef->GetProperties();
|
|
if (TargetFuncDef->GetProperties().Num() != 1 || !PropertyDef.SameType(*Properties[0]))
|
|
{
|
|
FString ExtendedCPPType;
|
|
FString CPPType = PropertyDef.GetCPPType(&ExtendedCPPType);
|
|
LogError(TEXT("Blueprint Property setter function %s must have exactly one parameter of type %s%s."), *TargetFuncDef->GetName(), *CPPType, *ExtendedCPPType);
|
|
}
|
|
}
|
|
|
|
if (TargetFuncDef->HasAnyFunctionFlags(FUNC_Event))
|
|
{
|
|
LogError(TEXT("Blueprint Property setter function cannot be a blueprint event."));
|
|
}
|
|
else if (!TargetFuncDef->HasAnyFunctionFlags(FUNC_BlueprintCallable))
|
|
{
|
|
LogError(TEXT("Blueprint Property setter function must be blueprint callable."));
|
|
}
|
|
else if (TargetFuncDef->HasAnyFunctionFlags(FUNC_BlueprintPure))
|
|
{
|
|
LogError(TEXT("Blueprint Property setter function must not be pure."));
|
|
}
|
|
}
|
|
|
|
void FHeaderParser::VerifyRepNotifyCallback(FUnrealPropertyDefinitionInfo& PropertyDef, FUnrealFunctionDefinitionInfo* TargetFuncDef)
|
|
{
|
|
if (TargetFuncDef)
|
|
{
|
|
if (TargetFuncDef->GetReturn())
|
|
{
|
|
LogError(TEXT("Replication notification function %s must not have return value."), *TargetFuncDef->GetName());
|
|
}
|
|
|
|
const bool bIsArrayProperty = PropertyDef.IsStaticArray() || PropertyDef.IsDynamicArray();
|
|
const int32 MaxParms = bIsArrayProperty ? 2 : 1;
|
|
|
|
const TArray<TSharedRef<FUnrealPropertyDefinitionInfo>>& Properties = TargetFuncDef->GetProperties();
|
|
if (Properties.Num() > MaxParms)
|
|
{
|
|
LogError(TEXT("Replication notification function %s has too many parameters."), *TargetFuncDef->GetName());
|
|
}
|
|
|
|
if (Properties.Num() >= 1)
|
|
{
|
|
const FUnrealPropertyDefinitionInfo* ParmDef = &*Properties[0];
|
|
// First parameter is always the old value:
|
|
if (!PropertyDef.SameType(*ParmDef))
|
|
{
|
|
FString ExtendedCPPType;
|
|
FString CPPType = ParmDef->GetCPPType(&ExtendedCPPType);
|
|
LogError(TEXT("Replication notification function %s has invalid parameter for property %s. First (optional) parameter must be of type %s%s."), *TargetFuncDef->GetName(), *ParmDef->GetName(), *CPPType, *ExtendedCPPType);
|
|
}
|
|
}
|
|
|
|
if (TargetFuncDef->GetProperties().Num() >= 2)
|
|
{
|
|
FUnrealPropertyDefinitionInfo* ParmDef = &*Properties[1];
|
|
FPropertyBase& ParmBase = ParmDef->GetPropertyBase();
|
|
// A 2nd parameter for arrays can be specified as a const TArray<uint8>&. This is a list of element indices that have changed
|
|
if (!(ParmBase.Type == CPT_Byte && ParmBase.ArrayType == EArrayType::Dynamic && ParmDef->HasAllPropertyFlags(CPF_ConstParm | CPF_ReferenceParm)))
|
|
{
|
|
LogError(TEXT("Replication notification function %s (optional) second parameter must be of type 'const TArray<uint8>&'"), *TargetFuncDef->GetName());
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Couldn't find a valid function...
|
|
LogError(TEXT("Replication notification function %s not found"), *PropertyDef.GetRepNotifyFunc().ToString() );
|
|
}
|
|
}
|
|
|
|
void FHeaderParser::VerifyNotifyValueChangedProperties(FUnrealPropertyDefinitionInfo& PropertyDef)
|
|
{
|
|
FUnrealTypeDefinitionInfo* Info = PropertyDef.GetOuter();
|
|
if (Info == nullptr)
|
|
{
|
|
LogError(TEXT("FieldNofity property %s does not have a outer."), *PropertyDef.GetName());
|
|
return;
|
|
}
|
|
|
|
FUnrealClassDefinitionInfo* ClassInfo = Info->AsClass();
|
|
if (ClassInfo == nullptr)
|
|
{
|
|
LogError(TEXT("FieldNofity property are only valid as UClass member variable."));
|
|
return;
|
|
}
|
|
if (ClassInfo->IsInterface())
|
|
{
|
|
LogError(TEXT("FieldNofity are not valid on UInterface."));
|
|
return;
|
|
}
|
|
|
|
ClassInfo->MarkHasFieldNotify();
|
|
}
|
|
|
|
void FHeaderParser::VerifyNotifyValueChangedFunction(const FUnrealFunctionDefinitionInfo& TargetFuncDef)
|
|
{
|
|
FUnrealTypeDefinitionInfo* Info = TargetFuncDef.GetOuter();
|
|
if (Info == nullptr)
|
|
{
|
|
LogError(TEXT("FieldNofity function %s does not have a outer."), *TargetFuncDef.GetName());
|
|
return;
|
|
}
|
|
|
|
FUnrealClassDefinitionInfo* ClassInfo = Info->AsClass();
|
|
if (ClassInfo == nullptr)
|
|
{
|
|
LogError(TEXT("FieldNofity function %s are only valid as UClass member function."), *TargetFuncDef.GetName());
|
|
return;
|
|
}
|
|
|
|
const TArray<TSharedRef<FUnrealPropertyDefinitionInfo>>& Properties = TargetFuncDef.GetProperties();
|
|
FUnrealPropertyDefinitionInfo* ReturnPropDef = TargetFuncDef.GetReturn();
|
|
if (Properties.Num() > 1 || (Properties.Num() == 1 && ReturnPropDef == nullptr))
|
|
{
|
|
LogError(TEXT("FieldNotify function %s must not have parameters."), *TargetFuncDef.GetName());
|
|
return;
|
|
}
|
|
if (ReturnPropDef == nullptr)
|
|
{
|
|
LogError(TEXT("FieldNotify function %s must return a value."), *TargetFuncDef.GetName());
|
|
return;
|
|
}
|
|
if (TargetFuncDef.HasAnyFunctionFlags(FUNC_Event))
|
|
{
|
|
LogError(TEXT("FieldNotify function %s cannot be a blueprint event."), *TargetFuncDef.GetName());
|
|
return;
|
|
}
|
|
if (!TargetFuncDef.HasAnyFunctionFlags(FUNC_BlueprintPure))
|
|
{
|
|
LogError(TEXT("FieldNotify function %s must be pure."), *TargetFuncDef.GetName());
|
|
return;
|
|
}
|
|
|
|
ClassInfo->MarkHasFieldNotify();
|
|
}
|
|
|
|
void FHeaderParser::VerifyPropertyMarkups(FUnrealClassDefinitionInfo& TargetClassDef)
|
|
{
|
|
// Iterate over all properties, looking for those flagged as CPF_RepNotify
|
|
for (TSharedRef<FUnrealPropertyDefinitionInfo> PropertyDef : TargetClassDef.GetProperties())
|
|
{
|
|
auto FindTargetFunction = [&](const FName FuncName) -> FUnrealFunctionDefinitionInfo*
|
|
{
|
|
// Search through this class and its super classes looking for the specified callback
|
|
for (FUnrealClassDefinitionInfo* SearchClassDef = &TargetClassDef; SearchClassDef; SearchClassDef = SearchClassDef->GetSuperClass())
|
|
{
|
|
// Since the function map is not valid yet, we have to iterate over the fields to look for the function
|
|
for (TSharedRef<FUnrealFunctionDefinitionInfo> FunctionDef : SearchClassDef->GetFunctions())
|
|
{
|
|
if (FunctionDef->GetFName() == FuncName)
|
|
{
|
|
return &*FunctionDef;
|
|
}
|
|
}
|
|
}
|
|
return nullptr;
|
|
};
|
|
|
|
TGuardValue<int32> GuardedInputPos(InputPos, PropertyDef->GetParsePosition());
|
|
TGuardValue<int32> GuardedInputLine(InputLine, PropertyDef->GetLineNumber());
|
|
|
|
if (PropertyDef->HasAnyPropertyFlags(CPF_RepNotify))
|
|
{
|
|
VerifyRepNotifyCallback(*PropertyDef, FindTargetFunction(PropertyDef->GetRepNotifyFunc()));
|
|
}
|
|
|
|
if (PropertyDef->HasAnyPropertyFlags(CPF_BlueprintVisible))
|
|
{
|
|
const FString& GetterFuncName = PropertyDef->GetMetaData(NAME_BlueprintGetter);
|
|
if (!GetterFuncName.IsEmpty())
|
|
{
|
|
if (FUnrealFunctionDefinitionInfo* TargetFuncDef = FindTargetFunction(*GetterFuncName))
|
|
{
|
|
VerifyBlueprintPropertyGetter(*PropertyDef, TargetFuncDef);
|
|
}
|
|
else
|
|
{
|
|
// Couldn't find a valid function...
|
|
LogError(TEXT("Blueprint Property getter function %s not found"), *GetterFuncName);
|
|
}
|
|
}
|
|
|
|
if (!PropertyDef->HasAnyPropertyFlags(CPF_BlueprintReadOnly))
|
|
{
|
|
const FString& SetterFuncName = PropertyDef->GetMetaData(NAME_BlueprintSetter);
|
|
if (!SetterFuncName.IsEmpty())
|
|
{
|
|
if (FUnrealFunctionDefinitionInfo* TargetFuncDef = FindTargetFunction(*SetterFuncName))
|
|
{
|
|
VerifyBlueprintPropertySetter(*PropertyDef, TargetFuncDef);
|
|
}
|
|
else
|
|
{
|
|
// Couldn't find a valid function...
|
|
LogError(TEXT("Blueprint Property setter function %s not found"), *SetterFuncName);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verify if native property setter and getter functions could actually be found while parsing class header
|
|
FPropertyBase& PropertyToken = PropertyDef->GetPropertyBase();
|
|
if (!PropertyToken.SetterName.IsEmpty())
|
|
{
|
|
if (PropertyToken.SetterName != TEXT("None") && !PropertyToken.bSetterFunctionFound)
|
|
{
|
|
LogError(TEXT("Property %s setter function %s not found"), *PropertyDef->GetName(), *PropertyToken.SetterName);
|
|
}
|
|
}
|
|
if (!PropertyToken.GetterName.IsEmpty())
|
|
{
|
|
if (PropertyToken.GetterName != TEXT("None") && !PropertyToken.bGetterFunctionFound)
|
|
{
|
|
LogError(TEXT("Property %s getter function %s not found"), *PropertyDef->GetName(), *PropertyToken.GetterName);
|
|
}
|
|
}
|
|
|
|
if (PropertyDef->GetPropertyBase().bFieldNotify)
|
|
{
|
|
VerifyNotifyValueChangedProperties(*PropertyDef);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FHeaderParser::VerifyFunctionsMarkups(FUnrealClassDefinitionInfo& TargetClassDef)
|
|
{
|
|
for (TSharedRef<FUnrealFunctionDefinitionInfo> FunctionDef : TargetClassDef.GetFunctions())
|
|
{
|
|
if (FunctionDef->GetFunctionData().bFieldNotify)
|
|
{
|
|
VerifyNotifyValueChangedFunction(*FunctionDef);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*-----------------------------------------------------------------------------
|
|
Compiler directives.
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
//
|
|
// Process a compiler directive.
|
|
//
|
|
void FHeaderParser::CompileDirective()
|
|
{
|
|
FToken Directive;
|
|
|
|
int32 LineAtStartOfDirective = InputLine;
|
|
// Define directive are skipped but they can be multiline.
|
|
bool bDefineDirective = false;
|
|
|
|
if (!GetIdentifier(Directive))
|
|
{
|
|
Throwf(TEXT("Missing compiler directive after '#'") );
|
|
}
|
|
else if (Directive.IsValue(TEXT("error"), ESearchCase::CaseSensitive))
|
|
{
|
|
Throwf(TEXT("#error directive encountered") );
|
|
}
|
|
else if (Directive.IsValue(TEXT("pragma"), ESearchCase::CaseSensitive))
|
|
{
|
|
// Ignore all pragmas
|
|
}
|
|
else if (Directive.IsValue(TEXT("linenumber"), ESearchCase::CaseSensitive))
|
|
{
|
|
FToken Number;
|
|
if (!GetToken(Number) || !Number.IsConstInt())
|
|
{
|
|
Throwf(TEXT("Missing line number in line number directive"));
|
|
}
|
|
|
|
int32 newInputLine;
|
|
if ( Number.GetConstInt(newInputLine) )
|
|
{
|
|
InputLine = newInputLine;
|
|
}
|
|
}
|
|
else if (Directive.IsValue(TEXT("include"), ESearchCase::CaseSensitive))
|
|
{
|
|
FToken IncludeName;
|
|
if (GetToken(IncludeName) && IncludeName.IsConstString())
|
|
{
|
|
FTokenString InludeNameString = IncludeName.GetTokenString();
|
|
const FString& ExpectedHeaderName = SourceFile.GetGeneratedHeaderFilename();
|
|
if (FCString::Stricmp(*InludeNameString, *ExpectedHeaderName) == 0)
|
|
{
|
|
bSpottedAutogeneratedHeaderInclude = true;
|
|
}
|
|
}
|
|
}
|
|
else if (Directive.IsValue(TEXT("if"), ESearchCase::CaseSensitive))
|
|
{
|
|
// Eat the ! if present
|
|
bool bNotDefined = MatchSymbol(TEXT('!'));
|
|
|
|
int32 TempInt;
|
|
const bool bParsedInt = GetConstInt(TempInt);
|
|
if (bParsedInt && (TempInt == 0 || TempInt == 1))
|
|
{
|
|
PushCompilerDirective(ECompilerDirective::Insignificant);
|
|
}
|
|
else
|
|
{
|
|
FToken Define;
|
|
if (!GetIdentifier(Define))
|
|
{
|
|
Throwf(TEXT("Missing define name '#if'") );
|
|
}
|
|
|
|
if ( Define.IsValue(TEXT("WITH_EDITORONLY_DATA"), ESearchCase::CaseSensitive) )
|
|
{
|
|
PushCompilerDirective(ECompilerDirective::WithEditorOnlyData);
|
|
}
|
|
else if ( Define.IsValue(TEXT("WITH_EDITOR"), ESearchCase::CaseSensitive) )
|
|
{
|
|
PushCompilerDirective(ECompilerDirective::WithEditor);
|
|
}
|
|
else if (Define.IsValue(TEXT("WITH_HOT_RELOAD"), ESearchCase::CaseSensitive) || Define.IsValue(TEXT("WITH_HOT_RELOAD_CTORS"), ESearchCase::CaseSensitive) || Define.IsSymbol(TEXT('1'))) // @TODO: This symbol test can never work after a GetIdentifier
|
|
{
|
|
PushCompilerDirective(ECompilerDirective::Insignificant);
|
|
}
|
|
else if ( Define.IsValue(TEXT("CPP"), ESearchCase::CaseSensitive) && bNotDefined)
|
|
{
|
|
PushCompilerDirective(ECompilerDirective::Insignificant);
|
|
}
|
|
else
|
|
{
|
|
Throwf(TEXT("Unknown define '#if %s' in class or global scope"), *Define.GetTokenValue());
|
|
}
|
|
}
|
|
}
|
|
else if (Directive.IsValue(TEXT("endif"), ESearchCase::CaseSensitive))
|
|
{
|
|
PopCompilerDirective();
|
|
}
|
|
else if (Directive.IsValue(TEXT("define"), ESearchCase::CaseSensitive))
|
|
{
|
|
// Ignore the define directive (can be multiline).
|
|
bDefineDirective = true;
|
|
}
|
|
else if (Directive.IsValue(TEXT("ifdef"), ESearchCase::CaseSensitive) || Directive.IsValue(TEXT("ifndef"), ESearchCase::CaseSensitive))
|
|
{
|
|
PushCompilerDirective(ECompilerDirective::Insignificant);
|
|
}
|
|
else if (Directive.IsValue(TEXT("undef"), ESearchCase::CaseSensitive) || Directive.IsValue(TEXT("else"), ESearchCase::CaseSensitive))
|
|
{
|
|
// Ignore. UHT can only handle #if directive
|
|
}
|
|
else
|
|
{
|
|
Throwf(TEXT("Unrecognized compiler directive %s"), *Directive.GetTokenValue() );
|
|
}
|
|
|
|
// Skip to end of line (or end of multiline #define).
|
|
if (LineAtStartOfDirective == InputLine)
|
|
{
|
|
TCHAR LastCharacter = TEXT('\0');
|
|
TCHAR c;
|
|
do
|
|
{
|
|
while ( !IsEOL( c=GetChar() ) )
|
|
{
|
|
LastCharacter = c;
|
|
}
|
|
}
|
|
// Continue until the entire multiline directive has been skipped.
|
|
while (LastCharacter == '\\' && bDefineDirective);
|
|
|
|
if (c == 0)
|
|
{
|
|
UngetChar();
|
|
}
|
|
|
|
// Remove any comments parsed to prevent any trailing comments from being picked up
|
|
ClearComment();
|
|
}
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------
|
|
Variable declaration parser.
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
void FHeaderParser::GetVarType(
|
|
FScope* Scope,
|
|
EGetVarTypeOptions Options,
|
|
FPropertyBase& VarProperty,
|
|
EPropertyFlags Disallow,
|
|
EUHTPropertyType OuterPropertyType,
|
|
EPropertyFlags OuterPropertyFlags,
|
|
EPropertyDeclarationStyle::Type PropertyDeclarationStyle,
|
|
EVariableCategory VariableCategory,
|
|
FIndexRange* ParsedVarIndexRange,
|
|
ELayoutMacroType* OutLayoutMacroType
|
|
)
|
|
{
|
|
FUnrealStructDefinitionInfo* OwnerStructDef = Scope->IsFileScope() ? nullptr : &((FStructScope*)Scope)->GetStructDef();
|
|
FUnrealScriptStructDefinitionInfo* OwnerScriptStructDef = UHTCast<FUnrealScriptStructDefinitionInfo>(OwnerStructDef);
|
|
FUnrealClassDefinitionInfo* OwnerClassDef = UHTCast<FUnrealClassDefinitionInfo>(OwnerStructDef);
|
|
FName RepCallbackName = FName(NAME_None);
|
|
FString SetterName;
|
|
FString GetterName;
|
|
bool bHasSetterTag = false;
|
|
bool bHasGetterTag = false;
|
|
bool bFieldNotify = false;
|
|
|
|
// Get flags.
|
|
EPropertyFlags Flags = CPF_None;
|
|
EPropertyFlags ImpliedFlags = CPF_None;
|
|
|
|
// force members to be 'blueprint read only' if in a const class
|
|
if (VariableCategory == EVariableCategory::Member)
|
|
{
|
|
if (OwnerClassDef)
|
|
{
|
|
if (OwnerClassDef->HasAnyClassFlags(CLASS_Const))
|
|
{
|
|
ImpliedFlags |= CPF_BlueprintReadOnly;
|
|
}
|
|
}
|
|
}
|
|
uint32 ExportFlags = PROPEXPORT_Public;
|
|
|
|
// Build up a list of specifiers
|
|
TArray<FPropertySpecifier> SpecifiersFound;
|
|
|
|
TMap<FName, FString> MetaDataFromNewStyle;
|
|
bool bNativeConst = false;
|
|
bool bNativeConstTemplateArg = false;
|
|
|
|
const bool bIsParamList = (VariableCategory != EVariableCategory::Member) && MatchIdentifier(TEXT("UPARAM"), ESearchCase::CaseSensitive);
|
|
|
|
// No specifiers are allowed inside a TArray
|
|
if (OuterPropertyType != EUHTPropertyType::DynamicArray)
|
|
{
|
|
// New-style UPROPERTY() syntax
|
|
if (PropertyDeclarationStyle == EPropertyDeclarationStyle::UPROPERTY || bIsParamList)
|
|
{
|
|
ReadSpecifierSetInsideMacro(SpecifiersFound, TEXT("Variable"), MetaDataFromNewStyle);
|
|
}
|
|
}
|
|
|
|
if (VariableCategory != EVariableCategory::Member)
|
|
{
|
|
// const before the variable type support (only for params)
|
|
if (MatchIdentifier(TEXT("const"), ESearchCase::CaseSensitive))
|
|
{
|
|
Flags |= CPF_ConstParm;
|
|
bNativeConst = true;
|
|
}
|
|
}
|
|
|
|
uint32 CurrentCompilerDirective = GetCurrentCompilerDirective();
|
|
if ((CurrentCompilerDirective & ECompilerDirective::WithEditorOnlyData) != 0)
|
|
{
|
|
Flags |= CPF_EditorOnly;
|
|
}
|
|
else if ((CurrentCompilerDirective & ECompilerDirective::WithEditor) != 0)
|
|
{
|
|
// Checking for this error is a bit tricky given legacy code.
|
|
// 1) If already wrapped in WITH_EDITORONLY_DATA (see above), then we ignore the error via the else
|
|
// 2) Ignore any module that is an editor module
|
|
const FManifestModule& Module = Scope->GetFileScope()->GetSourceFile()->GetPackageDef().GetModule();
|
|
const bool bIsEditorModule =
|
|
Module.ModuleType == EBuildModuleType::EngineEditor ||
|
|
Module.ModuleType == EBuildModuleType::GameEditor ||
|
|
Module.ModuleType == EBuildModuleType::EngineUncooked ||
|
|
Module.ModuleType == EBuildModuleType::GameUncooked;
|
|
if (VariableCategory == EVariableCategory::Member && !bIsEditorModule)
|
|
{
|
|
LogError(TEXT("UProperties should not be wrapped by WITH_EDITOR, use WITH_EDITORONLY_DATA instead."));
|
|
}
|
|
}
|
|
|
|
// Validate if we are using editor only data in a class or struct definition
|
|
if ((Flags & CPF_EditorOnly) != 0)
|
|
{
|
|
if (OwnerClassDef && OwnerClassDef->HasAnyClassFlags(CLASS_Optional))
|
|
{
|
|
LogError(TEXT("Cannot specify an editor only property inside an optional class."));
|
|
}
|
|
}
|
|
|
|
// Store the start and end positions of the parsed type
|
|
if (ParsedVarIndexRange)
|
|
{
|
|
ParsedVarIndexRange->StartIndex = InputPos;
|
|
}
|
|
|
|
// Process the list of specifiers
|
|
bool bSeenEditSpecifier = false;
|
|
bool bSeenBlueprintWriteSpecifier = false;
|
|
bool bSeenBlueprintReadOnlySpecifier = false;
|
|
bool bSeenBlueprintGetterSpecifier = false;
|
|
for (const FPropertySpecifier& Specifier : SpecifiersFound)
|
|
{
|
|
EVariableSpecifier SpecID = (EVariableSpecifier)Algo::FindSortedStringCaseInsensitive(*Specifier.Key, GVariableSpecifierStrings);
|
|
if (VariableCategory == EVariableCategory::Member)
|
|
{
|
|
switch (SpecID)
|
|
{
|
|
case EVariableSpecifier::EditAnywhere:
|
|
{
|
|
if (bSeenEditSpecifier)
|
|
{
|
|
LogError(TEXT("Found more than one edit/visibility specifier (%s), only one is allowed"), *Specifier.Key);
|
|
}
|
|
Flags |= CPF_Edit;
|
|
bSeenEditSpecifier = true;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::EditInstanceOnly:
|
|
{
|
|
if (bSeenEditSpecifier)
|
|
{
|
|
LogError(TEXT("Found more than one edit/visibility specifier (%s), only one is allowed"), *Specifier.Key);
|
|
}
|
|
Flags |= CPF_Edit | CPF_DisableEditOnTemplate;
|
|
bSeenEditSpecifier = true;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::EditDefaultsOnly:
|
|
{
|
|
if (bSeenEditSpecifier)
|
|
{
|
|
LogError(TEXT("Found more than one edit/visibility specifier (%s), only one is allowed"), *Specifier.Key);
|
|
}
|
|
Flags |= CPF_Edit | CPF_DisableEditOnInstance;
|
|
bSeenEditSpecifier = true;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::VisibleAnywhere:
|
|
{
|
|
if (bSeenEditSpecifier)
|
|
{
|
|
LogError(TEXT("Found more than one edit/visibility specifier (%s), only one is allowed"), *Specifier.Key);
|
|
}
|
|
Flags |= CPF_Edit | CPF_EditConst;
|
|
bSeenEditSpecifier = true;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::VisibleInstanceOnly:
|
|
{
|
|
if (bSeenEditSpecifier)
|
|
{
|
|
LogError(TEXT("Found more than one edit/visibility specifier (%s), only one is allowed"), *Specifier.Key);
|
|
}
|
|
Flags |= CPF_Edit | CPF_EditConst | CPF_DisableEditOnTemplate;
|
|
bSeenEditSpecifier = true;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::VisibleDefaultsOnly:
|
|
{
|
|
if (bSeenEditSpecifier)
|
|
{
|
|
LogError(TEXT("Found more than one edit/visibility specifier (%s), only one is allowed"), *Specifier.Key);
|
|
}
|
|
Flags |= CPF_Edit | CPF_EditConst | CPF_DisableEditOnInstance;
|
|
bSeenEditSpecifier = true;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::BlueprintReadWrite:
|
|
{
|
|
if (bSeenBlueprintReadOnlySpecifier)
|
|
{
|
|
LogError(TEXT("Cannot specify a property as being both BlueprintReadOnly and BlueprintReadWrite."));
|
|
}
|
|
|
|
const FString* PrivateAccessMD = MetaDataFromNewStyle.Find(NAME_AllowPrivateAccess); // FBlueprintMetadata::MD_AllowPrivateAccess
|
|
const bool bAllowPrivateAccess = PrivateAccessMD ? (*PrivateAccessMD != TEXT("false")) : false;
|
|
if (CurrentAccessSpecifier == ACCESS_Private && !bAllowPrivateAccess)
|
|
{
|
|
LogError(TEXT("BlueprintReadWrite should not be used on private members"));
|
|
}
|
|
|
|
if ((Flags & CPF_EditorOnly) != 0 && OwnerScriptStructDef != nullptr)
|
|
{
|
|
LogError(TEXT("Blueprint exposed struct members cannot be editor only"));
|
|
}
|
|
|
|
Flags |= CPF_BlueprintVisible;
|
|
bSeenBlueprintWriteSpecifier = true;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::BlueprintSetter:
|
|
{
|
|
if (bSeenBlueprintReadOnlySpecifier)
|
|
{
|
|
LogError(TEXT("Cannot specify a property as being both BlueprintReadOnly and having a BlueprintSetter."));
|
|
}
|
|
|
|
if (OwnerScriptStructDef != nullptr)
|
|
{
|
|
LogError(TEXT("Cannot specify BlueprintSetter for a struct member."));
|
|
}
|
|
|
|
const FString BlueprintSetterFunction = RequireExactlyOneSpecifierValue(*this, Specifier);
|
|
MetaDataFromNewStyle.Add(NAME_BlueprintSetter, BlueprintSetterFunction);
|
|
|
|
Flags |= CPF_BlueprintVisible;
|
|
bSeenBlueprintWriteSpecifier = true;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::BlueprintReadOnly:
|
|
{
|
|
if (bSeenBlueprintWriteSpecifier)
|
|
{
|
|
LogError(TEXT("Cannot specify both BlueprintReadOnly and BlueprintReadWrite or BlueprintSetter."), *Specifier.Key);
|
|
}
|
|
|
|
const FString* PrivateAccessMD = MetaDataFromNewStyle.Find(NAME_AllowPrivateAccess); // FBlueprintMetadata::MD_AllowPrivateAccess
|
|
const bool bAllowPrivateAccess = PrivateAccessMD ? (*PrivateAccessMD != TEXT("false")) : false;
|
|
if (CurrentAccessSpecifier == ACCESS_Private && !bAllowPrivateAccess)
|
|
{
|
|
LogError(TEXT("BlueprintReadOnly should not be used on private members"));
|
|
}
|
|
|
|
if ((Flags & CPF_EditorOnly) != 0 && OwnerScriptStructDef != nullptr)
|
|
{
|
|
LogError(TEXT("Blueprint exposed struct members cannot be editor only"));
|
|
}
|
|
|
|
Flags |= CPF_BlueprintVisible | CPF_BlueprintReadOnly;
|
|
ImpliedFlags &= ~CPF_BlueprintReadOnly;
|
|
bSeenBlueprintReadOnlySpecifier = true;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::BlueprintGetter:
|
|
{
|
|
if (OwnerScriptStructDef != nullptr)
|
|
{
|
|
LogError(TEXT("Cannot specify BlueprintGetter for a struct member."));
|
|
}
|
|
|
|
const FString BlueprintGetterFunction = RequireExactlyOneSpecifierValue(*this, Specifier);
|
|
MetaDataFromNewStyle.Add(NAME_BlueprintGetter, BlueprintGetterFunction);
|
|
|
|
Flags |= CPF_BlueprintVisible;
|
|
bSeenBlueprintGetterSpecifier = true;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::Config:
|
|
{
|
|
Flags |= CPF_Config;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::GlobalConfig:
|
|
{
|
|
Flags |= CPF_GlobalConfig | CPF_Config;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::Localized:
|
|
{
|
|
LogError(TEXT("The Localized specifier is deprecated"));
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::Transient:
|
|
{
|
|
Flags |= CPF_Transient;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::DuplicateTransient:
|
|
{
|
|
Flags |= CPF_DuplicateTransient;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::TextExportTransient:
|
|
{
|
|
Flags |= CPF_TextExportTransient;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::NonPIETransient:
|
|
{
|
|
LogWarning(TEXT("NonPIETransient is deprecated - NonPIEDuplicateTransient should be used instead"));
|
|
Flags |= CPF_NonPIEDuplicateTransient;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::NonPIEDuplicateTransient:
|
|
{
|
|
Flags |= CPF_NonPIEDuplicateTransient;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::Export:
|
|
{
|
|
Flags |= CPF_ExportObject;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::EditInline:
|
|
{
|
|
LogError(TEXT("EditInline is deprecated. Remove it, or use Instanced instead."));
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::NoClear:
|
|
{
|
|
Flags |= CPF_NoClear;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::EditFixedSize:
|
|
{
|
|
Flags |= CPF_EditFixedSize;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::Replicated:
|
|
case EVariableSpecifier::ReplicatedUsing:
|
|
{
|
|
if (OwnerScriptStructDef != nullptr)
|
|
{
|
|
LogError(TEXT("Struct members cannot be replicated"));
|
|
}
|
|
|
|
Flags |= CPF_Net;
|
|
|
|
// See if we've specified a rep notification function
|
|
if (SpecID == EVariableSpecifier::ReplicatedUsing)
|
|
{
|
|
RepCallbackName = FName(*RequireExactlyOneSpecifierValue(*this, Specifier));
|
|
Flags |= CPF_RepNotify;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::NotReplicated:
|
|
{
|
|
if (OwnerScriptStructDef == nullptr)
|
|
{
|
|
LogError(TEXT("Only Struct members can be marked NotReplicated"));
|
|
}
|
|
|
|
Flags |= CPF_RepSkip;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::RepRetry:
|
|
{
|
|
LogError(TEXT("'RepRetry' is deprecated."));
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::Interp:
|
|
{
|
|
Flags |= CPF_Edit;
|
|
Flags |= CPF_BlueprintVisible;
|
|
Flags |= CPF_Interp;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::NonTransactional:
|
|
{
|
|
Flags |= CPF_NonTransactional;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::Instanced:
|
|
{
|
|
Flags |= CPF_PersistentInstance | CPF_ExportObject | CPF_InstancedReference;
|
|
AddEditInlineMetaData(MetaDataFromNewStyle);
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::BlueprintAssignable:
|
|
{
|
|
Flags |= CPF_BlueprintAssignable;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::BlueprintCallable:
|
|
{
|
|
Flags |= CPF_BlueprintCallable;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::BlueprintAuthorityOnly:
|
|
{
|
|
Flags |= CPF_BlueprintAuthorityOnly;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::AssetRegistrySearchable:
|
|
{
|
|
Flags |= CPF_AssetRegistrySearchable;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::SimpleDisplay:
|
|
{
|
|
Flags |= CPF_SimpleDisplay;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::AdvancedDisplay:
|
|
{
|
|
Flags |= CPF_AdvancedDisplay;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::SaveGame:
|
|
{
|
|
Flags |= CPF_SaveGame;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::SkipSerialization:
|
|
{
|
|
Flags |= CPF_SkipSerialization;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::Setter:
|
|
{
|
|
bHasSetterTag = true;
|
|
if (Specifier.Values.Num() == 1)
|
|
{
|
|
SetterName = Specifier.Values[0];
|
|
}
|
|
else if (Specifier.Values.Num() > 1)
|
|
{
|
|
Throwf(TEXT("The specifier '%s' must be given exactly one value"), *Specifier.Key);
|
|
}
|
|
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::Getter:
|
|
{
|
|
bHasGetterTag = true;
|
|
if (Specifier.Values.Num() == 1)
|
|
{
|
|
GetterName = Specifier.Values[0];
|
|
}
|
|
else if (Specifier.Values.Num() > 1)
|
|
{
|
|
Throwf(TEXT("The specifier '%s' must be given exactly one value"), *Specifier.Key);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::FieldNotify:
|
|
{
|
|
bFieldNotify = true;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
{
|
|
LogError(TEXT("Unknown variable specifier '%s'"), *Specifier.Key);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
switch (SpecID)
|
|
{
|
|
case EVariableSpecifier::Const:
|
|
{
|
|
Flags |= CPF_ConstParm;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::Ref:
|
|
{
|
|
Flags |= CPF_OutParm | CPF_ReferenceParm;
|
|
}
|
|
break;
|
|
|
|
case EVariableSpecifier::NotReplicated:
|
|
{
|
|
if (VariableCategory == EVariableCategory::ReplicatedParameter)
|
|
{
|
|
VariableCategory = EVariableCategory::RegularParameter;
|
|
Flags |= CPF_RepSkip;
|
|
}
|
|
else
|
|
{
|
|
LogError(TEXT("Only parameters in service request functions can be marked NotReplicated"));
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
{
|
|
LogError(TEXT("Unknown variable specifier '%s'"), *Specifier.Key);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we saw a BlueprintGetter but did not see BlueprintSetter or
|
|
// or BlueprintReadWrite then treat as BlueprintReadOnly
|
|
if (bSeenBlueprintGetterSpecifier && !bSeenBlueprintWriteSpecifier)
|
|
{
|
|
Flags |= CPF_BlueprintReadOnly;
|
|
ImpliedFlags &= ~CPF_BlueprintReadOnly;
|
|
}
|
|
|
|
{
|
|
const FString* ExposeOnSpawnStr = MetaDataFromNewStyle.Find(NAME_ExposeOnSpawn);
|
|
const bool bExposeOnSpawn = (NULL != ExposeOnSpawnStr);
|
|
if (bExposeOnSpawn)
|
|
{
|
|
if (0 != (CPF_DisableEditOnInstance & Flags))
|
|
{
|
|
LogWarning(TEXT("Property cannot have both 'DisableEditOnInstance' and 'ExposeOnSpawn' flags"));
|
|
}
|
|
if (0 == (CPF_BlueprintVisible & Flags))
|
|
{
|
|
LogWarning(TEXT("Property cannot have 'ExposeOnSpawn' without 'BlueprintVisible' flag."));
|
|
}
|
|
Flags |= CPF_ExposeOnSpawn;
|
|
}
|
|
}
|
|
|
|
if (CurrentAccessSpecifier == ACCESS_Public || VariableCategory != EVariableCategory::Member)
|
|
{
|
|
Flags &= ~CPF_Protected;
|
|
ExportFlags |= PROPEXPORT_Public;
|
|
ExportFlags &= ~(PROPEXPORT_Private|PROPEXPORT_Protected);
|
|
|
|
Flags &= ~CPF_NativeAccessSpecifiers;
|
|
Flags |= CPF_NativeAccessSpecifierPublic;
|
|
}
|
|
else if (CurrentAccessSpecifier == ACCESS_Protected)
|
|
{
|
|
Flags |= CPF_Protected;
|
|
ExportFlags |= PROPEXPORT_Protected;
|
|
ExportFlags &= ~(PROPEXPORT_Public|PROPEXPORT_Private);
|
|
|
|
Flags &= ~CPF_NativeAccessSpecifiers;
|
|
Flags |= CPF_NativeAccessSpecifierProtected;
|
|
}
|
|
else if (CurrentAccessSpecifier == ACCESS_Private)
|
|
{
|
|
Flags &= ~CPF_Protected;
|
|
ExportFlags |= PROPEXPORT_Private;
|
|
ExportFlags &= ~(PROPEXPORT_Public|PROPEXPORT_Protected);
|
|
|
|
Flags &= ~CPF_NativeAccessSpecifiers;
|
|
Flags |= CPF_NativeAccessSpecifierPrivate;
|
|
}
|
|
else
|
|
{
|
|
Throwf(TEXT("Unknown access level"));
|
|
}
|
|
|
|
// Swallow inline keywords
|
|
if (VariableCategory == EVariableCategory::Return)
|
|
{
|
|
FToken InlineToken;
|
|
if (!GetIdentifier(InlineToken, true))
|
|
{
|
|
Throwf(TEXT("%s: Missing variable type"), GetHintText(*this, VariableCategory));
|
|
}
|
|
|
|
if (!InlineToken.IsValue(TEXT("inline"), ESearchCase::CaseSensitive) &&
|
|
!InlineToken.IsValue(TEXT("FORCENOINLINE"), ESearchCase::CaseSensitive) &&
|
|
!InlineToken.ValueStartsWith(TEXT("FORCEINLINE"), ESearchCase::CaseSensitive))
|
|
{
|
|
UngetToken(InlineToken);
|
|
}
|
|
}
|
|
|
|
// Get variable type.
|
|
bool bUnconsumedStructKeyword = false;
|
|
bool bUnconsumedClassKeyword = false;
|
|
bool bUnconsumedEnumKeyword = false;
|
|
bool bUnconsumedConstKeyword = false;
|
|
|
|
// Handle MemoryLayout.h macros
|
|
ELayoutMacroType LayoutMacroType = ELayoutMacroType::None;
|
|
bool bHasWrapperBrackets = false;
|
|
ON_SCOPE_EXIT
|
|
{
|
|
if (OutLayoutMacroType)
|
|
{
|
|
*OutLayoutMacroType = LayoutMacroType;
|
|
if (bHasWrapperBrackets)
|
|
{
|
|
RequireSymbol(TEXT(')'), GLayoutMacroNames[(int32)LayoutMacroType]);
|
|
}
|
|
}
|
|
};
|
|
|
|
if (OutLayoutMacroType)
|
|
{
|
|
*OutLayoutMacroType = ELayoutMacroType::None;
|
|
|
|
FToken LayoutToken;
|
|
if (GetToken(LayoutToken))
|
|
{
|
|
if (LayoutToken.IsIdentifier())
|
|
{
|
|
FTokenValue Name = LayoutToken.GetTokenValue();
|
|
LayoutMacroType = (ELayoutMacroType)Algo::FindSortedStringCaseInsensitive(*Name, GLayoutMacroNames, UE_ARRAY_COUNT(GLayoutMacroNames));
|
|
if (LayoutMacroType != ELayoutMacroType::None)
|
|
{
|
|
RequireSymbol(TEXT('('), GLayoutMacroNames[(int32)LayoutMacroType]);
|
|
if (LayoutMacroType == ELayoutMacroType::ArrayEditorOnly || LayoutMacroType == ELayoutMacroType::FieldEditorOnly || LayoutMacroType == ELayoutMacroType::BitfieldEditorOnly)
|
|
{
|
|
Flags |= CPF_EditorOnly;
|
|
}
|
|
bHasWrapperBrackets = MatchSymbol(TEXT("("));
|
|
}
|
|
else
|
|
{
|
|
UngetToken(LayoutToken);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (MatchIdentifier(TEXT("mutable"), ESearchCase::CaseSensitive))
|
|
{
|
|
//@TODO: Should flag as settable from a const context, but this is at least good enough to allow use for C++ land
|
|
}
|
|
|
|
const int32 VarStartPos = InputPos;
|
|
|
|
if (MatchIdentifier(TEXT("const"), ESearchCase::CaseSensitive))
|
|
{
|
|
//@TODO: UCREMOVAL: Should use this to set the new (currently non-existent) CPF_Const flag appropriately!
|
|
bUnconsumedConstKeyword = true;
|
|
bNativeConst = true;
|
|
}
|
|
|
|
if (MatchIdentifier(TEXT("struct"), ESearchCase::CaseSensitive))
|
|
{
|
|
bUnconsumedStructKeyword = true;
|
|
}
|
|
else if (MatchIdentifier(TEXT("class"), ESearchCase::CaseSensitive))
|
|
{
|
|
bUnconsumedClassKeyword = true;
|
|
}
|
|
else if (MatchIdentifier(TEXT("enum"), ESearchCase::CaseSensitive))
|
|
{
|
|
if (VariableCategory == EVariableCategory::Member)
|
|
{
|
|
Throwf(TEXT("%s: Cannot declare enum at variable declaration"), GetHintText(*this, VariableCategory));
|
|
}
|
|
|
|
bUnconsumedEnumKeyword = true;
|
|
}
|
|
|
|
//
|
|
FToken VarType;
|
|
if ( !GetIdentifier(VarType, true) )
|
|
{
|
|
Throwf(TEXT("%s: Missing variable type"), GetHintText(*this, VariableCategory));
|
|
}
|
|
|
|
RedirectTypeIdentifier(VarType);
|
|
|
|
if ( VariableCategory == EVariableCategory::Return && VarType.IsValue(TEXT("void"), ESearchCase::CaseSensitive) )
|
|
{
|
|
VarProperty = FPropertyBase(CPT_None);
|
|
}
|
|
else if ( VarType.IsValue(TEXT("int8"), ESearchCase::CaseSensitive) )
|
|
{
|
|
VarProperty = FPropertyBase(CPT_Int8);
|
|
}
|
|
else if ( VarType.IsValue(TEXT("int16"), ESearchCase::CaseSensitive) )
|
|
{
|
|
VarProperty = FPropertyBase(CPT_Int16);
|
|
}
|
|
else if ( VarType.IsValue(TEXT("int32"), ESearchCase::CaseSensitive) )
|
|
{
|
|
VarProperty = FPropertyBase(CPT_Int);
|
|
}
|
|
else if ( VarType.IsValue(TEXT("int64"), ESearchCase::CaseSensitive) )
|
|
{
|
|
VarProperty = FPropertyBase(CPT_Int64);
|
|
}
|
|
else if ( VarType.IsValue(TEXT("uint64"), ESearchCase::CaseSensitive) && IsBitfieldProperty(LayoutMacroType) )
|
|
{
|
|
// 64-bit bitfield (bool) type, treat it like 8 bit type
|
|
VarProperty = FPropertyBase(CPT_Bool8);
|
|
}
|
|
else if ( VarType.IsValue(TEXT("uint32"), ESearchCase::CaseSensitive) && IsBitfieldProperty(LayoutMacroType) )
|
|
{
|
|
// 32-bit bitfield (bool) type, treat it like 8 bit type
|
|
VarProperty = FPropertyBase(CPT_Bool8);
|
|
}
|
|
else if ( VarType.IsValue(TEXT("uint16"), ESearchCase::CaseSensitive) && IsBitfieldProperty(LayoutMacroType) )
|
|
{
|
|
// 16-bit bitfield (bool) type, treat it like 8 bit type.
|
|
VarProperty = FPropertyBase(CPT_Bool8);
|
|
}
|
|
else if ( VarType.IsValue(TEXT("uint8"), ESearchCase::CaseSensitive) && IsBitfieldProperty(LayoutMacroType) )
|
|
{
|
|
// 8-bit bitfield (bool) type
|
|
VarProperty = FPropertyBase(CPT_Bool8);
|
|
}
|
|
else if ( VarType.IsValue(TEXT("int"), ESearchCase::CaseSensitive) )
|
|
{
|
|
VarProperty = FPropertyBase(CPT_Int, EIntType::Unsized);
|
|
}
|
|
else if ( VarType.IsValue(TEXT("signed"), ESearchCase::CaseSensitive) )
|
|
{
|
|
MatchIdentifier(TEXT("int"), ESearchCase::CaseSensitive);
|
|
VarProperty = FPropertyBase(CPT_Int, EIntType::Unsized);
|
|
}
|
|
else if (VarType.IsValue(TEXT("unsigned"), ESearchCase::CaseSensitive))
|
|
{
|
|
MatchIdentifier(TEXT("int"), ESearchCase::CaseSensitive);
|
|
VarProperty = FPropertyBase(CPT_UInt32, EIntType::Unsized);
|
|
}
|
|
else if ( VarType.IsValue(TEXT("bool"), ESearchCase::CaseSensitive) )
|
|
{
|
|
if (IsBitfieldProperty(LayoutMacroType))
|
|
{
|
|
LogError(TEXT("bool bitfields are not supported."));
|
|
}
|
|
// C++ bool type
|
|
VarProperty = FPropertyBase(CPT_Bool);
|
|
}
|
|
else if ( VarType.IsValue(TEXT("uint8"), ESearchCase::CaseSensitive) )
|
|
{
|
|
// Intrinsic Byte type.
|
|
VarProperty = FPropertyBase(CPT_Byte);
|
|
}
|
|
else if ( VarType.IsValue(TEXT("uint16"), ESearchCase::CaseSensitive) )
|
|
{
|
|
VarProperty = FPropertyBase(CPT_UInt16);
|
|
}
|
|
else if ( VarType.IsValue(TEXT("uint32"), ESearchCase::CaseSensitive) )
|
|
{
|
|
VarProperty = FPropertyBase(CPT_UInt32);
|
|
}
|
|
else if ( VarType.IsValue(TEXT("uint64"), ESearchCase::CaseSensitive) )
|
|
{
|
|
VarProperty = FPropertyBase(CPT_UInt64);
|
|
}
|
|
else if ( VarType.IsValue(TEXT("float"), ESearchCase::CaseSensitive) )
|
|
{
|
|
// Intrinsic single precision floating point type.
|
|
VarProperty = FPropertyBase(CPT_Float);
|
|
}
|
|
else if ( VarType.IsValue(TEXT("double"), ESearchCase::CaseSensitive) )
|
|
{
|
|
// Intrinsic double precision floating point type type.
|
|
VarProperty = FPropertyBase(CPT_Double);
|
|
}
|
|
else if (VarType.IsValue(TEXT("FLargeWorldCoordinatesReal"), ESearchCase::CaseSensitive)) // LWC_TODO: Remove FLargeWorldCoordinatesReal?
|
|
{
|
|
if (!SourceFile.IsNoExportTypes())
|
|
{
|
|
Throwf(TEXT("FLargeWorldCoordinatesReal is intended for LWC support only and should not be used outside of NoExportTypes.h"));
|
|
}
|
|
VarProperty = FPropertyBase(CPT_FLargeWorldCoordinatesReal);
|
|
}
|
|
else if ( VarType.IsValue(TEXT("FName"), ESearchCase::CaseSensitive) )
|
|
{
|
|
// Intrinsic Name type.
|
|
VarProperty = FPropertyBase(CPT_Name);
|
|
}
|
|
else if ( VarType.IsValue(TEXT("TArray"), ESearchCase::CaseSensitive) )
|
|
{
|
|
RequireSymbol( TEXT('<'), TEXT("'tarray'") );
|
|
|
|
GetVarType(Scope, Options, VarProperty, Disallow, EUHTPropertyType::DynamicArray, Flags, EPropertyDeclarationStyle::None, VariableCategory);
|
|
if (VarProperty.IsContainer())
|
|
{
|
|
Throwf(TEXT("Nested containers are not supported.") );
|
|
}
|
|
// TODO: Prevent sparse delegate types from being used in a container
|
|
|
|
if (VarProperty.MetaData.Find(NAME_NativeConst))
|
|
{
|
|
bNativeConstTemplateArg = true;
|
|
}
|
|
|
|
VarProperty.ArrayType = EArrayType::Dynamic;
|
|
|
|
FToken CloseTemplateToken;
|
|
if (!GetToken(CloseTemplateToken, /*bNoConsts=*/ true, ESymbolParseOption::CloseTemplateBracket))
|
|
{
|
|
Throwf(TEXT("Missing token while parsing TArray."));
|
|
}
|
|
|
|
if (!CloseTemplateToken.IsSymbol(TEXT('>')))
|
|
{
|
|
// If we didn't find a comma, report it
|
|
if (!CloseTemplateToken.IsSymbol(TEXT(',')))
|
|
{
|
|
Throwf(TEXT("Expected '>' but found '%s'"), *CloseTemplateToken.GetTokenValue());
|
|
}
|
|
|
|
// If we found a comma, read the next thing, assume it's an allocator, and report that
|
|
FToken AllocatorToken;
|
|
if (!GetToken(AllocatorToken, /*bNoConsts=*/ true, ESymbolParseOption::CloseTemplateBracket))
|
|
{
|
|
Throwf(TEXT("Unexpected end of file when parsing TArray allocator."));
|
|
}
|
|
|
|
if (!AllocatorToken.IsIdentifier())
|
|
{
|
|
Throwf(TEXT("Found '%s' - expected a '>' or ','."), *AllocatorToken.GetTokenValue());
|
|
}
|
|
|
|
if (AllocatorToken.IsValue(TEXT("FMemoryImageAllocator"), ESearchCase::CaseSensitive))
|
|
{
|
|
if (EnumHasAnyFlags(Flags, CPF_Net))
|
|
{
|
|
Throwf(TEXT("Replicated arrays with MemoryImageAllocators are not yet supported"));
|
|
}
|
|
|
|
RequireSymbol(TEXT('>'), TEXT("TArray template arguments"), ESymbolParseOption::CloseTemplateBracket);
|
|
|
|
VarProperty.AllocatorType = EAllocatorType::MemoryImage;
|
|
}
|
|
else if (AllocatorToken.IsValue(TEXT("TMemoryImageAllocator"), ESearchCase::CaseSensitive))
|
|
{
|
|
if (EnumHasAnyFlags(Flags, CPF_Net))
|
|
{
|
|
Throwf(TEXT("Replicated arrays with MemoryImageAllocators are not yet supported"));
|
|
}
|
|
|
|
RequireSymbol(TEXT('<'), TEXT("TMemoryImageAllocator template arguments"));
|
|
|
|
FToken SkipToken;
|
|
for (;;)
|
|
{
|
|
if (!GetToken(SkipToken, /*bNoConsts=*/ false, ESymbolParseOption::CloseTemplateBracket))
|
|
{
|
|
Throwf(TEXT("Unexpected end of file when parsing TMemoryImageAllocator template arguments."));
|
|
}
|
|
|
|
if (SkipToken.IsSymbol(TEXT('>')))
|
|
{
|
|
RequireSymbol(TEXT('>'), TEXT("TArray template arguments"), ESymbolParseOption::CloseTemplateBracket);
|
|
VarProperty.AllocatorType = EAllocatorType::MemoryImage;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Throwf(TEXT("Found '%s' - explicit allocators are not supported in TArray properties."), *AllocatorToken.GetTokenValue());
|
|
}
|
|
}
|
|
}
|
|
else if ( VarType.IsValue(TEXT("TMap"), ESearchCase::CaseSensitive) )
|
|
{
|
|
RequireSymbol( TEXT('<'), TEXT("'tmap'") );
|
|
|
|
FPropertyBase MapKeyType(CPT_None);
|
|
GetVarType(Scope, Options, MapKeyType, Disallow, EUHTPropertyType::Map, Flags, EPropertyDeclarationStyle::None, VariableCategory);
|
|
if (MapKeyType.IsContainer())
|
|
{
|
|
Throwf(TEXT("Nested containers are not supported.") );
|
|
}
|
|
// TODO: Prevent sparse delegate types from being used in a container
|
|
|
|
if (MapKeyType.Type == CPT_Interface)
|
|
{
|
|
Throwf(TEXT("UINTERFACEs are not currently supported as key types."));
|
|
}
|
|
|
|
if (MapKeyType.Type == CPT_Text)
|
|
{
|
|
Throwf(TEXT("FText is not currently supported as a key type."));
|
|
}
|
|
|
|
if (EnumHasAnyFlags(Flags, CPF_Net))
|
|
{
|
|
LogError(TEXT("Replicated maps are not supported."));
|
|
}
|
|
|
|
FToken CommaToken;
|
|
if (!GetToken(CommaToken, /*bNoConsts=*/ true) || !CommaToken.IsSymbol(TEXT(',')))
|
|
{
|
|
Throwf(TEXT("Missing value type while parsing TMap."));
|
|
}
|
|
|
|
GetVarType(Scope, Options, VarProperty, Disallow, EUHTPropertyType::Map, Flags, EPropertyDeclarationStyle::None, VariableCategory);
|
|
if (VarProperty.IsContainer())
|
|
{
|
|
Throwf(TEXT("Nested containers are not supported.") );
|
|
}
|
|
// TODO: Prevent sparse delegate types from being used in a container
|
|
MapKeyType.PropertyFlags = (MapKeyType.PropertyFlags & CPF_UObjectWrapper); // Make sure the 'UObjectWrapper' flag is maintained so that 'TMap<TSubclassOf<...>, ...>' works
|
|
VarProperty.MapKeyProp = MakeShared<FPropertyBase>(MoveTemp(MapKeyType));
|
|
VarProperty.MapKeyProp->DisallowFlags = ~(CPF_ContainsInstancedReference | CPF_InstancedReference | CPF_UObjectWrapper);
|
|
|
|
FToken CloseTemplateToken;
|
|
if (!GetToken(CloseTemplateToken, /*bNoConsts=*/ true, ESymbolParseOption::CloseTemplateBracket))
|
|
{
|
|
Throwf(TEXT("Missing token while parsing TMap."));
|
|
}
|
|
|
|
if (!CloseTemplateToken.IsSymbol(TEXT('>')))
|
|
{
|
|
// If we didn't find a comma, report it
|
|
if (!CloseTemplateToken.IsSymbol(TEXT(',')))
|
|
{
|
|
Throwf(TEXT("Expected '>' but found '%s'"), *CloseTemplateToken.GetTokenValue());
|
|
}
|
|
|
|
// If we found a comma, read the next thing, assume it's an allocator, and report that
|
|
FToken AllocatorToken;
|
|
if (!GetToken(AllocatorToken, /*bNoConsts=*/ true, ESymbolParseOption::CloseTemplateBracket))
|
|
{
|
|
Throwf(TEXT("Unexpected end of file when parsing TArray allocator."));
|
|
}
|
|
|
|
if (!AllocatorToken.IsIdentifier())
|
|
{
|
|
Throwf(TEXT("Found '%s' - expected a '>' or ','."), *AllocatorToken.GetTokenValue());
|
|
}
|
|
|
|
if (AllocatorToken.IsIdentifier(TEXT("FMemoryImageSetAllocator"), ESearchCase::CaseSensitive))
|
|
{
|
|
RequireSymbol(TEXT('>'), TEXT("TMap template arguments"), ESymbolParseOption::CloseTemplateBracket);
|
|
|
|
VarProperty.AllocatorType = EAllocatorType::MemoryImage;
|
|
}
|
|
else
|
|
{
|
|
Throwf(TEXT("Found '%s' - explicit allocators are not supported in TMap properties."), *AllocatorToken.GetTokenValue());
|
|
}
|
|
}
|
|
}
|
|
else if ( VarType.IsValue(TEXT("TSet"), ESearchCase::CaseSensitive) )
|
|
{
|
|
RequireSymbol( TEXT('<'), TEXT("'tset'") );
|
|
|
|
GetVarType(Scope, Options, VarProperty, Disallow, EUHTPropertyType::Set, Flags, EPropertyDeclarationStyle::None, VariableCategory);
|
|
if (VarProperty.IsContainer())
|
|
{
|
|
Throwf(TEXT("Nested containers are not supported.") );
|
|
}
|
|
// TODO: Prevent sparse delegate types from being used in a container
|
|
|
|
if (VarProperty.Type == CPT_Interface)
|
|
{
|
|
Throwf(TEXT("UINTERFACEs are not currently supported as element types."));
|
|
}
|
|
|
|
if (VarProperty.Type == CPT_Text)
|
|
{
|
|
Throwf(TEXT("FText is not currently supported as an element type."));
|
|
}
|
|
|
|
if (EnumHasAnyFlags(Flags, CPF_Net))
|
|
{
|
|
LogError(TEXT("Replicated sets are not supported."));
|
|
}
|
|
|
|
VarProperty.ArrayType = EArrayType::Set;
|
|
|
|
FToken CloseTemplateToken;
|
|
if (!GetToken(CloseTemplateToken, /*bNoConsts=*/ true, ESymbolParseOption::CloseTemplateBracket))
|
|
{
|
|
Throwf(TEXT("Missing token while parsing TArray."));
|
|
}
|
|
|
|
if (!CloseTemplateToken.IsSymbol(TEXT('>')))
|
|
{
|
|
// If we didn't find a comma, report it
|
|
if (!CloseTemplateToken.IsSymbol(TEXT(',')))
|
|
{
|
|
Throwf(TEXT("Expected '>' but found '%s'"), *CloseTemplateToken.GetTokenValue());
|
|
}
|
|
|
|
// If we found a comma, read the next thing, assume it's a keyfuncs, and report that
|
|
FToken AllocatorToken;
|
|
if (!GetToken(AllocatorToken, /*bNoConsts=*/ true, ESymbolParseOption::CloseTemplateBracket))
|
|
{
|
|
Throwf(TEXT("Expected '>' but found '%s'"), *CloseTemplateToken.GetTokenValue());
|
|
}
|
|
|
|
Throwf(TEXT("Found '%s' - explicit KeyFuncs are not supported in TSet properties."), *AllocatorToken.GetTokenValue());
|
|
}
|
|
}
|
|
else if ( VarType.IsValue(TEXT("FString"), ESearchCase::CaseSensitive) || VarType.IsValue(TEXT("FMemoryImageString"), ESearchCase::CaseSensitive))
|
|
{
|
|
VarProperty = FPropertyBase(CPT_String);
|
|
|
|
if (VariableCategory != EVariableCategory::Member)
|
|
{
|
|
if (MatchSymbol(TEXT('&')))
|
|
{
|
|
if (Flags & CPF_ConstParm)
|
|
{
|
|
// 'const FString& Foo' came from 'FString' in .uc, no flags
|
|
Flags &= ~CPF_ConstParm;
|
|
|
|
// We record here that we encountered a const reference, because we need to remove that information from flags for code generation purposes.
|
|
VarProperty.RefQualifier = ERefQualifier::ConstRef;
|
|
}
|
|
else
|
|
{
|
|
// 'FString& Foo' came from 'out FString' in .uc
|
|
Flags |= CPF_OutParm;
|
|
|
|
// And we record here that we encountered a non-const reference here too.
|
|
VarProperty.RefQualifier = ERefQualifier::NonConstRef;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if ( VarType.IsValue(TEXT("Text"), ESearchCase::IgnoreCase) )
|
|
{
|
|
Throwf(TEXT("%s' is missing a prefix, expecting 'FText'"), *VarType.GetTokenValue());
|
|
}
|
|
else if ( VarType.IsValue(TEXT("FText"), ESearchCase::CaseSensitive) )
|
|
{
|
|
VarProperty = FPropertyBase(CPT_Text);
|
|
}
|
|
else if (VarType.IsValue(TEXT("TEnumAsByte"), ESearchCase::CaseSensitive))
|
|
{
|
|
RequireSymbol(TEXT('<'), VarType.Value);
|
|
|
|
// Eat the forward declaration enum text if present
|
|
MatchIdentifier(TEXT("enum"), ESearchCase::CaseSensitive);
|
|
|
|
bool bFoundEnum = false;
|
|
|
|
FToken InnerEnumType;
|
|
if (GetIdentifier(InnerEnumType, true))
|
|
{
|
|
if (FUnrealEnumDefinitionInfo* EnumDef = GTypeDefinitionInfoMap.FindByName<FUnrealEnumDefinitionInfo>(*InnerEnumType.GetTokenValue()))
|
|
{
|
|
// In-scope enumeration.
|
|
VarProperty = FPropertyBase(*EnumDef, CPT_Byte);
|
|
bFoundEnum = true;
|
|
}
|
|
}
|
|
|
|
// Try to handle namespaced enums
|
|
// Note: We do not verify the scoped part is correct, and trust in the C++ compiler to catch that sort of mistake
|
|
if (MatchSymbol(TEXT("::")))
|
|
{
|
|
FToken ScopedTrueEnumName;
|
|
if (!GetIdentifier(ScopedTrueEnumName, true))
|
|
{
|
|
Throwf(TEXT("Expected a namespace scoped enum name.") );
|
|
}
|
|
}
|
|
|
|
if (!bFoundEnum)
|
|
{
|
|
Throwf(TEXT("Expected the name of a previously defined enum"));
|
|
}
|
|
|
|
RequireSymbol(TEXT('>'), VarType.Value, ESymbolParseOption::CloseTemplateBracket);
|
|
}
|
|
else if (VarType.IsValue(TEXT("TFieldPath"), ESearchCase::CaseSensitive ))
|
|
{
|
|
RequireSymbol( TEXT('<'), TEXT("'TFieldPath'") );
|
|
|
|
FName FieldClassName = NAME_None;
|
|
FToken PropertyTypeToken;
|
|
if (!GetToken(PropertyTypeToken, /* bNoConsts = */ true))
|
|
{
|
|
Throwf(TEXT("Expected the property type"));
|
|
}
|
|
else
|
|
{
|
|
FieldClassName = FName(PropertyTypeToken.Value.RightChop(1), FNAME_Add);
|
|
if (!FPropertyTraits::IsValidFieldClass(FieldClassName))
|
|
{
|
|
Throwf(TEXT("Undefined property type: %s"), *PropertyTypeToken.GetTokenValue());
|
|
}
|
|
}
|
|
|
|
RequireSymbol(TEXT('>'), VarType.Value, ESymbolParseOption::CloseTemplateBracket);
|
|
|
|
VarProperty = FPropertyBase(FieldClassName, CPT_FieldPath);
|
|
}
|
|
else if (FUnrealEnumDefinitionInfo* EnumDef = GTypeDefinitionInfoMap.FindByName<FUnrealEnumDefinitionInfo>(*VarType.GetTokenValue()))
|
|
{
|
|
EPropertyType UnderlyingType = CPT_Byte;
|
|
|
|
if (VariableCategory == EVariableCategory::Member)
|
|
{
|
|
if (EnumDef->GetCppForm() != UEnum::ECppForm::EnumClass)
|
|
{
|
|
Throwf(TEXT("You cannot use the raw enum name as a type for member variables, instead use TEnumAsByte or a C++11 enum class with an explicit underlying type."), *EnumDef->GetCppType());
|
|
}
|
|
}
|
|
|
|
// Try to handle namespaced enums
|
|
// Note: We do not verify the scoped part is correct, and trust in the C++ compiler to catch that sort of mistake
|
|
if (MatchSymbol(TEXT("::")))
|
|
{
|
|
FToken ScopedTrueEnumName;
|
|
if (!GetIdentifier(ScopedTrueEnumName, true))
|
|
{
|
|
Throwf(TEXT("Expected a namespace scoped enum name.") );
|
|
}
|
|
}
|
|
|
|
// In-scope enumeration.
|
|
VarProperty = FPropertyBase(*EnumDef, UnderlyingType);
|
|
bUnconsumedEnumKeyword = false;
|
|
}
|
|
else
|
|
{
|
|
// Check for structs/classes
|
|
bool bHandledType = false;
|
|
FString IdentifierStripped = GetClassNameWithPrefixRemoved(FString(VarType.Value));
|
|
bool bStripped = false;
|
|
FUnrealScriptStructDefinitionInfo* StructDef = GTypeDefinitionInfoMap.FindByName<FUnrealScriptStructDefinitionInfo>(*VarType.GetTokenValue());
|
|
if (!StructDef)
|
|
{
|
|
StructDef = GTypeDefinitionInfoMap.FindByName<FUnrealScriptStructDefinitionInfo>(*IdentifierStripped);
|
|
bStripped = true;
|
|
}
|
|
|
|
auto SetDelegateType = [&](FUnrealFunctionDefinitionInfo& InFunctionDef, const FString& InIdentifierStripped)
|
|
{
|
|
bHandledType = true;
|
|
|
|
VarProperty = FPropertyBase(InFunctionDef.HasAnyFunctionFlags(FUNC_MulticastDelegate) ? CPT_MulticastDelegate : CPT_Delegate);
|
|
VarProperty.DelegateName = *InIdentifierStripped;
|
|
VarProperty.FunctionDef = &InFunctionDef;
|
|
};
|
|
|
|
if (!StructDef && MatchSymbol(TEXT("::")))
|
|
{
|
|
FToken DelegateName;
|
|
if (GetIdentifier(DelegateName))
|
|
{
|
|
if (FUnrealClassDefinitionInfo* LocalOwnerClassDef = FUnrealClassDefinitionInfo::FindClass(*IdentifierStripped))
|
|
{
|
|
if (!BusyWait([LocalOwnerClassDef]()
|
|
{
|
|
return LocalOwnerClassDef->IsParsed() || LocalOwnerClassDef->GetUnrealSourceFile().IsParsed();
|
|
}))
|
|
{
|
|
Throwf(TEXT("Timeout waiting for '%s' to be parsed. Make sure that '%s' directly references the include file or that all intermediate include files contain some type of UCLASS, USTRUCT, or UENUM."),
|
|
*LocalOwnerClassDef->GetUnrealSourceFile().GetFilename(), *SourceFile.GetFilename());
|
|
}
|
|
const FString DelegateIdentifierStripped = GetClassNameWithPrefixRemoved(FString(DelegateName.Value));
|
|
if (FUnrealFieldDefinitionInfo* FoundDef = LocalOwnerClassDef->GetScope()->FindTypeByName(*(DelegateIdentifierStripped + HEADER_GENERATED_DELEGATE_SIGNATURE_SUFFIX)))
|
|
{
|
|
if (FUnrealFunctionDefinitionInfo* DelegateFuncDef = FoundDef->AsFunction())
|
|
{
|
|
SetDelegateType(*DelegateFuncDef, DelegateIdentifierStripped);
|
|
VarProperty.DelegateSignatureOwnerClassDef = LocalOwnerClassDef;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Throwf(TEXT("Cannot find class '%s', to resolve delegate '%s'"), *IdentifierStripped, *DelegateName.GetTokenValue());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bHandledType)
|
|
{
|
|
}
|
|
else if (StructDef)
|
|
{
|
|
if (bStripped)
|
|
{
|
|
const TCHAR* PrefixCPP = UHTConfig.StructsWithTPrefix.Contains(IdentifierStripped) ? TEXT("T") : StructDef->GetPrefixCPP();
|
|
FString ExpectedStructName = FString::Printf(TEXT("%s%s"), PrefixCPP, *StructDef->GetName());
|
|
if (!VarType.IsValue(*ExpectedStructName, ESearchCase::CaseSensitive))
|
|
{
|
|
Throwf(TEXT("Struct '%s' is missing or has an incorrect prefix, expecting '%s'"), *VarType.GetTokenValue(), *ExpectedStructName);
|
|
}
|
|
}
|
|
else if (!UHTConfig.StructsWithNoPrefix.Contains(VarType.Value))
|
|
{
|
|
const TCHAR* PrefixCPP = UHTConfig.StructsWithTPrefix.Contains(VarType.Value) ? TEXT("T") : StructDef->GetPrefixCPP();
|
|
Throwf(TEXT("Struct '%s' is missing a prefix, expecting '%s'"), *VarType.GetTokenValue(), *FString::Printf(TEXT("%s%s"), PrefixCPP, *StructDef->GetName()));
|
|
}
|
|
|
|
bHandledType = true;
|
|
|
|
VarProperty = FPropertyBase(*StructDef);
|
|
|
|
// Struct keyword in front of a struct is legal, we 'consume' it
|
|
bUnconsumedStructKeyword = false;
|
|
}
|
|
else if (GTypeDefinitionInfoMap.FindByName<FUnrealScriptStructDefinitionInfo>(*IdentifierStripped) != nullptr)
|
|
{
|
|
bHandledType = true;
|
|
|
|
// Struct keyword in front of a struct is legal, we 'consume' it
|
|
bUnconsumedStructKeyword = false;
|
|
}
|
|
else
|
|
{
|
|
FUnrealFunctionDefinitionInfo* DelegateFuncDef = UHTCast<FUnrealFunctionDefinitionInfo>(Scope->FindTypeByName(*(IdentifierStripped + HEADER_GENERATED_DELEGATE_SIGNATURE_SUFFIX)));
|
|
|
|
if (DelegateFuncDef)
|
|
{
|
|
SetDelegateType(*DelegateFuncDef, IdentifierStripped);
|
|
}
|
|
else
|
|
{
|
|
// An object reference of some type (maybe a restricted class?)
|
|
FUnrealClassDefinitionInfo* TempClassDef = nullptr;
|
|
|
|
EPropertyType PropertyType = CPT_ObjectReference;
|
|
const bool bIsSoftObjectPtrTemplate = VarType.IsValue(TEXT("TSoftObjectPtr"), ESearchCase::CaseSensitive);
|
|
|
|
bool bWeakIsAuto = false;
|
|
|
|
if (VarType.IsValue(TEXT("TSubclassOf"), ESearchCase::CaseSensitive))
|
|
{
|
|
TempClassDef = GUClassDef;
|
|
}
|
|
else if (VarType.IsValue(TEXT("FScriptInterface"), ESearchCase::CaseSensitive))
|
|
{
|
|
TempClassDef = GUInterfaceDef;
|
|
Flags |= CPF_UObjectWrapper;
|
|
}
|
|
else if (VarType.IsValue(TEXT("TSoftClassPtr"), ESearchCase::CaseSensitive))
|
|
{
|
|
TempClassDef = GUClassDef;
|
|
PropertyType = CPT_SoftObjectReference;
|
|
}
|
|
else
|
|
{
|
|
const bool bIsLazyPtrTemplate = VarType.IsValue(TEXT("TLazyObjectPtr"), ESearchCase::CaseSensitive);
|
|
const bool bIsObjectPtrTemplate = VarType.IsValue(TEXT("TObjectPtr"), ESearchCase::CaseSensitive);
|
|
const bool bIsWeakPtrTemplate = VarType.IsValue(TEXT("TWeakObjectPtr"), ESearchCase::CaseSensitive);
|
|
const bool bIsAutoweakPtrTemplate = VarType.IsValue(TEXT("TAutoWeakObjectPtr"), ESearchCase::CaseSensitive);
|
|
const bool bIsScriptInterfaceWrapper = VarType.IsValue(TEXT("TScriptInterface"), ESearchCase::CaseSensitive);
|
|
|
|
if (bIsLazyPtrTemplate || bIsObjectPtrTemplate || bIsWeakPtrTemplate || bIsAutoweakPtrTemplate || bIsScriptInterfaceWrapper || bIsSoftObjectPtrTemplate)
|
|
{
|
|
RequireSymbol(TEXT('<'), VarType.Value);
|
|
|
|
// Also consume const
|
|
bNativeConstTemplateArg |= MatchIdentifier(TEXT("const"), ESearchCase::CaseSensitive);
|
|
|
|
// Consume a forward class declaration 'class' if present
|
|
MatchIdentifier(TEXT("class"), ESearchCase::CaseSensitive);
|
|
|
|
// Find the lazy/weak class
|
|
FToken InnerClass;
|
|
if (GetIdentifier(InnerClass))
|
|
{
|
|
RedirectTypeIdentifier(InnerClass);
|
|
|
|
TempClassDef = FUnrealClassDefinitionInfo::FindScriptClass(FString(InnerClass.Value));
|
|
if (TempClassDef == nullptr)
|
|
{
|
|
FTokenValue InnerClassName = InnerClass.GetTokenValue();
|
|
Throwf(TEXT("Unrecognized type '%s' (in expression %s<%s>) - type must be a UCLASS"), *InnerClassName, *VarType.GetTokenValue(), *InnerClassName);
|
|
}
|
|
|
|
if (bIsAutoweakPtrTemplate)
|
|
{
|
|
PropertyType = CPT_WeakObjectReference;
|
|
bWeakIsAuto = true;
|
|
}
|
|
else if (bIsLazyPtrTemplate)
|
|
{
|
|
PropertyType = CPT_LazyObjectReference;
|
|
}
|
|
else if (bIsObjectPtrTemplate)
|
|
{
|
|
PropertyType = CPT_ObjectPtrReference;
|
|
}
|
|
else if (bIsWeakPtrTemplate)
|
|
{
|
|
PropertyType = CPT_WeakObjectReference;
|
|
}
|
|
else if (bIsSoftObjectPtrTemplate)
|
|
{
|
|
PropertyType = CPT_SoftObjectReference;
|
|
}
|
|
|
|
Flags |= CPF_UObjectWrapper;
|
|
}
|
|
else
|
|
{
|
|
Throwf(TEXT("%s: Missing template type"), *VarType.GetTokenValue());
|
|
}
|
|
|
|
// Const after template argument type but before end of template symbol
|
|
bNativeConstTemplateArg |= MatchIdentifier(TEXT("const"), ESearchCase::CaseSensitive);
|
|
|
|
RequireSymbol(TEXT('>'), VarType.Value, ESymbolParseOption::CloseTemplateBracket);
|
|
}
|
|
else
|
|
{
|
|
TempClassDef = FUnrealClassDefinitionInfo::FindScriptClass(*VarType.GetTokenValue());
|
|
}
|
|
}
|
|
|
|
if (TempClassDef != NULL)
|
|
{
|
|
bHandledType = true;
|
|
|
|
if ((PropertyType == CPT_WeakObjectReference) && (Disallow & CPF_AutoWeak)) // if it is not allowing anything, force it strong. this is probably a function arg
|
|
{
|
|
PropertyType = CPT_ObjectReference;
|
|
}
|
|
|
|
// if this is an interface class, we use the FInterfaceProperty class instead of FObjectProperty
|
|
if ((PropertyType == CPT_ObjectReference) && TempClassDef->HasAnyClassFlags(CLASS_Interface))
|
|
{
|
|
PropertyType = CPT_Interface;
|
|
}
|
|
|
|
VarProperty = FPropertyBase(*TempClassDef, PropertyType, bWeakIsAuto);
|
|
if (TempClassDef->IsChildOf(*GUClassDef))
|
|
{
|
|
if (MatchSymbol(TEXT('<')))
|
|
{
|
|
Flags |= CPF_UObjectWrapper;
|
|
|
|
// Consume a forward class declaration 'class' if present
|
|
MatchIdentifier(TEXT("class"), ESearchCase::CaseSensitive);
|
|
|
|
// Get the actual class type to restrict this to
|
|
FToken Limitor;
|
|
if (!GetIdentifier(Limitor))
|
|
{
|
|
Throwf(TEXT("'class': Missing class limitor"));
|
|
}
|
|
|
|
RedirectTypeIdentifier(Limitor);
|
|
|
|
VarProperty.MetaClassDef = FUnrealClassDefinitionInfo::FindScriptClassOrThrow(*this, FString(Limitor.Value));
|
|
|
|
RequireSymbol(TEXT('>'), TEXT("'class limitor'"), ESymbolParseOption::CloseTemplateBracket);
|
|
}
|
|
else
|
|
{
|
|
VarProperty.MetaClassDef = GUObjectDef;
|
|
}
|
|
|
|
if (PropertyType == CPT_WeakObjectReference)
|
|
{
|
|
Throwf(TEXT("Class variables cannot be weak, they are always strong."));
|
|
}
|
|
else if (PropertyType == CPT_LazyObjectReference)
|
|
{
|
|
Throwf(TEXT("Class variables cannot be lazy, they are always strong."));
|
|
}
|
|
|
|
if (bIsSoftObjectPtrTemplate)
|
|
{
|
|
Throwf(TEXT("Class variables cannot be stored in TSoftObjectPtr, use TSoftClassPtr instead."));
|
|
}
|
|
}
|
|
|
|
// Eat the star that indicates this is a pointer to the UObject
|
|
if (!(Flags & CPF_UObjectWrapper))
|
|
{
|
|
// Const after variable type but before pointer symbol
|
|
bNativeConst |= MatchIdentifier(TEXT("const"), ESearchCase::CaseSensitive);
|
|
|
|
RequireSymbol(TEXT('*'), TEXT("Expected a pointer type"));
|
|
|
|
// Optionally emit messages about native pointer members and swallow trailing 'const' after pointer properties
|
|
if (VariableCategory == EVariableCategory::Member)
|
|
{
|
|
// TODO: Remove exclusion for plugins under engine when all plugins have had their raw pointers converted.
|
|
ConditionalLogPointerUsage(bIsCurrentModulePartOfEngine && !PackageDef.GetModule().BaseDirectory.Contains(TEXT("/Plugins/")) ? UHTConfig.EngineNativePointerMemberBehavior : UHTConfig.NonEngineNativePointerMemberBehavior,
|
|
TEXT("Native pointer"), FString(InputPos - VarStartPos, Input + VarStartPos).TrimStartAndEnd().ReplaceCharWithEscapedChar(), TEXT("TObjectPtr"));
|
|
|
|
MatchIdentifier(TEXT("const"), ESearchCase::CaseSensitive);
|
|
}
|
|
|
|
VarProperty.PointerType = EPointerType::Native;
|
|
}
|
|
else if ((PropertyType == CPT_ObjectPtrReference) && (VariableCategory == EVariableCategory::Member))
|
|
{
|
|
// TODO: Remove exclusion for plugins under engine when all plugins have had their raw pointers converted.
|
|
ConditionalLogPointerUsage(bIsCurrentModulePartOfEngine && !PackageDef.GetModule().BaseDirectory.Contains(TEXT("/Plugins/")) ? UHTConfig.EngineObjectPtrMemberBehavior : UHTConfig.NonEngineObjectPtrMemberBehavior,
|
|
TEXT("ObjectPtr"), FString(InputPos - VarStartPos, Input + VarStartPos).TrimStartAndEnd().ReplaceCharWithEscapedChar(), nullptr);
|
|
}
|
|
|
|
// Imply const if it's a parameter that is a pointer to a const class
|
|
// NOTE: We shouldn't be automatically adding const param because in some cases with functions and blueprint native event, the
|
|
// generated code won't match. For now, just disabled the auto add in that case and check for the error in the validation code.
|
|
// Otherwise, the user is not warned and they will get compile errors.
|
|
if (VariableCategory != EVariableCategory::Member && (TempClassDef != NULL) && (TempClassDef->HasAnyClassFlags(CLASS_Const)))
|
|
{
|
|
if (!EnumHasAnyFlags(Options, EGetVarTypeOptions::NoAutoConst))
|
|
{
|
|
Flags |= CPF_ConstParm;
|
|
}
|
|
}
|
|
|
|
// Class keyword in front of a class is legal, we 'consume' it
|
|
bUnconsumedClassKeyword = false;
|
|
bUnconsumedConstKeyword = false;
|
|
}
|
|
}
|
|
|
|
// Resolve delegates declared in another class //@TODO: UCREMOVAL: This seems extreme
|
|
if (!bHandledType)
|
|
{
|
|
FString FullName = IdentifierStripped + HEADER_GENERATED_DELEGATE_SIGNATURE_SUFFIX;
|
|
bHandledType = BusyWait([&SetDelegateType, &IdentifierStripped, &FullName]()
|
|
{
|
|
FUnrealFunctionDefinitionInfo* DelegateDef = nullptr;
|
|
{
|
|
FScopeLock Lock(&GlobalDelegatesLock);
|
|
DelegateDef = GlobalDelegates.FindRef(FullName);
|
|
}
|
|
if (DelegateDef)
|
|
{
|
|
SetDelegateType(*DelegateDef, IdentifierStripped);
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
if (!bHandledType)
|
|
{
|
|
Throwf(TEXT("Unrecognized type '%s' - type must be a UCLASS, USTRUCT, UENUM, or global delegate."), *VarType.GetTokenValue());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (VariableCategory != EVariableCategory::Member)
|
|
{
|
|
// const after the variable type support (only for params)
|
|
if (MatchIdentifier(TEXT("const"), ESearchCase::CaseSensitive))
|
|
{
|
|
Flags |= CPF_ConstParm;
|
|
bNativeConst = true;
|
|
}
|
|
}
|
|
|
|
if (bUnconsumedConstKeyword)
|
|
{
|
|
if (VariableCategory == EVariableCategory::Member)
|
|
{
|
|
Throwf(TEXT("Const properties are not supported."));
|
|
}
|
|
else
|
|
{
|
|
Throwf(TEXT("Inappropriate keyword 'const' on variable of type '%s'"), *VarType.GetTokenValue());
|
|
}
|
|
}
|
|
|
|
if (bUnconsumedClassKeyword)
|
|
{
|
|
Throwf(TEXT("Inappropriate keyword 'class' on variable of type '%s'"), *VarType.GetTokenValue());
|
|
}
|
|
|
|
if (bUnconsumedStructKeyword)
|
|
{
|
|
Throwf(TEXT("Inappropriate keyword 'struct' on variable of type '%s'"), *VarType.GetTokenValue());
|
|
}
|
|
|
|
if (bUnconsumedEnumKeyword)
|
|
{
|
|
Throwf(TEXT("Inappropriate keyword 'enum' on variable of type '%s'"), *VarType.GetTokenValue());
|
|
}
|
|
|
|
if (MatchSymbol(TEXT('*')))
|
|
{
|
|
Throwf(TEXT("Inappropriate '*' on variable of type '%s', cannot have an exposed pointer to this type."), *VarType.GetTokenValue());
|
|
}
|
|
|
|
//@TODO: UCREMOVAL: 'const' member variables that will get written post-construction by defaultproperties
|
|
if (VariableCategory == EVariableCategory::Member && OwnerClassDef != nullptr && OwnerClassDef->HasAnyClassFlags(CLASS_Const))
|
|
{
|
|
// Eat a 'not quite truthful' const after the type; autogenerated for member variables of const classes.
|
|
bNativeConst |= MatchIdentifier(TEXT("const"), ESearchCase::CaseSensitive);
|
|
}
|
|
|
|
// Arrays are passed by reference but are only implicitly so; setting it explicitly could cause a problem with replicated functions
|
|
if (MatchSymbol(TEXT('&')))
|
|
{
|
|
switch (VariableCategory)
|
|
{
|
|
case EVariableCategory::RegularParameter:
|
|
case EVariableCategory::Return:
|
|
{
|
|
Flags |= CPF_OutParm;
|
|
|
|
//@TODO: UCREMOVAL: How to determine if we have a ref param?
|
|
if (Flags & CPF_ConstParm)
|
|
{
|
|
Flags |= CPF_ReferenceParm;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case EVariableCategory::ReplicatedParameter:
|
|
{
|
|
if (!(Flags & CPF_ConstParm))
|
|
{
|
|
Throwf(TEXT("Replicated %s parameters cannot be passed by non-const reference"), *VarType.GetTokenValue());
|
|
}
|
|
|
|
Flags |= CPF_ReferenceParm;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
{
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (Flags & CPF_ConstParm)
|
|
{
|
|
VarProperty.RefQualifier = ERefQualifier::ConstRef;
|
|
}
|
|
else
|
|
{
|
|
VarProperty.RefQualifier = ERefQualifier::NonConstRef;
|
|
}
|
|
}
|
|
|
|
// Set FPropertyBase info.
|
|
VarProperty.PropertyFlags |= Flags | ImpliedFlags;
|
|
VarProperty.ImpliedPropertyFlags |= ImpliedFlags;
|
|
VarProperty.PropertyExportFlags = ExportFlags;
|
|
VarProperty.DisallowFlags = Disallow;
|
|
|
|
// Set the RepNotify name, if the variable needs it
|
|
if( VarProperty.PropertyFlags & CPF_RepNotify )
|
|
{
|
|
if( RepCallbackName != NAME_None )
|
|
{
|
|
VarProperty.RepNotifyName = RepCallbackName;
|
|
}
|
|
else
|
|
{
|
|
Throwf(TEXT("Must specify a valid function name for replication notifications"));
|
|
}
|
|
}
|
|
|
|
VarProperty.bSetterTagFound = bHasSetterTag;
|
|
VarProperty.SetterName = SetterName;
|
|
VarProperty.bGetterTagFound = bHasGetterTag;
|
|
VarProperty.GetterName = GetterName;
|
|
|
|
VarProperty.bFieldNotify = bFieldNotify;
|
|
|
|
// Perform some more specific validation on the property flags
|
|
if (VarProperty.PropertyFlags & CPF_PersistentInstance)
|
|
{
|
|
if ((VarProperty.Type == CPT_ObjectReference) || (VarProperty.Type == CPT_ObjectPtrReference))
|
|
{
|
|
if (VarProperty.ClassDef->IsChildOf(*GUClassDef))
|
|
{
|
|
Throwf(TEXT("'Instanced' cannot be applied to class properties (UClass* or TSubclassOf<>)"));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Throwf(TEXT("'Instanced' is only allowed on an object property, an array of objects, a set of objects, or a map with an object value type."));
|
|
}
|
|
}
|
|
|
|
if ( VarProperty.IsObjectOrInterface() && VarProperty.Type != CPT_SoftObjectReference && VarProperty.MetaClassDef == nullptr && (VarProperty.PropertyFlags&CPF_Config) != 0 )
|
|
{
|
|
Throwf(TEXT("Not allowed to use 'config' with object variables"));
|
|
}
|
|
|
|
if ((VarProperty.PropertyFlags & CPF_BlueprintAssignable) && VarProperty.Type != CPT_MulticastDelegate)
|
|
{
|
|
Throwf(TEXT("'BlueprintAssignable' is only allowed on multicast delegate properties"));
|
|
}
|
|
|
|
if ((VarProperty.PropertyFlags & CPF_BlueprintCallable) && VarProperty.Type != CPT_MulticastDelegate)
|
|
{
|
|
Throwf(TEXT("'BlueprintCallable' is only allowed on a property when it is a multicast delegate"));
|
|
}
|
|
|
|
if ((VarProperty.PropertyFlags & CPF_BlueprintAuthorityOnly) && VarProperty.Type != CPT_MulticastDelegate)
|
|
{
|
|
Throwf(TEXT("'BlueprintAuthorityOnly' is only allowed on a property when it is a multicast delegate"));
|
|
}
|
|
|
|
if (VariableCategory != EVariableCategory::Member)
|
|
{
|
|
// These conditions are checked externally for struct/member variables where the flag can be inferred later on from the variable name itself
|
|
ValidatePropertyIsDeprecatedIfNecessary(EnumHasAnyFlags(Options, EGetVarTypeOptions::OuterTypeDeprecated), VariableCategory, VarProperty, OuterPropertyType, OuterPropertyFlags);
|
|
}
|
|
|
|
// Check for invalid transients
|
|
EPropertyFlags Transients = VarProperty.PropertyFlags & (CPF_DuplicateTransient | CPF_TextExportTransient | CPF_NonPIEDuplicateTransient);
|
|
if (Transients && OwnerClassDef == nullptr)
|
|
{
|
|
TArray<const TCHAR*> FlagStrs = ParsePropertyFlags(Transients);
|
|
Throwf(TEXT("'%s' specifier(s) are only allowed on class member variables"), *FString::Join(FlagStrs, TEXT(", ")));
|
|
}
|
|
|
|
// Make sure the overrides are allowed here.
|
|
if( VarProperty.PropertyFlags & Disallow )
|
|
{
|
|
Throwf(TEXT("Specified type modifiers not allowed here") );
|
|
}
|
|
|
|
// For now, copy the flags that a TMap value has to the key
|
|
if (FPropertyBase* KeyProp = VarProperty.MapKeyProp.Get())
|
|
{
|
|
// Make sure the 'UObjectWrapper' flag is maintained so that both 'TMap<TSubclassOf<...>, ...>' and 'TMap<UClass*, TSubclassOf<...>>' works correctly
|
|
KeyProp->PropertyFlags = (VarProperty.PropertyFlags & ~CPF_UObjectWrapper) | (KeyProp->PropertyFlags & CPF_UObjectWrapper);
|
|
}
|
|
|
|
VarProperty.MetaData = MetaDataFromNewStyle;
|
|
if (bNativeConst)
|
|
{
|
|
VarProperty.MetaData.Add(NAME_NativeConst, FString());
|
|
}
|
|
if (bNativeConstTemplateArg)
|
|
{
|
|
VarProperty.MetaData.Add(NAME_NativeConstTemplateArg, FString());
|
|
}
|
|
|
|
if (ParsedVarIndexRange)
|
|
{
|
|
ParsedVarIndexRange->Count = InputPos - ParsedVarIndexRange->StartIndex;
|
|
}
|
|
}
|
|
|
|
FUnrealPropertyDefinitionInfo& FHeaderParser::GetVarNameAndDim
|
|
(
|
|
FUnrealStructDefinitionInfo& ParentStruct,
|
|
FPropertyBase& VarProperty,
|
|
EVariableCategory VariableCategory,
|
|
ELayoutMacroType LayoutMacroType
|
|
)
|
|
{
|
|
const TCHAR* HintText = GetHintText(*this, VariableCategory);
|
|
|
|
AddModuleRelativePathToMetadata(ParentStruct, VarProperty.MetaData);
|
|
FStringView Identifier;
|
|
|
|
// Get variable name.
|
|
if (VariableCategory == EVariableCategory::Return)
|
|
{
|
|
// Hard-coded variable name, such as with return value.
|
|
Identifier = FStringView(TEXT("ReturnValue"));
|
|
}
|
|
else
|
|
{
|
|
FToken VarToken;
|
|
if (!GetIdentifier(VarToken))
|
|
{
|
|
Throwf(TEXT("Missing variable name") );
|
|
}
|
|
|
|
switch (LayoutMacroType)
|
|
{
|
|
case ELayoutMacroType::Array:
|
|
case ELayoutMacroType::ArrayEditorOnly:
|
|
case ELayoutMacroType::Bitfield:
|
|
case ELayoutMacroType::BitfieldEditorOnly:
|
|
case ELayoutMacroType::FieldInitialized:
|
|
RequireSymbol(TEXT(','), GLayoutMacroNames[(int32)LayoutMacroType]);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
Identifier = VarToken.Value;
|
|
}
|
|
|
|
// Check to see if the variable is deprecated, and if so set the flag
|
|
{
|
|
const int32 DeprecatedIndex = Identifier.Find(TEXT("_DEPRECATED"));
|
|
const int32 NativizedPropertyPostfixIndex = Identifier.Find(TEXT("__pf")); //TODO: check OverrideNativeName in Meta Data, to be sure it's not a random occurrence of the "__pf" string.
|
|
bool bIgnoreDeprecatedWord = (NativizedPropertyPostfixIndex != INDEX_NONE) && (NativizedPropertyPostfixIndex > DeprecatedIndex);
|
|
if ((DeprecatedIndex != INDEX_NONE) && !bIgnoreDeprecatedWord)
|
|
{
|
|
if (DeprecatedIndex != Identifier.Len() - 11)
|
|
{
|
|
Throwf(TEXT("Deprecated variables must end with _DEPRECATED"));
|
|
}
|
|
|
|
// We allow deprecated properties in blueprints that have getters and setters assigned as they may be part of a backwards compatibility path
|
|
const bool bBlueprintVisible = (VarProperty.PropertyFlags & CPF_BlueprintVisible) > 0;
|
|
const bool bWarnOnGetter = bBlueprintVisible && !VarProperty.MetaData.Contains(NAME_BlueprintGetter);
|
|
const bool bWarnOnSetter = bBlueprintVisible && !(VarProperty.PropertyFlags & CPF_BlueprintReadOnly) && !VarProperty.MetaData.Contains(NAME_BlueprintSetter);
|
|
|
|
if (bWarnOnGetter)
|
|
{
|
|
LogWarning(TEXT("%s: Deprecated property '%s' should not be marked as blueprint visible without having a BlueprintGetter"), HintText, *FString(Identifier));
|
|
}
|
|
|
|
if (bWarnOnSetter)
|
|
{
|
|
LogWarning(TEXT("%s: Deprecated property '%s' should not be marked as blueprint writeable without having a BlueprintSetter"), HintText, *FString(Identifier));
|
|
}
|
|
|
|
|
|
// Warn if a deprecated property is visible
|
|
if (VarProperty.PropertyFlags & (CPF_Edit | CPF_EditConst) || // Property is marked as editable
|
|
(!bBlueprintVisible && (VarProperty.PropertyFlags & CPF_BlueprintReadOnly) && !(VarProperty.ImpliedPropertyFlags & CPF_BlueprintReadOnly)) ) // Is BPRO, but not via Implied Flags and not caught by Getter/Setter path above
|
|
{
|
|
LogWarning(TEXT("%s: Deprecated property '%s' should not be marked as visible or editable"), HintText, *FString(Identifier));
|
|
}
|
|
|
|
VarProperty.PropertyFlags |= CPF_Deprecated;
|
|
Identifier.MidInline(0, DeprecatedIndex);
|
|
}
|
|
}
|
|
|
|
// Make sure it doesn't conflict.
|
|
FString VarName(Identifier);
|
|
int32 OuterContextCount = 0;
|
|
FUnrealFunctionDefinitionInfo* ExistingFunctionDef = FindFunction(ParentStruct, *VarName, true, nullptr);
|
|
FUnrealPropertyDefinitionInfo* ExistingPropertyDef = FindProperty(ParentStruct, *VarName, true);
|
|
|
|
if (ExistingFunctionDef != nullptr || ExistingPropertyDef != nullptr)
|
|
{
|
|
bool bErrorDueToShadowing = true;
|
|
|
|
if (ExistingFunctionDef && (VariableCategory != EVariableCategory::Member))
|
|
{
|
|
// A function parameter with the same name as a method is allowed
|
|
bErrorDueToShadowing = false;
|
|
}
|
|
|
|
//@TODO: This exception does not seem sound either, but there is enough existing code that it will need to be
|
|
// fixed up first before the exception it is removed.
|
|
if (ExistingPropertyDef)
|
|
{
|
|
const bool bExistingPropDeprecated = ExistingPropertyDef->HasAnyPropertyFlags(CPF_Deprecated);
|
|
const bool bNewPropDeprecated = (VariableCategory == EVariableCategory::Member) && ((VarProperty.PropertyFlags & CPF_Deprecated) != 0);
|
|
if (bNewPropDeprecated || bExistingPropDeprecated)
|
|
{
|
|
// if this is a property and one of them is deprecated, ignore it since it will be removed soon
|
|
bErrorDueToShadowing = false;
|
|
}
|
|
}
|
|
|
|
if (bErrorDueToShadowing)
|
|
{
|
|
Throwf(TEXT("%s: '%s' cannot be defined in '%s' as it is already defined in scope '%s' (shadowing is not allowed)"),
|
|
HintText,
|
|
*FString(Identifier),
|
|
*ParentStruct.GetName(),
|
|
ExistingFunctionDef ? *ExistingFunctionDef->GetOuter()->GetName() : *ExistingPropertyDef->GetFullName());
|
|
}
|
|
}
|
|
|
|
// Get optional dimension immediately after name.
|
|
FTokenString Dimensions;
|
|
if ((LayoutMacroType == ELayoutMacroType::None && MatchSymbol(TEXT('['))) || LayoutMacroType == ELayoutMacroType::Array || LayoutMacroType == ELayoutMacroType::ArrayEditorOnly)
|
|
{
|
|
switch (VariableCategory)
|
|
{
|
|
case EVariableCategory::Return:
|
|
{
|
|
Throwf(TEXT("Arrays aren't allowed as return types"));
|
|
}
|
|
|
|
case EVariableCategory::RegularParameter:
|
|
case EVariableCategory::ReplicatedParameter:
|
|
{
|
|
Throwf(TEXT("Arrays aren't allowed as function parameters"));
|
|
}
|
|
}
|
|
|
|
if (VarProperty.IsContainer())
|
|
{
|
|
Throwf(TEXT("Static arrays of containers are not allowed"));
|
|
}
|
|
|
|
if (VarProperty.IsBool())
|
|
{
|
|
Throwf(TEXT("Bool arrays are not allowed") );
|
|
}
|
|
|
|
if (LayoutMacroType == ELayoutMacroType::None)
|
|
{
|
|
// Ignore how the actual array dimensions are actually defined - we'll calculate those with the compiler anyway.
|
|
if (!GetRawString(Dimensions, TEXT(']')))
|
|
{
|
|
Throwf(TEXT("%s %s: Missing ']'"), HintText, *FString(Identifier));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Ignore how the actual array dimensions are actually defined - we'll calculate those with the compiler anyway.
|
|
if (!GetRawString(Dimensions, TEXT(')')))
|
|
{
|
|
Throwf(TEXT("%s %s: Missing ']'"), HintText, *FString(Identifier));
|
|
}
|
|
}
|
|
|
|
// Only static arrays are declared with []. Dynamic arrays use TArray<> instead.
|
|
VarProperty.ArrayType = EArrayType::Static;
|
|
|
|
if (LayoutMacroType == ELayoutMacroType::None)
|
|
{
|
|
MatchSymbol(TEXT(']'));
|
|
}
|
|
}
|
|
|
|
// Try gathering metadata for member fields
|
|
if (VariableCategory == EVariableCategory::Member)
|
|
{
|
|
ParseFieldMetaData(VarProperty.MetaData, *VarName);
|
|
AddFormattedPrevCommentAsTooltipMetaData(VarProperty.MetaData);
|
|
}
|
|
// validate UFunction parameters
|
|
else
|
|
{
|
|
// UFunctions with a smart pointer as input parameter wont compile anyway, because of missing P_GET_... macro.
|
|
// UFunctions with a smart pointer as return type will crash when called via blueprint, because they are not supported in VM.
|
|
// WeakPointer is supported by VM as return type (see UObject::execLetWeakObjPtr), but there is no P_GET_... macro for WeakPointer.
|
|
if (VarProperty.Type == CPT_LazyObjectReference)
|
|
{
|
|
Throwf(TEXT("UFunctions cannot take a lazy pointer as a parameter."));
|
|
}
|
|
else if (VarProperty.Type == CPT_ObjectPtrReference)
|
|
{
|
|
// @TODO: OBJPTR: Investigate TObjectPtr support for UFunction parameters.
|
|
Throwf(TEXT("UFunctions cannot take a TObjectPtr as a parameter."));
|
|
}
|
|
}
|
|
|
|
// Create the property
|
|
FName PropertyName(Identifier);
|
|
TSharedRef<FUnrealPropertyDefinitionInfo> PropDefRef = FPropertyTraits::CreateProperty(VarProperty, ParentStruct, PropertyName, VariableCategory, CurrentAccessSpecifier, Dimensions.String, SourceFile, InputLine, InputPos);
|
|
|
|
// Add the property to the parent
|
|
ParentStruct.AddProperty(PropDefRef);
|
|
return *PropDefRef;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------
|
|
Statement compiler.
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
//
|
|
// Compile a declaration in Token. Returns 1 if compiled, 0 if not.
|
|
//
|
|
bool FHeaderParser::CompileDeclaration(TArray<FUnrealFunctionDefinitionInfo*>& DelegatesToFixup, FToken& Token)
|
|
{
|
|
EAccessSpecifier AccessSpecifier = ParseAccessProtectionSpecifier(Token);
|
|
if (AccessSpecifier)
|
|
{
|
|
if (!IsAllowedInThisNesting(ENestAllowFlags::VarDecl) && !IsAllowedInThisNesting(ENestAllowFlags::Function))
|
|
{
|
|
Throwf(TEXT("Access specifier %s not allowed here."), *Token.GetTokenValue());
|
|
}
|
|
check(TopNest->NestType == ENestType::Class || TopNest->NestType == ENestType::Interface || TopNest->NestType == ENestType::NativeInterface);
|
|
CurrentAccessSpecifier = AccessSpecifier;
|
|
return true;
|
|
}
|
|
|
|
if (Token.IsIdentifier(TEXT("class"), ESearchCase::CaseSensitive) && (TopNest->NestType == ENestType::GlobalScope))
|
|
{
|
|
// Make sure the previous class ended with valid nesting.
|
|
if (bEncounteredNewStyleClass_UnmatchedBrackets)
|
|
{
|
|
Throwf(TEXT("Missing } at end of class"));
|
|
}
|
|
|
|
// Start parsing the second class
|
|
bEncounteredNewStyleClass_UnmatchedBrackets = true;
|
|
CurrentAccessSpecifier = ACCESS_Private;
|
|
|
|
if (!TryParseIInterfaceClass())
|
|
{
|
|
bEncounteredNewStyleClass_UnmatchedBrackets = false;
|
|
UngetToken(Token);
|
|
return SkipDeclaration(Token);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (Token.IsIdentifier(TEXT("GENERATED_IINTERFACE_BODY"), ESearchCase::CaseSensitive) || (Token.IsIdentifier(TEXT("GENERATED_BODY"), ESearchCase::CaseSensitive) && TopNest->NestType == ENestType::NativeInterface))
|
|
{
|
|
if (TopNest->NestType != ENestType::NativeInterface)
|
|
{
|
|
Throwf(TEXT("%s must occur inside the native interface definition"), *Token.GetTokenValue());
|
|
}
|
|
RequireSymbol(TEXT('('), Token.Value);
|
|
CompileVersionDeclaration(GetCurrentClassDef());
|
|
RequireSymbol(TEXT(')'), Token.Value);
|
|
|
|
FUnrealClassDefinitionInfo& ClassDef = GetCurrentClassDef();
|
|
if (ClassDef.GetParsedInterfaceState() == EParsedInterface::NotAnInterface)
|
|
{
|
|
FString CurrentClassName = ClassDef.GetName();
|
|
Throwf(TEXT("Could not find the associated 'U%s' class while parsing 'I%s' - it could be missing or malformed"), *CurrentClassName, *CurrentClassName);
|
|
}
|
|
|
|
if (ClassDef.GetParsedInterfaceState() == EParsedInterface::ParsedIInterface)
|
|
{
|
|
FString CurrentClassName = ClassDef.GetName();
|
|
Throwf(TEXT("Duplicate IInterface definition found while parsing 'I%s'"), *CurrentClassName);
|
|
}
|
|
|
|
check(ClassDef.GetParsedInterfaceState() == EParsedInterface::ParsedUInterface);
|
|
|
|
ClassDef.SetParsedInterfaceState(EParsedInterface::ParsedIInterface);
|
|
ClassDef.SetGeneratedBodyMacroAccessSpecifier(CurrentAccessSpecifier);
|
|
ClassDef.SetInterfaceGeneratedBodyLine(InputLine);
|
|
|
|
bClassHasGeneratedIInterfaceBody = true;
|
|
|
|
if (Token.IsIdentifier(TEXT("GENERATED_IINTERFACE_BODY"), ESearchCase::CaseSensitive))
|
|
{
|
|
CurrentAccessSpecifier = ACCESS_Public;
|
|
}
|
|
|
|
if (Token.IsIdentifier(TEXT("GENERATED_BODY"), ESearchCase::CaseSensitive))
|
|
{
|
|
GetCurrentClassDef().MarkGeneratedBody();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (Token.IsIdentifier(TEXT("GENERATED_UINTERFACE_BODY"), ESearchCase::CaseSensitive) || (Token.IsIdentifier(TEXT("GENERATED_BODY"), ESearchCase::CaseSensitive) && TopNest->NestType == ENestType::Interface))
|
|
{
|
|
if (TopNest->NestType != ENestType::Interface)
|
|
{
|
|
Throwf(TEXT("%s must occur inside the interface definition"), *Token.GetTokenValue());
|
|
}
|
|
RequireSymbol(TEXT('('), Token.Value);
|
|
CompileVersionDeclaration(GetCurrentClassDef());
|
|
RequireSymbol(TEXT(')'), Token.Value);
|
|
|
|
FUnrealClassDefinitionInfo& ClassDef = GetCurrentClassDef();
|
|
|
|
ClassDef.SetGeneratedBodyMacroAccessSpecifier(CurrentAccessSpecifier);
|
|
ClassDef.SetGeneratedBodyLine(InputLine);
|
|
|
|
bClassHasGeneratedUInterfaceBody = true;
|
|
|
|
if (Token.IsIdentifier(TEXT("GENERATED_UINTERFACE_BODY"), ESearchCase::CaseSensitive))
|
|
{
|
|
CurrentAccessSpecifier = ACCESS_Public;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (Token.IsIdentifier(TEXT("GENERATED_UCLASS_BODY"), ESearchCase::CaseSensitive) || (Token.IsIdentifier(TEXT("GENERATED_BODY"), ESearchCase::CaseSensitive) && TopNest->NestType == ENestType::Class))
|
|
{
|
|
if (TopNest->NestType != ENestType::Class)
|
|
{
|
|
Throwf(TEXT("%s must occur inside the class definition"), *Token.GetTokenValue());
|
|
}
|
|
|
|
FUnrealClassDefinitionInfo& ClassDef = GetCurrentClassDef();
|
|
|
|
if (Token.IsIdentifier(TEXT("GENERATED_BODY"), ESearchCase::CaseSensitive))
|
|
{
|
|
GetCurrentClassDef().MarkGeneratedBody();
|
|
|
|
ClassDef.SetGeneratedBodyMacroAccessSpecifier(CurrentAccessSpecifier);
|
|
}
|
|
else
|
|
{
|
|
CurrentAccessSpecifier = ACCESS_Public;
|
|
}
|
|
|
|
RequireSymbol(TEXT('('), Token.Value);
|
|
CompileVersionDeclaration(GetCurrentClassDef());
|
|
RequireSymbol(TEXT(')'), Token.Value);
|
|
|
|
ClassDef.SetGeneratedBodyLine(InputLine);
|
|
|
|
bClassHasGeneratedBody = true;
|
|
return true;
|
|
}
|
|
|
|
if (Token.IsIdentifier(TEXT("UCLASS"), ESearchCase::CaseSensitive))
|
|
{
|
|
bHaveSeenUClass = true;
|
|
bEncounteredNewStyleClass_UnmatchedBrackets = true;
|
|
CompileClassDeclaration();
|
|
return true;
|
|
}
|
|
|
|
if (Token.IsIdentifier(TEXT("UINTERFACE"), ESearchCase::CaseSensitive))
|
|
{
|
|
bHaveSeenUClass = true;
|
|
bEncounteredNewStyleClass_UnmatchedBrackets = true;
|
|
CompileInterfaceDeclaration();
|
|
return true;
|
|
}
|
|
|
|
if (Token.IsIdentifier(TEXT("UFUNCTION"), ESearchCase::CaseSensitive))
|
|
{
|
|
FUnrealFunctionDefinitionInfo& FunctionDef = CompileFunctionDeclaration();
|
|
return true;
|
|
}
|
|
|
|
if (Token.IsIdentifier(TEXT("UDELEGATE"), ESearchCase::CaseSensitive))
|
|
{
|
|
FUnrealFunctionDefinitionInfo& DelegateDef = CompileDelegateDeclaration(Token.Value, EDelegateSpecifierAction::Parse);
|
|
DelegatesToFixup.Add(&DelegateDef);
|
|
return true;
|
|
}
|
|
|
|
if (IsValidDelegateDeclaration(Token)) // Legacy delegate parsing - it didn't need a UDELEGATE
|
|
{
|
|
FUnrealFunctionDefinitionInfo& DelegateDef = CompileDelegateDeclaration(Token.Value);
|
|
DelegatesToFixup.Add(&DelegateDef);
|
|
return true;
|
|
}
|
|
|
|
if (Token.IsIdentifier(TEXT("UPROPERTY"), ESearchCase::CaseSensitive))
|
|
{
|
|
CheckAllow(TEXT("'Member variable declaration'"), ENestAllowFlags::VarDecl);
|
|
check(TopNest->NestType == ENestType::Class);
|
|
|
|
CompileVariableDeclaration(GetCurrentClassDef());
|
|
return true;
|
|
}
|
|
|
|
if (Token.IsIdentifier(TEXT("UENUM"), ESearchCase::CaseSensitive))
|
|
{
|
|
// Enumeration definition.
|
|
CompileEnum();
|
|
return true;
|
|
}
|
|
|
|
if (Token.IsIdentifier(TEXT("USTRUCT"), ESearchCase::CaseSensitive))
|
|
{
|
|
// Struct definition.
|
|
CompileStructDeclaration();
|
|
return true;
|
|
}
|
|
|
|
if (Token.IsSymbol(TEXT('#')))
|
|
{
|
|
// Compiler directive.
|
|
CompileDirective();
|
|
return true;
|
|
}
|
|
|
|
if (bEncounteredNewStyleClass_UnmatchedBrackets && Token.IsSymbol(TEXT('}')))
|
|
{
|
|
FUnrealClassDefinitionInfo& CurrentClassDef = GetCurrentClassDef();
|
|
CurrentClassDef.GetDefinitionRange().End = &Input[InputPos];
|
|
MatchSemi();
|
|
|
|
// Closing brace for class declaration
|
|
//@TODO: This is a very loose approximation of what we really need to do
|
|
// Instead, the whole statement-consumer loop should be in a nest
|
|
bEncounteredNewStyleClass_UnmatchedBrackets = false;
|
|
|
|
// Pop nesting here to allow other non UClass declarations in the header file.
|
|
// Make sure we treat UInterface as a class and not an interface
|
|
if (CurrentClassDef.HasAllClassFlags(CLASS_Interface) && &CurrentClassDef != GUInterfaceDef)
|
|
{
|
|
checkf(TopNest->NestType == ENestType::Interface || TopNest->NestType == ENestType::NativeInterface, TEXT("Unexpected end of interface block."));
|
|
PopNest(TopNest->NestType, TEXT("'Interface'"));
|
|
PostPopNestInterface(CurrentClassDef);
|
|
|
|
// Ensure the UINTERFACE classes have a GENERATED_BODY declaration
|
|
if (bHaveSeenUClass && !bClassHasGeneratedUInterfaceBody)
|
|
{
|
|
Throwf(TEXT("Expected a GENERATED_BODY() at the start of class"));
|
|
}
|
|
|
|
// Ensure the non-UINTERFACE interface classes have a GENERATED_BODY declaration
|
|
if (!bHaveSeenUClass && !bClassHasGeneratedIInterfaceBody)
|
|
{
|
|
Throwf(TEXT("Expected a GENERATED_BODY() at the start of class"));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (&CurrentClassDef == GUInterfaceDef && TopNest->NestType == ENestType::NativeInterface)
|
|
{
|
|
PopNest(TopNest->NestType, TEXT("'Interface'"));
|
|
}
|
|
else
|
|
{
|
|
PopNest(ENestType::Class, TEXT("'Class'"));
|
|
}
|
|
PostPopNestClass(CurrentClassDef);
|
|
|
|
// Ensure classes have a GENERATED_BODY declaration
|
|
if (bHaveSeenUClass && !bClassHasGeneratedBody)
|
|
{
|
|
Throwf(TEXT("Expected a GENERATED_BODY() at the start of class"));
|
|
}
|
|
}
|
|
|
|
bHaveSeenUClass = false;
|
|
bClassHasGeneratedBody = false;
|
|
bClassHasGeneratedUInterfaceBody = false;
|
|
bClassHasGeneratedIInterfaceBody = false;
|
|
|
|
GetCurrentScope()->AddType(CurrentClassDef);
|
|
return true;
|
|
}
|
|
|
|
if (Token.IsSymbol(TEXT(';')))
|
|
{
|
|
if (GetToken(Token))
|
|
{
|
|
Throwf(TEXT("Extra ';' before '%s'"), *Token.GetTokenValue());
|
|
}
|
|
else
|
|
{
|
|
Throwf(TEXT("Extra ';' before end of file"));
|
|
}
|
|
}
|
|
|
|
// Skip anything that looks like a macro followed by no bracket that we don't know about
|
|
if (ProbablyAnUnknownObjectLikeMacro(*this, Token))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
FRecordTokens RecordTokens(*this, bEncounteredNewStyleClass_UnmatchedBrackets && IsInAClass() ? &GetCurrentClassDef() : nullptr, &Token);
|
|
bool Result = SkipDeclaration(Token);
|
|
if (RecordTokens.Stop())
|
|
{
|
|
FUnrealStructDefinitionInfo& StructDef = GetCurrentClassDef();
|
|
const FDeclaration& Declaration = StructDef.GetDeclarations().Last();
|
|
if (CheckForConstructor(StructDef, Declaration))
|
|
{
|
|
}
|
|
else if (TopNest->NestType == ENestType::Class)
|
|
{
|
|
if (CheckForSerialize(StructDef, Declaration))
|
|
{
|
|
}
|
|
else if (CheckForPropertySetterFunction(StructDef, Declaration))
|
|
{
|
|
}
|
|
else if (CheckForPropertyGetterFunction(StructDef, Declaration))
|
|
{
|
|
}
|
|
}
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
bool FHeaderParser::CheckForConstructor(FUnrealStructDefinitionInfo& StructDef, const FDeclaration& Declaration)
|
|
{
|
|
FTokenReplay Tokens(Declaration.Tokens);
|
|
FUnrealClassDefinitionInfo& ClassDef = StructDef.AsClassChecked();
|
|
|
|
FToken Token;
|
|
if (!Tokens.GetToken(Token))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Allow explicit constructors
|
|
bool bFoundExplicit = Token.IsIdentifier(TEXT("explicit"), ESearchCase::CaseSensitive);
|
|
if (bFoundExplicit)
|
|
{
|
|
Tokens.GetToken(Token);
|
|
}
|
|
|
|
bool bSkippedAPIToken = false;
|
|
if (Token.Value.EndsWith(TEXT("_API"), ESearchCase::CaseSensitive))
|
|
{
|
|
if (!bFoundExplicit)
|
|
{
|
|
// Explicit can come before or after an _API
|
|
Tokens.MatchIdentifier(TEXT("explicit"), ESearchCase::CaseSensitive);
|
|
}
|
|
|
|
Tokens.GetToken(Token);
|
|
bSkippedAPIToken = true;
|
|
}
|
|
|
|
if (!Token.IsIdentifier(*ClassDef.GetAlternateNameCPP(), ESearchCase::IgnoreCase))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Tokens.GetToken(Token);
|
|
if (!Token.IsSymbol(TEXT('(')))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool bOICtor = false;
|
|
bool bVTCtor = false;
|
|
|
|
if (!ClassDef.IsDefaultConstructorDeclared() && Tokens.MatchSymbol(TEXT(')')))
|
|
{
|
|
ClassDef.MarkDefaultConstructorDeclared();
|
|
}
|
|
else if (!ClassDef.IsObjectInitializerConstructorDeclared()
|
|
|| !ClassDef.IsCustomVTableHelperConstructorDeclared())
|
|
{
|
|
bool bIsConst = false;
|
|
bool bIsRef = false;
|
|
int32 ParenthesesNestingLevel = 1;
|
|
|
|
while (ParenthesesNestingLevel && Tokens.GetToken(Token))
|
|
{
|
|
// Template instantiation or additional parameter excludes ObjectInitializer constructor.
|
|
if (Token.IsSymbol(TEXT(',')) || Token.IsSymbol(TEXT('<')))
|
|
{
|
|
bOICtor = false;
|
|
bVTCtor = false;
|
|
break;
|
|
}
|
|
|
|
if (Token.IsSymbol(TEXT('(')))
|
|
{
|
|
ParenthesesNestingLevel++;
|
|
continue;
|
|
}
|
|
|
|
if (Token.IsSymbol(TEXT(')')))
|
|
{
|
|
ParenthesesNestingLevel--;
|
|
continue;
|
|
}
|
|
|
|
if (Token.IsIdentifier(TEXT("const"), ESearchCase::CaseSensitive))
|
|
{
|
|
bIsConst = true;
|
|
continue;
|
|
}
|
|
|
|
if (Token.IsSymbol(TEXT('&')))
|
|
{
|
|
bIsRef = true;
|
|
continue;
|
|
}
|
|
|
|
if (Token.IsIdentifier(TEXT("FObjectInitializer"), ESearchCase::CaseSensitive)
|
|
|| Token.IsIdentifier(TEXT("FPostConstructInitializeProperties"), ESearchCase::CaseSensitive) // Deprecated, but left here, so it won't break legacy code.
|
|
)
|
|
{
|
|
bOICtor = true;
|
|
}
|
|
|
|
if (Token.IsIdentifier(TEXT("FVTableHelper"), ESearchCase::CaseSensitive))
|
|
{
|
|
bVTCtor = true;
|
|
}
|
|
}
|
|
|
|
// Parse until finish.
|
|
while (ParenthesesNestingLevel && Tokens.GetToken(Token))
|
|
{
|
|
if (Token.IsSymbol(TEXT('(')))
|
|
{
|
|
ParenthesesNestingLevel++;
|
|
continue;
|
|
}
|
|
|
|
if (Token.IsSymbol(TEXT(')')))
|
|
{
|
|
ParenthesesNestingLevel--;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (bOICtor && bIsRef && bIsConst)
|
|
{
|
|
ClassDef.MarkObjectInitializerConstructorDeclared();
|
|
}
|
|
if (bVTCtor && bIsRef)
|
|
{
|
|
ClassDef.MarkCustomVTableHelperConstructorDeclared();
|
|
}
|
|
}
|
|
|
|
if (!bVTCtor)
|
|
{
|
|
ClassDef.MarkConstructorDeclared();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool FHeaderParser::CheckForSerialize(FUnrealStructDefinitionInfo& StructDef, const FDeclaration& Declaration)
|
|
{
|
|
FTokenReplay Tokens(Declaration.Tokens);
|
|
FUnrealClassDefinitionInfo& ClassDef = StructDef.AsClassChecked();
|
|
|
|
FToken Token;
|
|
if (!Tokens.GetToken(Token))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
while (Token.IsIdentifier(TEXT("virtual"), ESearchCase::CaseSensitive) || Token.Value.EndsWith(TEXT("_API"), ESearchCase::CaseSensitive))
|
|
{
|
|
Tokens.GetToken(Token);
|
|
}
|
|
|
|
if (!Token.IsIdentifier(TEXT("void"), ESearchCase::CaseSensitive))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Tokens.GetToken(Token);
|
|
if (!Token.IsIdentifier(TEXT("Serialize"), ESearchCase::CaseSensitive))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Tokens.GetToken(Token);
|
|
if (!Token.IsSymbol(TEXT('(')))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Tokens.GetToken(Token);
|
|
|
|
ESerializerArchiveType ArchiveType = ESerializerArchiveType::None;
|
|
if (Token.IsIdentifier(TEXT("FArchive"), ESearchCase::CaseSensitive))
|
|
{
|
|
Tokens.GetToken(Token);
|
|
if (Token.IsSymbol(TEXT('&')))
|
|
{
|
|
Tokens.GetToken(Token);
|
|
|
|
// Allow the declaration to not define a name for the archive parameter
|
|
if (!Token.IsSymbol(TEXT(')')))
|
|
{
|
|
Tokens.GetToken(Token);
|
|
}
|
|
|
|
if (Token.IsSymbol(TEXT(')')))
|
|
{
|
|
ArchiveType = ESerializerArchiveType::Archive;
|
|
}
|
|
}
|
|
}
|
|
else if (Token.IsIdentifier(TEXT("FStructuredArchive"), ESearchCase::CaseSensitive))
|
|
{
|
|
Tokens.GetToken(Token);
|
|
if (Token.IsSymbol(TEXT("::"), ESearchCase::CaseSensitive))
|
|
{
|
|
Tokens.GetToken(Token);
|
|
|
|
if (Token.IsIdentifier(TEXT("FRecord"), ESearchCase::CaseSensitive))
|
|
{
|
|
Tokens.GetToken(Token);
|
|
|
|
// Allow the declaration to not define a name for the slot parameter
|
|
if (!Token.IsSymbol(TEXT(')')))
|
|
{
|
|
Tokens.GetToken(Token);
|
|
}
|
|
|
|
if (Token.IsSymbol(TEXT(')')))
|
|
{
|
|
ArchiveType = ESerializerArchiveType::StructuredArchiveRecord;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (Token.IsIdentifier(TEXT("FStructuredArchiveRecord"), ESearchCase::CaseSensitive))
|
|
{
|
|
Tokens.GetToken(Token);
|
|
|
|
// Allow the declaration to not define a name for the slot parameter
|
|
if (!Token.IsSymbol(TEXT(')')))
|
|
{
|
|
Tokens.GetToken(Token);
|
|
}
|
|
|
|
if (Token.IsSymbol(TEXT(')')))
|
|
{
|
|
ArchiveType = ESerializerArchiveType::StructuredArchiveRecord;
|
|
}
|
|
}
|
|
|
|
if (ArchiveType != ESerializerArchiveType::None)
|
|
{
|
|
// Found what we want!
|
|
if (Declaration.CurrentCompilerDirective == 0 || Declaration.CurrentCompilerDirective == ECompilerDirective::WithEditorOnlyData)
|
|
{
|
|
FString EnclosingDefine = Declaration.CurrentCompilerDirective != 0 ? TEXT("WITH_EDITORONLY_DATA") : TEXT("");
|
|
|
|
ClassDef.AddArchiveType(ArchiveType);
|
|
ClassDef.SetEnclosingDefine(MoveTemp(EnclosingDefine));
|
|
}
|
|
else
|
|
{
|
|
FUHTMessage(StructDef.GetUnrealSourceFile(), Token.InputLine).Throwf(TEXT("Serialize functions must not be inside preprocessor blocks, except for WITH_EDITORONLY_DATA"));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
static const TArray<FStringView> GSkipDeclarationWarningStrings =
|
|
{
|
|
TEXT("GENERATED_BODY"),
|
|
TEXT("GENERATED_IINTERFACE_BODY"),
|
|
TEXT("GENERATED_UCLASS_BODY"),
|
|
TEXT("GENERATED_UINTERFACE_BODY"),
|
|
TEXT("GENERATED_USTRUCT_BODY"),
|
|
// Leaving these disabled ATM since they can exist in the code without causing compile issues
|
|
//TEXT("RIGVM_METHOD"),
|
|
//TEXT("UCLASS"),
|
|
//TEXT("UDELEGATE"),
|
|
//TEXT("UENUM"),
|
|
//TEXT("UFUNCTION"),
|
|
//TEXT("UINTERFACE"),
|
|
//TEXT("UPROPERTY"),
|
|
//TEXT("USTRUCT"),
|
|
};
|
|
|
|
static bool ParseAccessorType(FString& OutAccessorType, FTokenReplay& Tokens)
|
|
{
|
|
FToken Token;
|
|
Tokens.GetToken(Token);
|
|
if (!Token.IsIdentifier())
|
|
{
|
|
return false;
|
|
}
|
|
if (Token.IsIdentifier(TEXT("const"), ESearchCase::CaseSensitive))
|
|
{
|
|
// skip const. We allow const in paramater and return values even if the property is not const
|
|
Tokens.GetToken(Token);
|
|
if (!Token.IsIdentifier())
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
OutAccessorType = Token.Value;
|
|
|
|
Tokens.GetToken(Token);
|
|
if (Token.IsSymbol(TEXT("<"), ESearchCase::CaseSensitive))
|
|
{
|
|
// template type - add everything until the matching '>' into the type string
|
|
OutAccessorType += Token.Value;
|
|
int32 TemplateNestCount = 1;
|
|
while (TemplateNestCount > 0)
|
|
{
|
|
Tokens.GetToken(Token);
|
|
if (Token.TokenType == ETokenType::None)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
OutAccessorType += Token.Value;
|
|
if (Token.IsSymbol(TEXT("<"), ESearchCase::CaseSensitive))
|
|
{
|
|
TemplateNestCount++;
|
|
}
|
|
else if (Token.IsSymbol(TEXT(">"), ESearchCase::CaseSensitive))
|
|
{
|
|
TemplateNestCount--;
|
|
}
|
|
}
|
|
Tokens.GetToken(Token);
|
|
}
|
|
// Skip '&' and add any '*'
|
|
for (; !Token.IsIdentifier(); Tokens.GetToken(Token))
|
|
{
|
|
if (Token.TokenType == ETokenType::None)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Support for passing values by ref for setters
|
|
if (!Token.IsSymbol(TEXT("&"), ESearchCase::CaseSensitive))
|
|
{
|
|
OutAccessorType += Token.Value;
|
|
}
|
|
}
|
|
// Return the last token since it's either ')' in case of setters or the getter name which will be parsed by the caller
|
|
Tokens.UngetToken(Token);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool FHeaderParser::CheckForPropertySetterFunction(FUnrealStructDefinitionInfo& StructDef, const FDeclaration& Declaration)
|
|
{
|
|
FTokenReplay Tokens(Declaration.Tokens);
|
|
FUnrealClassDefinitionInfo& ClassDef = StructDef.AsClassChecked();
|
|
|
|
FToken Token;
|
|
if (!Tokens.GetToken(Token))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Skip virtual keyword or any API macros
|
|
while (Token.IsIdentifier(TEXT("virtual"), ESearchCase::CaseSensitive) || Token.Value.EndsWith(TEXT("_API"), ESearchCase::CaseSensitive))
|
|
{
|
|
Tokens.GetToken(Token);
|
|
}
|
|
|
|
// Setters can only return void
|
|
if (!Token.IsIdentifier(TEXT("void"), ESearchCase::CaseSensitive))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FToken SetterNameToken;
|
|
Tokens.GetToken(SetterNameToken);
|
|
if (!SetterNameToken.IsIdentifier())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (SetterNameToken.Value.Compare(TEXT("SetHardwareCursors"), ESearchCase::CaseSensitive) == 0)
|
|
{
|
|
static volatile int32 xx = 0;
|
|
xx++;
|
|
}
|
|
|
|
FUnrealPropertyDefinitionInfo* PropertyWithSetter = nullptr;
|
|
FString SetterFunctionName;
|
|
TArray<TSharedRef<FUnrealPropertyDefinitionInfo>>& Properties = ClassDef.GetProperties();
|
|
bool bExplicitSetter = false;
|
|
|
|
// First check if there are any properties that already specify this function name as a setter
|
|
for (TSharedRef<FUnrealPropertyDefinitionInfo>& Prop : Properties)
|
|
{
|
|
FPropertyBase& BaseProp = Prop->GetPropertyBase();
|
|
if (!BaseProp.bSetterFunctionFound && !BaseProp.SetterName.IsEmpty() && BaseProp.SetterName != TEXT("None"))
|
|
{
|
|
if (SetterNameToken.Value.Compare(Prop->GetPropertyBase().SetterName, ESearchCase::CaseSensitive) == 0)
|
|
{
|
|
SetterFunctionName = Prop->GetPropertyBase().SetterName;
|
|
PropertyWithSetter = &Prop.Get();
|
|
bExplicitSetter = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If not, try with autogenerated setter names
|
|
if (!PropertyWithSetter && SetterNameToken.ValueStartsWith(TEXT("Set"), ESearchCase::CaseSensitive))
|
|
{
|
|
for (TSharedRef<FUnrealPropertyDefinitionInfo>& Prop : Properties)
|
|
{
|
|
FPropertyBase& BaseProp = Prop->GetPropertyBase();
|
|
if (!BaseProp.bSetterFunctionFound && BaseProp.SetterName.IsEmpty() &&
|
|
(BaseProp.bSetterTagFound || FUHTConfig::Get().bAllowAutomaticSettersAndGetters))
|
|
{
|
|
SetterFunctionName.Reset();
|
|
SetterFunctionName += TEXT("Set");
|
|
SetterFunctionName += Prop->GetName();
|
|
|
|
if (SetterNameToken.Value.Compare(SetterFunctionName, ESearchCase::CaseSensitive) == 0)
|
|
{
|
|
// We found a matching function name so we can move onto verifying if the declaration matches
|
|
bExplicitSetter = BaseProp.bSetterTagFound;
|
|
PropertyWithSetter = &Prop.Get();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!PropertyWithSetter)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Tokens.GetToken(Token);
|
|
if (!Token.IsSymbol(TEXT('(')))
|
|
{
|
|
if (bExplicitSetter)
|
|
{
|
|
LogError(TEXT("Expected '(' when parsing property %s setter function %s"), *PropertyWithSetter->GetName(), *SetterFunctionName);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
FString ParameterType;
|
|
if (!ParseAccessorType(ParameterType, Tokens))
|
|
{
|
|
if (bExplicitSetter)
|
|
{
|
|
LogError(TEXT("Error when parsing parameter type for property %s setter function %s"), *PropertyWithSetter->GetName(), *SetterFunctionName);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Skip parameter name
|
|
Tokens.GetToken(Token);
|
|
if (!Token.IsIdentifier())
|
|
{
|
|
if (bExplicitSetter)
|
|
{
|
|
LogError(TEXT("Expected parameter name when parsing property %s setter function %s"), *PropertyWithSetter->GetName(), *SetterFunctionName);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Setter has only one parameter
|
|
Tokens.GetToken(Token);
|
|
if (!Token.IsSymbol(TEXT(')')))
|
|
{
|
|
if (bExplicitSetter)
|
|
{
|
|
LogError(TEXT("Expected ')' when parsing property %s setter function %s"), *PropertyWithSetter->GetName(), *SetterFunctionName);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Parameter type and property type must match
|
|
FString ExtendedPropertyType;
|
|
FString PropertyType = PropertyWithSetter->GetCPPType(&ExtendedPropertyType, CPPF_Implementation | CPPF_ArgumentOrReturnValue | CPPF_NoRef);
|
|
ExtendedPropertyType.RemoveSpacesInline();
|
|
PropertyType += ExtendedPropertyType;
|
|
if (ParameterType.Compare(PropertyType, ESearchCase::CaseSensitive) != 0)
|
|
{
|
|
if (bExplicitSetter)
|
|
{
|
|
LogError(TEXT("Paramater type %s unsupported for property %s setter function %s (expected: %s)"),
|
|
*ParameterType, *PropertyWithSetter->GetName(), *SetterFunctionName, *PropertyType);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if ((GetCurrentCompilerDirective() & ECompilerDirective::WithEditor) != 0)
|
|
{
|
|
if (bExplicitSetter)
|
|
{
|
|
LogError(TEXT("Property %s setter function %s cannot be declared within WITH_EDITOR block. Use WITH_EDITORONLY_DATA instead."),
|
|
*PropertyWithSetter->GetName(), *SetterFunctionName);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if ((GetCurrentCompilerDirective() & ECompilerDirective::WithEditorOnlyData) != 0 && (PropertyWithSetter->GetPropertyFlags() & CPF_EditorOnly) == 0)
|
|
{
|
|
if (bExplicitSetter)
|
|
{
|
|
LogError(TEXT("Property %s is not editor-only but its setter function %s is."),
|
|
*PropertyWithSetter->GetName(), *SetterFunctionName);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
PropertyWithSetter->GetPropertyBase().SetterName = *SetterFunctionName;
|
|
PropertyWithSetter->GetPropertyBase().bSetterFunctionFound = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool FHeaderParser::CheckForPropertyGetterFunction(FUnrealStructDefinitionInfo& StructDef, const FDeclaration& Declaration)
|
|
{
|
|
FTokenReplay Tokens(Declaration.Tokens);
|
|
FUnrealClassDefinitionInfo& ClassDef = StructDef.AsClassChecked();
|
|
|
|
FToken Token;
|
|
if (!Tokens.GetToken(Token))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Skip virtual keyword and any API macros
|
|
while (Token.IsIdentifier(TEXT("virtual"), ESearchCase::CaseSensitive) || Token.Value.EndsWith(TEXT("_API"), ESearchCase::CaseSensitive))
|
|
{
|
|
Tokens.GetToken(Token);
|
|
}
|
|
|
|
Tokens.UngetToken(Token);
|
|
FString GetterType;
|
|
if (!ParseAccessorType(GetterType, Tokens))
|
|
{
|
|
// Unable to print a meaningful error before parsing the Getter name
|
|
return false;
|
|
}
|
|
|
|
FToken GetterNameToken;
|
|
Tokens.GetToken(GetterNameToken);
|
|
if (!GetterNameToken.IsIdentifier())
|
|
{
|
|
// Unable to print a meaningful error before parsing the Getter name
|
|
return false;
|
|
}
|
|
|
|
FUnrealPropertyDefinitionInfo* PropertyWithGetter = nullptr;
|
|
TArray<TSharedRef<FUnrealPropertyDefinitionInfo>>& Properties = ClassDef.GetProperties();
|
|
FString GetterFunctionName;
|
|
bool bExplicitGetter = false;
|
|
|
|
// First check if there are any properties that already specify this function name as a getter
|
|
for (TSharedRef<FUnrealPropertyDefinitionInfo>& Prop : Properties)
|
|
{
|
|
FPropertyBase& BaseProp = Prop->GetPropertyBase();
|
|
if (!BaseProp.bGetterFunctionFound && !BaseProp.GetterName.IsEmpty() && BaseProp.GetterName != TEXT("None"))
|
|
{
|
|
if (GetterNameToken.Value.Compare(Prop->GetPropertyBase().GetterName, ESearchCase::CaseSensitive) == 0)
|
|
{
|
|
GetterFunctionName = Prop->GetPropertyBase().GetterName;
|
|
PropertyWithGetter = &Prop.Get();
|
|
bExplicitGetter = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If not, try with autogenerated getter names
|
|
if (!PropertyWithGetter && GetterNameToken.ValueStartsWith(TEXT("Get"), ESearchCase::CaseSensitive))
|
|
{
|
|
for (TSharedRef<FUnrealPropertyDefinitionInfo>& Prop : Properties)
|
|
{
|
|
FPropertyBase& BaseProp = Prop->GetPropertyBase();
|
|
if (!BaseProp.bGetterFunctionFound && BaseProp.GetterName != TEXT("None") &&
|
|
(BaseProp.bGetterTagFound || FUHTConfig::Get().bAllowAutomaticSettersAndGetters))
|
|
{
|
|
GetterFunctionName.Reset();
|
|
GetterFunctionName += TEXT("Get");
|
|
GetterFunctionName += Prop->GetName();
|
|
if (GetterNameToken.Value.Compare(GetterFunctionName, ESearchCase::CaseSensitive) == 0)
|
|
{
|
|
bExplicitGetter = BaseProp.bGetterTagFound;
|
|
PropertyWithGetter = &Prop.Get();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!PropertyWithGetter)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FString ExtendedPropertyType;
|
|
FString PropertyType = PropertyWithGetter->GetCPPType(&ExtendedPropertyType, CPPF_Implementation | CPPF_ArgumentOrReturnValue);
|
|
PropertyType += ExtendedPropertyType;
|
|
if (GetterType.Compare(PropertyType, ESearchCase::CaseSensitive) != 0)
|
|
{
|
|
if (bExplicitGetter)
|
|
{
|
|
LogError(TEXT("Paramater type %s unsupported for property %s getter function %s (expected: %s)"),
|
|
*GetterType, *PropertyWithGetter->GetName(), *GetterFunctionName, *PropertyType);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Getter is a function that takes no arguments
|
|
Tokens.GetToken(Token);
|
|
if (!Token.IsSymbol(TEXT('(')))
|
|
{
|
|
if (bExplicitGetter)
|
|
{
|
|
LogError(TEXT("Expected '(' when parsing property %s getter function %s"), *PropertyWithGetter->GetName(), *GetterFunctionName);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Tokens.GetToken(Token);
|
|
if (!Token.IsSymbol(TEXT(')')))
|
|
{
|
|
if (bExplicitGetter)
|
|
{
|
|
LogError(TEXT("Expected ')' when parsing property %s getter function %s"), *PropertyWithGetter->GetName(), *GetterFunctionName);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Getters should be const functions
|
|
Tokens.GetToken(Token);
|
|
if (!Token.IsIdentifier(TEXT("const"), ESearchCase::CaseSensitive))
|
|
{
|
|
if (bExplicitGetter)
|
|
{
|
|
LogError(TEXT("Property %s getter function %s must be const"), *PropertyWithGetter->GetName(), *GetterFunctionName);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if ((GetCurrentCompilerDirective() & ECompilerDirective::WithEditor) != 0)
|
|
{
|
|
if (bExplicitGetter)
|
|
{
|
|
LogError(TEXT("Property %s setter function %s cannot be declared within WITH_EDITOR block. Use WITH_EDITORONLY_DATA instead."),
|
|
*PropertyWithGetter->GetName(), *GetterFunctionName);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if ((GetCurrentCompilerDirective() & ECompilerDirective::WithEditorOnlyData) != 0 && (PropertyWithGetter->GetPropertyFlags() & CPF_EditorOnly) == 0)
|
|
{
|
|
if (bExplicitGetter)
|
|
{
|
|
LogError(TEXT("Property %s is not editor-only but its getter function %s is."),
|
|
*PropertyWithGetter->GetName(), *GetterFunctionName);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
PropertyWithGetter->GetPropertyBase().GetterName = GetterFunctionName;
|
|
PropertyWithGetter->GetPropertyBase().bGetterFunctionFound = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool FHeaderParser::SkipDeclaration(FToken& Token)
|
|
{
|
|
// Store the current value of PrevComment so it can be restored after we parsed everything.
|
|
FString OldPrevComment(PrevComment);
|
|
// Consume all tokens until the end of declaration/definition has been found.
|
|
int32 NestedScopes = 0;
|
|
// Check if this is a class/struct declaration in which case it can be followed by member variable declaration.
|
|
bool bPossiblyClassDeclaration = Token.IsIdentifier(TEXT("class"), ESearchCase::CaseSensitive) || Token.IsIdentifier(TEXT("struct"), ESearchCase::CaseSensitive);
|
|
// (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 bMacroDeclaration = ProbablyAMacro(Token.Value) && !Token.IsIdentifier(TEXT("DECLARE_FUNCTION"), ESearchCase::CaseSensitive);
|
|
bool bEndOfDeclarationFound = false;
|
|
bool bDefinitionFound = false;
|
|
TCHAR OpeningBracket = bMacroDeclaration ? TEXT('(') : TEXT('{');
|
|
TCHAR ClosingBracket = bMacroDeclaration ? TEXT(')') : TEXT('}');
|
|
bool bRetestCurrentToken = false;
|
|
while (bRetestCurrentToken || GetToken(Token))
|
|
{
|
|
// 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 (bPossiblyClassDeclaration && NestedScopes == 0 && Token.IsSymbol(TEXT('(')))
|
|
{
|
|
bPossiblyClassDeclaration = false;
|
|
}
|
|
|
|
bRetestCurrentToken = false;
|
|
if (Token.IsSymbol(TEXT(';')) && NestedScopes == 0)
|
|
{
|
|
bEndOfDeclarationFound = true;
|
|
break;
|
|
}
|
|
|
|
if (Token.IsIdentifier())
|
|
{
|
|
// Use a trivial prefilter to avoid doing the search on things that aren't UE keywords we care about
|
|
if (Token.Value[0] == 'G' || Token.Value[0] == 'R' || Token.Value[0] == 'U')
|
|
{
|
|
if (Algo::BinarySearch(GSkipDeclarationWarningStrings, Token.Value,
|
|
[](const FStringView& Lhs, const FStringView& Rhs) { return Lhs.Compare(Rhs, ESearchCase::CaseSensitive) < 0; }) >= 0)
|
|
{
|
|
LogWarning(FString::Printf(TEXT("The identifier \'%s\' was detected in a block being skipped. Was this intentional?"), *FString(Token.Value)));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!bMacroDeclaration && Token.IsIdentifier(TEXT("PURE_VIRTUAL"), ESearchCase::CaseSensitive) && NestedScopes == 0)
|
|
{
|
|
OpeningBracket = TEXT('(');
|
|
ClosingBracket = TEXT(')');
|
|
}
|
|
|
|
if (Token.IsSymbol(OpeningBracket))
|
|
{
|
|
// This is a function definition or class declaration.
|
|
bDefinitionFound = true;
|
|
NestedScopes++;
|
|
}
|
|
else if (Token.IsSymbol(ClosingBracket))
|
|
{
|
|
NestedScopes--;
|
|
if (NestedScopes == 0)
|
|
{
|
|
// Could be a class declaration in all capitals, and not a macro
|
|
bool bReallyEndDeclaration = true;
|
|
if (bMacroDeclaration)
|
|
{
|
|
FToken PossibleBracketToken;
|
|
GetToken(PossibleBracketToken);
|
|
UngetToken(Token);
|
|
GetToken(Token);
|
|
|
|
// If Strcmp returns 0, it is probably a class, else a macro.
|
|
bReallyEndDeclaration = !PossibleBracketToken.IsSymbol(TEXT('{'));
|
|
}
|
|
|
|
if (bReallyEndDeclaration)
|
|
{
|
|
bEndOfDeclarationFound = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (NestedScopes < 0)
|
|
{
|
|
Throwf(TEXT("Unexpected '}'. Did you miss a semi-colon?"));
|
|
}
|
|
}
|
|
else if (bMacroDeclaration && NestedScopes == 0)
|
|
{
|
|
bMacroDeclaration = false;
|
|
OpeningBracket = TEXT('{');
|
|
ClosingBracket = TEXT('}');
|
|
bRetestCurrentToken = true;
|
|
}
|
|
}
|
|
if (bEndOfDeclarationFound)
|
|
{
|
|
// Member variable declaration after class declaration (see bPossiblyClassDeclaration).
|
|
if (bPossiblyClassDeclaration && bDefinitionFound)
|
|
{
|
|
// Should syntax errors be also handled when someone declares a variable after function definition?
|
|
// Consume the variable name.
|
|
FToken VariableName;
|
|
if( !GetToken(VariableName, true) )
|
|
{
|
|
return false;
|
|
}
|
|
if (!VariableName.IsIdentifier())
|
|
{
|
|
// Not a variable name.
|
|
UngetToken(VariableName);
|
|
}
|
|
else if (!SafeMatchSymbol(TEXT(';')))
|
|
{
|
|
Throwf(TEXT("Unexpected '%s'. Did you miss a semi-colon?"), *VariableName.GetTokenValue());
|
|
}
|
|
}
|
|
|
|
// C++ allows any number of ';' after member declaration/definition.
|
|
while (SafeMatchSymbol(TEXT(';')));
|
|
}
|
|
|
|
PrevComment = OldPrevComment;
|
|
// clear the current value for comment
|
|
//ClearComment();
|
|
|
|
// Successfully consumed C++ declaration unless mismatched pair of brackets has been found.
|
|
return NestedScopes == 0 && bEndOfDeclarationFound;
|
|
}
|
|
|
|
bool FHeaderParser::SafeMatchSymbol( const TCHAR Match )
|
|
{
|
|
// Remember the position before the next token (this can include comments before the next symbol).
|
|
FScriptLocation LocationBeforeNextSymbol;
|
|
InitScriptLocation(LocationBeforeNextSymbol);
|
|
|
|
FToken Token;
|
|
if (GetToken(Token, /*bNoConsts=*/ true))
|
|
{
|
|
if (Token.IsSymbol(Match))
|
|
{
|
|
return true;
|
|
}
|
|
UngetToken(Token);
|
|
}
|
|
|
|
// Return to the stored position.
|
|
ReturnToLocation(LocationBeforeNextSymbol);
|
|
return false;
|
|
}
|
|
|
|
FUnrealClassDefinitionInfo& FHeaderParser::ParseClassNameDeclaration(FString& DeclaredClassName, FString& RequiredAPIMacroIfPresent)
|
|
{
|
|
ParseNameWithPotentialAPIMacroPrefix(/*out*/ DeclaredClassName, /*out*/ RequiredAPIMacroIfPresent, TEXT("class"));
|
|
|
|
FUnrealClassDefinitionInfo* ClassDef = FUnrealClassDefinitionInfo::FindClass(*GetClassNameWithPrefixRemoved(*DeclaredClassName));
|
|
check(ClassDef);
|
|
ClassDef->SetClassCastFlags(ClassCastFlagMap::Get().GetCastFlag(DeclaredClassName));
|
|
ClassDef->SetClassCastFlags(ClassCastFlagMap::Get().GetCastFlag(FString(TEXT("F")) + DeclaredClassName.RightChop(1))); // For properties, check alternate name
|
|
|
|
// Skip optional final keyword
|
|
MatchIdentifier(TEXT("final"), ESearchCase::CaseSensitive);
|
|
|
|
// Parse the inheritance list
|
|
ParseInheritance(TEXT("class"), [](const TCHAR* ClassName, bool bIsSuperClass) {}); // Eat the results, already been parsed
|
|
|
|
// Collect data about the super and base classes
|
|
if (FUnrealClassDefinitionInfo* SuperClassDef = UHTCast<FUnrealClassDefinitionInfo>(ClassDef->GetSuperStructInfo().Struct))
|
|
{
|
|
ClassDef->SetClassCastFlags(SuperClassDef->GetClassCastFlags());
|
|
}
|
|
|
|
// Add the class flags from the interfaces
|
|
for (FUnrealStructDefinitionInfo::FBaseStructInfo& BaseClassInfo : ClassDef->GetBaseStructInfos())
|
|
{
|
|
if (FUnrealClassDefinitionInfo* BaseClassDef = UHTCast<FUnrealClassDefinitionInfo>(BaseClassInfo.Struct))
|
|
{
|
|
if (!BaseClassDef->HasAnyClassFlags(CLASS_Interface))
|
|
{
|
|
Throwf(TEXT("Implements: Class %s is not an interface; Can only inherit from non-UObjects or UInterface derived interfaces"), *BaseClassDef->GetName());
|
|
}
|
|
|
|
// Propagate the inheritable ClassFlags
|
|
ClassDef->SetClassFlags(BaseClassDef->GetClassFlags() & ClassDef->GetInheritClassFlags());
|
|
}
|
|
}
|
|
return *ClassDef;
|
|
}
|
|
|
|
/**
|
|
* Setups basic class settings after parsing.
|
|
*/
|
|
void PostParsingClassSetup(FUnrealClassDefinitionInfo& ClassDef)
|
|
{
|
|
//@TODO: Move to post parse finalization?
|
|
|
|
// Since this flag is computed in this method, we have to re-propagate the flag from the super
|
|
// just in case they were defined in this source file.
|
|
if (FUnrealClassDefinitionInfo* SuperClassDef = ClassDef.GetSuperClass())
|
|
{
|
|
ClassDef.SetClassFlags(SuperClassDef->GetClassFlags() & CLASS_Config);
|
|
}
|
|
|
|
// Set the class config flag if any properties have config
|
|
for (TSharedRef<FUnrealPropertyDefinitionInfo> PropertyDef : ClassDef.GetProperties())
|
|
{
|
|
if (PropertyDef->HasAnyPropertyFlags(CPF_Config))
|
|
{
|
|
ClassDef.SetClassFlags(CLASS_Config);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Class needs to specify which ini file is going to be used if it contains config variables.
|
|
if (ClassDef.HasAnyClassFlags(CLASS_Config) && ClassDef.GetClassConfigName() == NAME_None)
|
|
{
|
|
// Inherit config setting from base class.
|
|
ClassDef.SetClassConfigName(ClassDef.GetSuperClass() ? ClassDef.GetSuperClass()->GetClassConfigName() : NAME_None);
|
|
if (ClassDef.GetClassConfigName() == NAME_None)
|
|
{
|
|
ClassDef.Throwf(TEXT("Classes with config / globalconfig member variables need to specify config file."));
|
|
ClassDef.SetClassConfigName(NAME_Engine);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compiles a class declaration.
|
|
*/
|
|
FUnrealClassDefinitionInfo& FHeaderParser::CompileClassDeclaration()
|
|
{
|
|
// Start of a class block.
|
|
CheckAllow(TEXT("'class'"), ENestAllowFlags::Class);
|
|
|
|
// New-style UCLASS() syntax
|
|
TMap<FName, FString> MetaData;
|
|
|
|
TArray<FPropertySpecifier> SpecifiersFound;
|
|
ReadSpecifierSetInsideMacro(SpecifiersFound, TEXT("Class"), MetaData);
|
|
|
|
const int32 PrologFinishLine = InputLine;
|
|
|
|
// Members of classes have a default private access level in c++
|
|
// Setting this directly should be ok as we don't support nested classes, so the outer scope access should not need restoring
|
|
CurrentAccessSpecifier = ACCESS_Private;
|
|
|
|
AddFormattedPrevCommentAsTooltipMetaData(MetaData);
|
|
|
|
// New style files have the class name / extends afterwards
|
|
RequireIdentifier(TEXT("class"), ESearchCase::CaseSensitive, TEXT("Class declaration"));
|
|
|
|
SkipAlignasAndDeprecatedMacroIfNecessary(*this);
|
|
|
|
FString DeclaredClassName;
|
|
FString RequiredAPIMacroIfPresent;
|
|
|
|
FUnrealClassDefinitionInfo& ClassDef = ParseClassNameDeclaration(/*out*/ DeclaredClassName, /*out*/ RequiredAPIMacroIfPresent);
|
|
|
|
ClassDef.GetDefinitionRange().Start = &Input[InputPos];
|
|
|
|
// If we have existing class flags (preexisting engine objects), make sure we have some shared flags.
|
|
check(ClassDef.GetClassFlags() == 0 || (ClassDef.GetClassFlags() & ClassDef.GetParsedClassFlags()) != 0);
|
|
|
|
ClassDef.MarkParsed();
|
|
|
|
PushNest(ENestType::Class, &ClassDef);
|
|
|
|
ResetClassData();
|
|
|
|
// Make sure our parent classes is parsed.
|
|
for (FUnrealClassDefinitionInfo* TempDef = ClassDef.GetSuperClass(); TempDef; TempDef = TempDef->GetSuperClass())
|
|
{
|
|
bool bIsParsed = TempDef->IsParsed();
|
|
bool bIsIntrinsic = TempDef->HasAnyClassFlags(CLASS_Intrinsic);
|
|
if (!(bIsParsed || bIsIntrinsic))
|
|
{
|
|
Throwf(TEXT("'%s' can't be compiled: Parent class '%s' has errors"), *ClassDef.GetName(), *TempDef->GetName());
|
|
}
|
|
}
|
|
|
|
// Merge with categories inherited from the parent.
|
|
ClassDef.MergeClassCategories();
|
|
|
|
// Class attributes.
|
|
ClassDef.SetPrologLine(PrologFinishLine);
|
|
ClassDef.MergeAndValidateClassFlags(DeclaredClassName);
|
|
ClassDef.SetInternalFlags(EInternalObjectFlags::Native);
|
|
|
|
// Class metadata
|
|
MetaData.Append(ClassDef.GetParsedMetaData());
|
|
ClassDef.MergeCategoryMetaData(MetaData);
|
|
AddIncludePathToMetadata(ClassDef, MetaData);
|
|
AddModuleRelativePathToMetadata(ClassDef, MetaData);
|
|
|
|
// Register the metadata
|
|
FUHTMetaData::RemapAndAddMetaData(ClassDef, MoveTemp(MetaData));
|
|
|
|
// Handle the start of the rest of the class
|
|
RequireSymbol( TEXT('{'), TEXT("'Class'") );
|
|
|
|
// Copy properties from parent class.
|
|
if (FUnrealClassDefinitionInfo* SuperClassDef = ClassDef.GetSuperClass())
|
|
{
|
|
ClassDef.SetPropertiesSize(SuperClassDef->GetPropertiesSize());
|
|
}
|
|
|
|
// Validate sparse class data
|
|
CheckSparseClassData(ClassDef);
|
|
|
|
return ClassDef;
|
|
}
|
|
|
|
FUnrealClassDefinitionInfo* FHeaderParser::ParseInterfaceNameDeclaration(FString& DeclaredInterfaceName, FString& RequiredAPIMacroIfPresent)
|
|
{
|
|
ParseNameWithPotentialAPIMacroPrefix(/*out*/ DeclaredInterfaceName, /*out*/ RequiredAPIMacroIfPresent, TEXT("interface"));
|
|
|
|
FUnrealClassDefinitionInfo* ClassDef = FUnrealClassDefinitionInfo::FindClass(*GetClassNameWithPrefixRemoved(*DeclaredInterfaceName));
|
|
if (ClassDef == nullptr)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
// Get super interface
|
|
bool bSpecifiesParentClass = MatchSymbol(TEXT(':'));
|
|
if (!bSpecifiesParentClass)
|
|
{
|
|
return ClassDef;
|
|
}
|
|
|
|
RequireIdentifier(TEXT("public"), ESearchCase::CaseSensitive, TEXT("class inheritance"));
|
|
|
|
// verify if our super class is an interface class
|
|
// the super class should have been marked as CLASS_Interface at the importing stage, if it were an interface
|
|
FUnrealClassDefinitionInfo* TempClassDef = GetQualifiedClass(TEXT("'extends'"));
|
|
check(TempClassDef);
|
|
if (!TempClassDef->HasAnyClassFlags(CLASS_Interface))
|
|
{
|
|
// UInterface is special and actually extends from UObject, which isn't an interface
|
|
if (DeclaredInterfaceName != TEXT("UInterface"))
|
|
{
|
|
Throwf(TEXT("Interface class '%s' cannot inherit from non-interface class '%s'"), *DeclaredInterfaceName, *TempClassDef->GetName());
|
|
}
|
|
}
|
|
|
|
// The class super should have already been set by now
|
|
FUnrealClassDefinitionInfo* SuperClassDef = ClassDef->GetSuperClass();
|
|
check(SuperClassDef);
|
|
if (SuperClassDef != TempClassDef)
|
|
{
|
|
Throwf(TEXT("%s's superclass must be %s, not %s"), *ClassDef->GetPathName(), *SuperClassDef->GetPathName(), *TempClassDef->GetPathName());
|
|
}
|
|
|
|
return ClassDef;
|
|
}
|
|
|
|
bool FHeaderParser::TryParseIInterfaceClass()
|
|
{
|
|
// 'class' was already matched by the caller
|
|
|
|
// Get a class name
|
|
FString DeclaredInterfaceName;
|
|
FString RequiredAPIMacroIfPresent;
|
|
if (ParseInterfaceNameDeclaration(/*out*/ DeclaredInterfaceName, /*out*/ RequiredAPIMacroIfPresent) == nullptr)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (MatchSymbol(TEXT(';')))
|
|
{
|
|
// Forward declaration.
|
|
return false;
|
|
}
|
|
|
|
if (DeclaredInterfaceName[0] != 'I')
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FUnrealClassDefinitionInfo* FoundClassDef = nullptr;
|
|
if ((FoundClassDef = FUnrealClassDefinitionInfo::FindClass(*DeclaredInterfaceName.Mid(1))) == nullptr)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Continue parsing the second class as if it were a part of the first (for reflection data purposes, it is)
|
|
RequireSymbol(TEXT('{'), TEXT("C++ interface mix-in class declaration"));
|
|
|
|
// Push the interface class nesting again.
|
|
PushNest(ENestType::NativeInterface, FoundClassDef);
|
|
|
|
CurrentAccessSpecifier = ACCESS_Private;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* compiles Java or C# style interface declaration
|
|
*/
|
|
void FHeaderParser::CompileInterfaceDeclaration()
|
|
{
|
|
// Start of an interface block. Since Interfaces and Classes are always at the same nesting level,
|
|
// whereever a class declaration is allowed, an interface declaration is also allowed.
|
|
CheckAllow( TEXT("'interface'"), ENestAllowFlags::Class );
|
|
|
|
CurrentAccessSpecifier = ACCESS_Private;
|
|
|
|
FString DeclaredInterfaceName;
|
|
FString RequiredAPIMacroIfPresent;
|
|
TMap<FName, FString> MetaData;
|
|
|
|
// Build up a list of interface specifiers
|
|
TArray<FPropertySpecifier> SpecifiersFound;
|
|
|
|
// New-style UINTERFACE() syntax
|
|
ReadSpecifierSetInsideMacro(SpecifiersFound, TEXT("Interface"), MetaData);
|
|
|
|
int32 PrologFinishLine = InputLine;
|
|
|
|
// New style files have the interface name / extends afterwards
|
|
RequireIdentifier(TEXT("class"), ESearchCase::CaseSensitive, TEXT("Interface declaration"));
|
|
FUnrealClassDefinitionInfo* InterfaceClassDef = ParseInterfaceNameDeclaration(/*out*/ DeclaredInterfaceName, /*out*/ RequiredAPIMacroIfPresent);
|
|
check(InterfaceClassDef);
|
|
InterfaceClassDef->GetDefinitionRange().Start = &Input[InputPos];
|
|
|
|
// Record that this interface is RequiredAPI if the CORE_API style macro was present
|
|
if (!RequiredAPIMacroIfPresent.IsEmpty())
|
|
{
|
|
InterfaceClassDef->SetClassFlags(CLASS_RequiredAPI);
|
|
}
|
|
|
|
// Set the appropriate interface class flags
|
|
InterfaceClassDef->SetClassFlags(CLASS_Interface | CLASS_Abstract);
|
|
if (FUnrealClassDefinitionInfo* InterfaceSuperClassDef = InterfaceClassDef->GetSuperClass())
|
|
{
|
|
InterfaceClassDef->SetClassCastFlags(InterfaceSuperClassDef->GetClassCastFlags());
|
|
// All classes that are parsed are expected to be native
|
|
if (!InterfaceSuperClassDef->HasAnyClassFlags(CLASS_Native))
|
|
{
|
|
Throwf(TEXT("Native classes cannot extend non-native classes"));
|
|
}
|
|
InterfaceClassDef->SetClassWithin(InterfaceSuperClassDef->GetClassWithin());
|
|
}
|
|
else
|
|
{
|
|
InterfaceClassDef->SetClassWithin(GUObjectDef);
|
|
}
|
|
InterfaceClassDef->SetInternalFlags(EInternalObjectFlags::Native);
|
|
InterfaceClassDef->SetClassFlags(CLASS_Native);
|
|
|
|
// Process all of the interface specifiers
|
|
for (const FPropertySpecifier& Specifier : SpecifiersFound)
|
|
{
|
|
switch ((EInterfaceSpecifier)Algo::FindSortedStringCaseInsensitive(*Specifier.Key, GInterfaceSpecifierStrings))
|
|
{
|
|
default:
|
|
{
|
|
Throwf(TEXT("Unknown interface specifier '%s'"), *Specifier.Key);
|
|
}
|
|
break;
|
|
|
|
case EInterfaceSpecifier::DependsOn:
|
|
{
|
|
Throwf(TEXT("The dependsOn specifier is deprecated. Please use #include \"ClassHeaderFilename.h\" instead."));
|
|
}
|
|
break;
|
|
|
|
case EInterfaceSpecifier::MinimalAPI:
|
|
{
|
|
InterfaceClassDef->SetClassFlags(CLASS_MinimalAPI);
|
|
}
|
|
break;
|
|
|
|
case EInterfaceSpecifier::ConversionRoot:
|
|
{
|
|
MetaData.Add(FHeaderParserNames::NAME_IsConversionRoot, TEXT("true"));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// All classes must start with a valid Unreal prefix
|
|
const FString ExpectedInterfaceName = InterfaceClassDef->GetNameWithPrefix(EEnforceInterfacePrefix::U);
|
|
if (DeclaredInterfaceName != ExpectedInterfaceName)
|
|
{
|
|
Throwf(TEXT("Interface name '%s' is invalid, the first class should be identified as '%s'"), *DeclaredInterfaceName, *ExpectedInterfaceName );
|
|
}
|
|
|
|
// Try parsing metadata for the interface
|
|
InterfaceClassDef->SetPrologLine(PrologFinishLine);
|
|
|
|
// Register the metadata
|
|
AddModuleRelativePathToMetadata(*InterfaceClassDef, MetaData);
|
|
FUHTMetaData::RemapAndAddMetaData(*InterfaceClassDef, MoveTemp(MetaData));
|
|
|
|
// Handle the start of the rest of the interface
|
|
RequireSymbol( TEXT('{'), TEXT("'Class'") );
|
|
|
|
// Push the interface class nesting.
|
|
// we need a more specific set of allow flags for ENestType::Interface, only function declaration is allowed, no other stuff are allowed
|
|
PushNest(ENestType::Interface, InterfaceClassDef);
|
|
}
|
|
|
|
void FHeaderParser::CompileRigVMMethodDeclaration(FUnrealStructDefinitionInfo& StructDef)
|
|
{
|
|
if (!MatchSymbol(TEXT("(")))
|
|
{
|
|
Throwf(TEXT("Bad RIGVM_METHOD definition"));
|
|
}
|
|
|
|
// find the next close brace
|
|
while (!MatchSymbol(TEXT(")")))
|
|
{
|
|
FToken Token;
|
|
if (!GetToken(Token))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
FToken PrefixToken, ReturnTypeToken, NameToken, PostfixToken;
|
|
if (!GetToken(PrefixToken))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (PrefixToken.IsIdentifier(TEXT("virtual"), ESearchCase::CaseSensitive))
|
|
{
|
|
if (!GetToken(ReturnTypeToken))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ReturnTypeToken = PrefixToken;
|
|
}
|
|
|
|
if (!GetToken(NameToken))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!MatchSymbol(TEXT("(")))
|
|
{
|
|
Throwf(TEXT("Bad RIGVM_METHOD definition"));
|
|
}
|
|
|
|
TArray<FString> ParamsContent;
|
|
while (!MatchSymbol(TEXT(")")))
|
|
{
|
|
FToken Token;
|
|
if (!GetToken(Token))
|
|
{
|
|
break;
|
|
}
|
|
ParamsContent.Add(FString(Token.Value));
|
|
}
|
|
|
|
while (!PostfixToken.IsSymbol(TEXT(';')))
|
|
{
|
|
if (!GetToken(PostfixToken))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
FRigVMMethodInfo MethodInfo;
|
|
MethodInfo.ReturnType = FString(ReturnTypeToken.Value);
|
|
MethodInfo.Name = FString(NameToken.Value);
|
|
|
|
// look out for the upgrade info method
|
|
static const FString GetUpgradeInfoString = TEXT("GetUpgradeInfo");
|
|
static const FString RigVMStructUpgradeInfoString = TEXT("FRigVMStructUpgradeInfo");
|
|
if(MethodInfo.ReturnType == RigVMStructUpgradeInfoString && MethodInfo.Name == GetUpgradeInfoString)
|
|
{
|
|
FRigVMStructInfo& StructRigVMInfo = StructDef.GetRigVMInfo();
|
|
StructRigVMInfo.bHasGetUpgradeInfoMethod = true;
|
|
return;
|
|
}
|
|
|
|
FString ParamString = FString::Join(ParamsContent, TEXT(" "));
|
|
if (!ParamString.IsEmpty())
|
|
{
|
|
FString ParamPrev, ParamLeft, ParamRight;
|
|
ParamPrev = ParamString;
|
|
while (ParamPrev.Contains(TEXT(",")))
|
|
{
|
|
ParamPrev.Split(TEXT(","), &ParamLeft, &ParamRight);
|
|
FRigVMParameter Parameter;
|
|
Parameter.Name = ParamLeft.TrimStartAndEnd();
|
|
MethodInfo.Parameters.Add(Parameter);
|
|
ParamPrev = ParamRight;
|
|
}
|
|
|
|
ParamPrev = ParamPrev.TrimStartAndEnd();
|
|
if (!ParamPrev.IsEmpty())
|
|
{
|
|
FRigVMParameter Parameter;
|
|
Parameter.Name = ParamPrev.TrimStartAndEnd();
|
|
MethodInfo.Parameters.Add(Parameter);
|
|
}
|
|
}
|
|
|
|
for (FRigVMParameter& Parameter : MethodInfo.Parameters)
|
|
{
|
|
FString FullParameter = Parameter.Name;
|
|
|
|
int32 LastEqual = INDEX_NONE;
|
|
if (FullParameter.FindLastChar(TCHAR('='), LastEqual))
|
|
{
|
|
FullParameter = FullParameter.Mid(0, LastEqual);
|
|
}
|
|
|
|
FullParameter.TrimStartAndEndInline();
|
|
|
|
FString ParameterType = FullParameter;
|
|
FString ParameterName = FullParameter;
|
|
|
|
int32 LastSpace = INDEX_NONE;
|
|
if (FullParameter.FindLastChar(TCHAR(' '), LastSpace))
|
|
{
|
|
Parameter.Type = FullParameter.Mid(0, LastSpace);
|
|
Parameter.Name = FullParameter.Mid(LastSpace + 1);
|
|
Parameter.Type.TrimStartAndEndInline();
|
|
Parameter.Name.TrimStartAndEndInline();
|
|
}
|
|
}
|
|
|
|
FRigVMStructInfo& StructRigVMInfo = StructDef.GetRigVMInfo();
|
|
StructRigVMInfo.bHasRigVM = true;
|
|
StructRigVMInfo.Name = StructDef.GetName();
|
|
StructRigVMInfo.Methods.Add(MethodInfo);
|
|
}
|
|
|
|
const FName FHeaderParser::NAME_InputText(TEXT("Input"));
|
|
const FName FHeaderParser::NAME_OutputText(TEXT("Output"));
|
|
const FName FHeaderParser::NAME_ConstantText(TEXT("Constant"));
|
|
const FName FHeaderParser::NAME_VisibleText(TEXT("Visible"));
|
|
|
|
const FName FHeaderParser::NAME_SingletonText(TEXT("Singleton"));
|
|
|
|
const TCHAR* FHeaderParser::TArrayText = TEXT("TArray");
|
|
const TCHAR* FHeaderParser::TEnumAsByteText = TEXT("TEnumAsByte");
|
|
const TCHAR* FHeaderParser::GetRefText = TEXT("GetRef");
|
|
|
|
const TCHAR* FHeaderParser::FTArrayText = TEXT("TArray");
|
|
const TCHAR* FHeaderParser::FTArrayViewText = TEXT("TArrayView");
|
|
const TCHAR* FHeaderParser::GetArrayText = TEXT("GetArray");
|
|
const TCHAR* FHeaderParser::GetArrayViewText = TEXT("GetArrayView");
|
|
|
|
void FHeaderParser::ParseRigVMMethodParameters(FUnrealStructDefinitionInfo& StructDef)
|
|
{
|
|
FRigVMStructInfo& StructRigVMInfo = StructDef.GetRigVMInfo();
|
|
if (!StructRigVMInfo.bHasRigVM)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// validate the property types for this struct
|
|
for (FUnrealPropertyDefinitionInfo* PropertyDef : TUHTFieldRange<FUnrealPropertyDefinitionInfo>(StructDef))
|
|
{
|
|
FString MemberCPPType;
|
|
FString ExtendedCPPType;
|
|
MemberCPPType = PropertyDef->GetCPPType(&ExtendedCPPType);
|
|
|
|
if (ExtendedCPPType.IsEmpty() && MemberCPPType.StartsWith(TEnumAsByteText))
|
|
{
|
|
MemberCPPType = MemberCPPType.LeftChop(1).RightChop(12);
|
|
}
|
|
|
|
FRigVMParameter Parameter;
|
|
Parameter.Name = PropertyDef->GetName();
|
|
Parameter.Type = MemberCPPType + ExtendedCPPType;
|
|
Parameter.bConstant = PropertyDef->HasMetaData(NAME_ConstantText);
|
|
Parameter.bInput = PropertyDef->HasMetaData(NAME_InputText);
|
|
Parameter.bOutput = PropertyDef->HasMetaData(NAME_OutputText);
|
|
Parameter.Getter = GetRefText;
|
|
Parameter.bEditorOnly = PropertyDef->IsEditorOnlyProperty();
|
|
Parameter.bSingleton = PropertyDef->HasMetaData(NAME_SingletonText);
|
|
Parameter.bIsEnum = PropertyDef->GetPropertyBase().IsEnum();
|
|
|
|
if (PropertyDef->HasMetaData(NAME_VisibleText))
|
|
{
|
|
Parameter.bConstant = true;
|
|
Parameter.bInput = true;
|
|
Parameter.bOutput = false;
|
|
}
|
|
|
|
if (Parameter.bEditorOnly)
|
|
{
|
|
LogError(TEXT("RigVM Struct '%s' - Member '%s' is editor only - WITH_EDITORONLY_DATA not allowed on structs with RIGVM_METHOD."), *StructDef.GetName(), *Parameter.Name, *MemberCPPType);
|
|
}
|
|
|
|
#if !UE_RIGVM_UOBJECT_PROPERTIES_ENABLED
|
|
if(PropertyDef->GetPropertyBase().IsObjectOrInterface())
|
|
{
|
|
LogError(TEXT("RigVM Struct '%s' - Member '%s' is a UObject or interface - object types are not allowed on structs with RIGVM_METHOD."), *StructDef.GetName(), *Parameter.Name);
|
|
}
|
|
#endif
|
|
|
|
if (!ExtendedCPPType.IsEmpty())
|
|
{
|
|
// we only support arrays - no maps or similar data structures
|
|
if (MemberCPPType != TArrayText && MemberCPPType != TEnumAsByteText)
|
|
{
|
|
LogError(TEXT("RigVM Struct '%s' - Member '%s' type '%s' not supported by RigVM."), *StructDef.GetName(), *Parameter.Name, *MemberCPPType);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (MemberCPPType.StartsWith(TArrayText, ESearchCase::CaseSensitive))
|
|
{
|
|
ExtendedCPPType = FString::Printf(TEXT("<%s>"), *ExtendedCPPType.LeftChop(1).RightChop(1));
|
|
|
|
if(Parameter.IsConst())
|
|
{
|
|
ExtendedCPPType = FString::Printf(TEXT("<const %s>"), *ExtendedCPPType.LeftChop(1).RightChop(1));
|
|
Parameter.CastName = FString::Printf(TEXT("%s_%d_Array"), *Parameter.Name, StructRigVMInfo.Members.Num());
|
|
Parameter.CastType = FString::Printf(TEXT("%s%s"), FTArrayViewText, *ExtendedCPPType);
|
|
}
|
|
}
|
|
|
|
StructRigVMInfo.Members.Add(MoveTemp(Parameter));
|
|
}
|
|
|
|
if (StructRigVMInfo.Members.Num() == 0)
|
|
{
|
|
LogError(TEXT("RigVM Struct '%s' - has zero members - invalid RIGVM_METHOD."), *StructDef.GetName());
|
|
}
|
|
|
|
if (StructRigVMInfo.Members.Num() > 64)
|
|
{
|
|
LogError(TEXT("RigVM Struct '%s' - has %d members (64 is the limit)."), *StructDef.GetName(), StructRigVMInfo.Members.Num());
|
|
}
|
|
}
|
|
|
|
// Returns true if the token is a dynamic delegate declaration
|
|
bool FHeaderParser::IsValidDelegateDeclaration(const FToken& Token) const
|
|
{
|
|
return Token.IsIdentifier() && Token.ValueStartsWith(TEXT("DECLARE_DYNAMIC_"), ESearchCase::CaseSensitive);
|
|
}
|
|
|
|
// Parse the parameter list of a function or delegate declaration
|
|
void FHeaderParser::ParseParameterList(FUnrealFunctionDefinitionInfo& FunctionDef, bool bExpectCommaBeforeName, TMap<FName, FString>* MetaData, EGetVarTypeOptions Options)
|
|
{
|
|
// Get parameter list.
|
|
if (MatchSymbol(TEXT(')')))
|
|
{
|
|
return;
|
|
}
|
|
|
|
FAdvancedDisplayParameterHandler AdvancedDisplay(MetaData);
|
|
do
|
|
{
|
|
// Get parameter type.
|
|
FPropertyBase Property(CPT_None);
|
|
EVariableCategory VariableCategory = FunctionDef.HasAnyFunctionFlags(FUNC_Net) ? EVariableCategory::ReplicatedParameter : EVariableCategory::RegularParameter;
|
|
GetVarType(GetCurrentScope(), Options, Property, ~(CPF_ParmFlags | CPF_AutoWeak | CPF_RepSkip | CPF_UObjectWrapper | CPF_NativeAccessSpecifiers), EUHTPropertyType::None, CPF_None, EPropertyDeclarationStyle::None, VariableCategory);
|
|
Property.PropertyFlags |= CPF_Parm;
|
|
|
|
if (bExpectCommaBeforeName)
|
|
{
|
|
RequireSymbol(TEXT(','), TEXT("Delegate definitions require a , between the parameter type and parameter name"));
|
|
}
|
|
|
|
FUnrealPropertyDefinitionInfo& PropDef = GetVarNameAndDim(FunctionDef, Property, VariableCategory);
|
|
|
|
if( AdvancedDisplay.CanMarkMore() && AdvancedDisplay.ShouldMarkParameter(PropDef.GetName()) )
|
|
{
|
|
PropDef.SetPropertyFlags(CPF_AdvancedDisplay);
|
|
}
|
|
|
|
// Check parameters.
|
|
if (FunctionDef.HasAnyFunctionFlags(FUNC_Net))
|
|
{
|
|
if (Property.MapKeyProp.IsValid())
|
|
{
|
|
if (!FunctionDef.HasAnyFunctionFlags(FUNC_NetRequest | FUNC_NetResponse))
|
|
{
|
|
LogError(TEXT("Maps are not supported in an RPC."));
|
|
}
|
|
}
|
|
else if (Property.ArrayType == EArrayType::Set)
|
|
{
|
|
if (!FunctionDef.HasAnyFunctionFlags(FUNC_NetRequest | FUNC_NetResponse))
|
|
{
|
|
LogError(TEXT("Sets are not supported in an RPC."));
|
|
}
|
|
}
|
|
|
|
if (Property.Type == CPT_Struct)
|
|
{
|
|
if (!FunctionDef.HasAnyFunctionFlags(FUNC_NetRequest | FUNC_NetResponse))
|
|
{
|
|
ValidateScriptStructOkForNet(Property.ScriptStructDef->GetName(), *Property.ScriptStructDef);
|
|
}
|
|
}
|
|
|
|
if (!FunctionDef.HasAnyFunctionFlags(FUNC_NetRequest))
|
|
{
|
|
if (Property.PropertyFlags & CPF_OutParm)
|
|
{
|
|
LogError(TEXT("Replicated functions cannot contain out parameters"));
|
|
}
|
|
|
|
if (Property.PropertyFlags & CPF_RepSkip)
|
|
{
|
|
LogError(TEXT("Only service request functions cannot contain NoReplication parameters"));
|
|
}
|
|
|
|
if (PropDef.GetPropertyBase().IsDelegateOrDelegateStaticArray())
|
|
{
|
|
LogError(TEXT("Replicated functions cannot contain delegate parameters (this would be insecure)"));
|
|
}
|
|
|
|
if (Property.Type == CPT_String && Property.RefQualifier != ERefQualifier::ConstRef && !PropDef.IsStaticArray())
|
|
{
|
|
LogError(TEXT("Replicated FString parameters must be passed by const reference"));
|
|
}
|
|
|
|
if (Property.ArrayType == EArrayType::Dynamic && Property.RefQualifier != ERefQualifier::ConstRef && !PropDef.IsStaticArray())
|
|
{
|
|
LogError(TEXT("Replicated TArray parameters must be passed by const reference"));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!(Property.PropertyFlags & CPF_RepSkip) && (Property.PropertyFlags & CPF_OutParm))
|
|
{
|
|
LogError(TEXT("Service request functions cannot contain out parameters, unless marked NotReplicated"));
|
|
}
|
|
|
|
if (!(Property.PropertyFlags & CPF_RepSkip) && PropDef.GetPropertyBase().IsDelegateOrDelegateStaticArray())
|
|
{
|
|
LogError(TEXT("Service request functions cannot contain delegate parameters, unless marked NotReplicated"));
|
|
}
|
|
}
|
|
}
|
|
if (FunctionDef.HasAnyFunctionFlags(FUNC_BlueprintEvent|FUNC_BlueprintCallable))
|
|
{
|
|
if (Property.Type == CPT_Byte && Property.IsPrimitiveOrPrimitiveStaticArray())
|
|
{
|
|
if (FUnrealEnumDefinitionInfo* EnumDef = Property.AsEnum())
|
|
{
|
|
if (EnumDef->GetUnderlyingType() != EUnderlyingEnumType::uint8 &&
|
|
EnumDef->GetUnderlyingType() != EUnderlyingEnumType::Unspecified)
|
|
{
|
|
Throwf(TEXT("Invalid enum param for Blueprints - currently only uint8 supported"));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check that the parameter name is valid and does not conflict with pre-defined types
|
|
{
|
|
const static TArray<FString> InvalidParamNames =
|
|
{
|
|
TEXT("self"),
|
|
};
|
|
|
|
for (const FString& InvalidName : InvalidParamNames)
|
|
{
|
|
if (FCString::Stricmp(*PropDef.GetNameCPP(), *InvalidName) == 0)
|
|
{
|
|
LogError(TEXT("Paramater name '%s' in function is invalid, '%s' is a reserved name."), *InvalidName, *InvalidName);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Default value.
|
|
if (MatchSymbol(TEXT('=')))
|
|
{
|
|
// Skip past the native specified default value; we make no attempt to parse it
|
|
FToken SkipToken;
|
|
int32 ParenthesisNestCount=0;
|
|
int32 StartPos=-1;
|
|
int32 EndPos=-1;
|
|
while ( GetToken(SkipToken) )
|
|
{
|
|
if (StartPos == -1)
|
|
{
|
|
StartPos = SkipToken.StartPos;
|
|
}
|
|
if ( ParenthesisNestCount == 0
|
|
&& (SkipToken.IsSymbol(TEXT(')')) || SkipToken.IsSymbol(TEXT(','))) )
|
|
{
|
|
// went too far
|
|
UngetToken(SkipToken);
|
|
break;
|
|
}
|
|
EndPos = InputPos;
|
|
if ( SkipToken.IsSymbol(TEXT('(')) )
|
|
{
|
|
ParenthesisNestCount++;
|
|
}
|
|
else if ( SkipToken.IsSymbol(TEXT(')')) )
|
|
{
|
|
ParenthesisNestCount--;
|
|
}
|
|
}
|
|
|
|
// allow exec functions to be added to the metaData, this is so we can have default params for them.
|
|
const bool bStoreCppDefaultValueInMetaData = FunctionDef.HasAnyFunctionFlags(FUNC_BlueprintCallable | FUNC_Exec);
|
|
|
|
if((EndPos > -1) && bStoreCppDefaultValueInMetaData)
|
|
{
|
|
FString DefaultArgText(EndPos - StartPos, Input + StartPos);
|
|
FString Key(TEXT("CPP_Default_"));
|
|
Key += PropDef.GetName();
|
|
FName KeyName = FName(*Key);
|
|
if (!MetaData->Contains(KeyName))
|
|
{
|
|
FString InnerDefaultValue;
|
|
const bool bDefaultValueParsed = FPropertyTraits::DefaultValueStringCppFormatToInnerFormat(PropDef, DefaultArgText, InnerDefaultValue);
|
|
if (!bDefaultValueParsed)
|
|
{
|
|
Throwf(TEXT("C++ Default parameter not parsed: %s \"%s\" "), *PropDef.GetName(), *DefaultArgText);
|
|
}
|
|
|
|
MetaData->Add(KeyName, InnerDefaultValue);
|
|
UE_LOG(LogCompile, Verbose, TEXT("C++ Default parameter parsed: %s \"%s\" -> \"%s\" "), *PropDef.GetName(), *DefaultArgText, *InnerDefaultValue );
|
|
}
|
|
}
|
|
}
|
|
} while( MatchSymbol(TEXT(',')) );
|
|
RequireSymbol( TEXT(')'), TEXT("parameter list") );
|
|
}
|
|
FUnrealFunctionDefinitionInfo& FHeaderParser::CompileDelegateDeclaration(const FStringView& DelegateIdentifier, EDelegateSpecifierAction::Type SpecifierAction)
|
|
{
|
|
const TCHAR* CurrentScopeName = TEXT("Delegate Declaration");
|
|
|
|
TMap<FName, FString> MetaData;
|
|
AddModuleRelativePathToMetadata(SourceFile, MetaData);
|
|
|
|
FFuncInfo FuncInfo;
|
|
|
|
// If this is a UDELEGATE, parse the specifiers first
|
|
FString DelegateMacro;
|
|
if (SpecifierAction == EDelegateSpecifierAction::Parse)
|
|
{
|
|
TArray<FPropertySpecifier> SpecifiersFound;
|
|
ReadSpecifierSetInsideMacro(SpecifiersFound, TEXT("Delegate"), MetaData);
|
|
|
|
ProcessFunctionSpecifiers(*this, FuncInfo, SpecifiersFound, MetaData);
|
|
|
|
// Get the next token and ensure it looks like a delegate
|
|
FToken Token;
|
|
GetToken(Token);
|
|
if (!IsValidDelegateDeclaration(Token))
|
|
{
|
|
Throwf(TEXT("Unexpected token following UDELEGATE(): %s"), *Token.GetTokenValue());
|
|
}
|
|
|
|
DelegateMacro = FString(Token.Value);
|
|
CheckAllow(CurrentScopeName, ENestAllowFlags::TypeDecl);
|
|
}
|
|
else
|
|
{
|
|
DelegateMacro = DelegateIdentifier;
|
|
CheckAllow(CurrentScopeName, ENestAllowFlags::ImplicitDelegateDecl);
|
|
}
|
|
|
|
EGetVarTypeOptions Options = EGetVarTypeOptions::None;
|
|
if (MetaData.Contains(NAME_DeprecatedFunction))
|
|
{
|
|
Options |= EGetVarTypeOptions::OuterTypeDeprecated;
|
|
}
|
|
|
|
// Break the delegate declaration macro down into parts
|
|
const bool bHasReturnValue = DelegateMacro.Contains(TEXT("_RetVal"), ESearchCase::CaseSensitive);
|
|
const bool bDeclaredConst = DelegateMacro.Contains(TEXT("_Const"), ESearchCase::CaseSensitive);
|
|
const bool bIsMulticast = DelegateMacro.Contains(TEXT("_MULTICAST"), ESearchCase::CaseSensitive);
|
|
const bool bIsSparse = DelegateMacro.Contains(TEXT("_SPARSE"), ESearchCase::CaseSensitive);
|
|
|
|
// Determine the parameter count
|
|
const FString* FoundParamCount = UHTConfig.DelegateParameterCountStrings.FindByPredicate([&](const FString& Str){ return DelegateMacro.Contains(Str); });
|
|
|
|
// Try reconstructing the string to make sure it matches our expectations
|
|
FString ExpectedOriginalString = FString::Printf(TEXT("DECLARE_DYNAMIC%s%s_DELEGATE%s%s%s"),
|
|
bIsMulticast ? TEXT("_MULTICAST") : TEXT(""),
|
|
bIsSparse ? TEXT("_SPARSE") : TEXT(""),
|
|
bHasReturnValue ? TEXT("_RetVal") : TEXT(""),
|
|
FoundParamCount ? **FoundParamCount : TEXT(""),
|
|
bDeclaredConst ? TEXT("_Const") : TEXT(""));
|
|
|
|
if (DelegateMacro != ExpectedOriginalString)
|
|
{
|
|
Throwf(TEXT("Unable to parse delegate declaration; expected '%s' but found '%s'."), *ExpectedOriginalString, *DelegateMacro);
|
|
}
|
|
|
|
// Multi-cast delegate function signatures are not allowed to have a return value
|
|
if (bHasReturnValue && bIsMulticast)
|
|
{
|
|
LogError(TEXT("Multi-cast delegates function signatures must not return a value"));
|
|
}
|
|
|
|
// Delegate signature
|
|
FuncInfo.FunctionFlags |= FUNC_Public | FUNC_Delegate;
|
|
|
|
if (bIsMulticast)
|
|
{
|
|
FuncInfo.FunctionFlags |= FUNC_MulticastDelegate;
|
|
}
|
|
|
|
const TCHAR* StartPos = &Input[InputPos];
|
|
|
|
// Now parse the macro body
|
|
RequireSymbol(TEXT('('), CurrentScopeName);
|
|
|
|
// Parse the return value type
|
|
FPropertyBase ReturnType( CPT_None );
|
|
|
|
if (bHasReturnValue)
|
|
{
|
|
GetVarType(GetCurrentScope(), Options, ReturnType, CPF_None, EUHTPropertyType::None, CPF_None, EPropertyDeclarationStyle::None, EVariableCategory::Return);
|
|
RequireSymbol(TEXT(','), CurrentScopeName);
|
|
}
|
|
|
|
// Skip whitespaces to get InputPos exactly on beginning of function name.
|
|
SkipWhitespaceAndComments();
|
|
|
|
FuncInfo.InputPos = InputPos;
|
|
|
|
// Get the delegate name
|
|
FToken FuncNameToken;
|
|
if (!GetIdentifier(FuncNameToken))
|
|
{
|
|
Throwf(TEXT("Missing name for %s"), CurrentScopeName );
|
|
}
|
|
FString FuncName(FuncNameToken.Value);
|
|
|
|
// If this is a delegate function then go ahead and mangle the name so we don't collide with
|
|
// actual functions or properties
|
|
{
|
|
//@TODO: UCREMOVAL: Eventually this mangling shouldn't occur
|
|
|
|
// Remove the leading F
|
|
|
|
if (!FuncName.StartsWith(TEXT("F"), ESearchCase::CaseSensitive))
|
|
{
|
|
Throwf(TEXT("Delegate type declarations must start with F"));
|
|
}
|
|
|
|
FuncName.RightChopInline(1, false);
|
|
|
|
// Append the signature goo
|
|
FuncName += HEADER_GENERATED_DELEGATE_SIGNATURE_SUFFIX;
|
|
}
|
|
|
|
FUnrealFunctionDefinitionInfo& DelegateSignatureFunctionDef = CreateFunction(*FuncName, MoveTemp(FuncInfo), bIsSparse ? EFunctionType::SparseDelegate : EFunctionType::Delegate);
|
|
DelegateSignatureFunctionDef.GetDefinitionRange().Start = StartPos;
|
|
|
|
// determine whether this function should be 'const'
|
|
if (bDeclaredConst)
|
|
{
|
|
DelegateSignatureFunctionDef.SetFunctionFlags(FUNC_Const);
|
|
}
|
|
|
|
if (bIsSparse)
|
|
{
|
|
FToken OwningClass;
|
|
|
|
RequireSymbol(TEXT(','), TEXT("Delegate Declaration"));
|
|
|
|
if (!GetIdentifier(OwningClass))
|
|
{
|
|
Throwf(TEXT("Missing OwningClass specifier."));
|
|
}
|
|
RequireSymbol(TEXT(','), TEXT("Delegate Declaration"));
|
|
|
|
FToken DelegateName;
|
|
if (!GetIdentifier(DelegateName))
|
|
{
|
|
Throwf(TEXT("Missing Delegate Name."));
|
|
}
|
|
|
|
DelegateSignatureFunctionDef.SetSparseOwningClassName(*GetClassNameWithoutPrefix(FString(OwningClass.Value)));
|
|
DelegateSignatureFunctionDef.SetSparseDelegateName(FName(DelegateName.Value, FNAME_Add));
|
|
}
|
|
|
|
DelegateSignatureFunctionDef.SetLineNumber(InputLine);
|
|
|
|
// Get parameter list.
|
|
if (FoundParamCount)
|
|
{
|
|
RequireSymbol(TEXT(','), CurrentScopeName);
|
|
|
|
ParseParameterList(DelegateSignatureFunctionDef, /*bExpectCommaBeforeName=*/ true);
|
|
|
|
// Check the expected versus actual number of parameters
|
|
int32 ParamCount = UE_PTRDIFF_TO_INT32(FoundParamCount - UHTConfig.DelegateParameterCountStrings.GetData()) + 1;
|
|
if (DelegateSignatureFunctionDef.GetProperties().Num() != ParamCount)
|
|
{
|
|
Throwf(TEXT("Expected %d parameters but found %d parameters"), ParamCount, DelegateSignatureFunctionDef.GetProperties().Num());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Require the closing paren even with no parameter list
|
|
RequireSymbol(TEXT(')'), TEXT("Delegate Declaration"));
|
|
}
|
|
|
|
DelegateSignatureFunctionDef.GetDefinitionRange().End = &Input[InputPos];
|
|
|
|
// The macro line must be set here
|
|
DelegateSignatureFunctionDef.GetFunctionData().MacroLine = InputLine;
|
|
|
|
// Create the return value property
|
|
if (bHasReturnValue)
|
|
{
|
|
ReturnType.PropertyFlags |= CPF_Parm | CPF_OutParm | CPF_ReturnParm;
|
|
FUnrealPropertyDefinitionInfo& PropDef = GetVarNameAndDim(DelegateSignatureFunctionDef, ReturnType, EVariableCategory::Return);
|
|
}
|
|
|
|
// Try parsing metadata for the function
|
|
ParseFieldMetaData(MetaData, *DelegateSignatureFunctionDef.GetName());
|
|
|
|
AddFormattedPrevCommentAsTooltipMetaData(MetaData);
|
|
|
|
FUHTMetaData::RemapAndAddMetaData(DelegateSignatureFunctionDef, MoveTemp(MetaData));
|
|
|
|
// Optionally consume a semicolon, it's not required for the delegate macro since it contains one internally
|
|
MatchSemi();
|
|
|
|
// End the nesting
|
|
PostPopFunctionDeclaration(DelegateSignatureFunctionDef);
|
|
|
|
// Don't allow delegate signatures to be redefined.
|
|
auto FunctionIterator = GetCurrentScope()->GetTypeIterator<FUnrealFunctionDefinitionInfo>();
|
|
while (FunctionIterator.MoveNext())
|
|
{
|
|
FUnrealFunctionDefinitionInfo* TestFuncDef = *FunctionIterator;
|
|
if (TestFuncDef != &DelegateSignatureFunctionDef && TestFuncDef->GetFName() == DelegateSignatureFunctionDef.GetFName())
|
|
{
|
|
Throwf(TEXT("Can't override delegate signature function '%s'"), *DelegateSignatureFunctionDef.GetNameCPP());
|
|
}
|
|
}
|
|
|
|
return DelegateSignatureFunctionDef;
|
|
}
|
|
|
|
/**
|
|
* Parses and compiles a function declaration
|
|
*/
|
|
FUnrealFunctionDefinitionInfo& FHeaderParser::CompileFunctionDeclaration()
|
|
{
|
|
CheckAllow(TEXT("'Function'"), ENestAllowFlags::Function);
|
|
|
|
TMap<FName, FString> MetaData;
|
|
AddModuleRelativePathToMetadata(SourceFile, MetaData);
|
|
|
|
// New-style UFUNCTION() syntax
|
|
TArray<FPropertySpecifier> SpecifiersFound;
|
|
ReadSpecifierSetInsideMacro(SpecifiersFound, TEXT("Function"), MetaData);
|
|
|
|
FUnrealClassDefinitionInfo& OuterClassDef = GetCurrentClassDef();
|
|
if (!OuterClassDef.HasAnyClassFlags(CLASS_Native))
|
|
{
|
|
Throwf(TEXT("Should only be here for native classes!"));
|
|
}
|
|
|
|
// Process all specifiers.
|
|
const TCHAR* TypeOfFunction = TEXT("function");
|
|
|
|
bool bAutomaticallyFinal = true;
|
|
|
|
FFuncInfo FuncInfo;
|
|
FuncInfo.MacroLine = InputLine;
|
|
FuncInfo.FunctionFlags = FUNC_Native;
|
|
|
|
// Infer the function's access level from the currently declared C++ access level
|
|
if (CurrentAccessSpecifier == ACCESS_Public)
|
|
{
|
|
FuncInfo.FunctionFlags |= FUNC_Public;
|
|
}
|
|
else if (CurrentAccessSpecifier == ACCESS_Protected)
|
|
{
|
|
FuncInfo.FunctionFlags |= FUNC_Protected;
|
|
}
|
|
else if (CurrentAccessSpecifier == ACCESS_Private)
|
|
{
|
|
FuncInfo.FunctionFlags |= FUNC_Private;
|
|
FuncInfo.FunctionFlags |= FUNC_Final;
|
|
|
|
// This is automatically final as well, but in a different way and for a different reason
|
|
bAutomaticallyFinal = false;
|
|
}
|
|
else
|
|
{
|
|
Throwf(TEXT("Unknown access level"));
|
|
}
|
|
|
|
// non-static functions in a const class must be const themselves
|
|
if (OuterClassDef.HasAnyClassFlags(CLASS_Const))
|
|
{
|
|
FuncInfo.FunctionFlags |= FUNC_Const;
|
|
}
|
|
|
|
if (MatchIdentifier(TEXT("static"), ESearchCase::CaseSensitive))
|
|
{
|
|
FuncInfo.FunctionFlags |= FUNC_Static;
|
|
FuncInfo.FunctionExportFlags |= FUNCEXPORT_CppStatic;
|
|
}
|
|
|
|
if (MetaData.Contains(NAME_CppFromBpEvent))
|
|
{
|
|
FuncInfo.FunctionFlags |= FUNC_Event;
|
|
}
|
|
|
|
if ((GetCurrentCompilerDirective() & ECompilerDirective::WithEditor) != 0)
|
|
{
|
|
FuncInfo.FunctionFlags |= FUNC_EditorOnly;
|
|
}
|
|
|
|
ProcessFunctionSpecifiers(*this, FuncInfo, SpecifiersFound, MetaData);
|
|
|
|
if ((0 != (FuncInfo.FunctionExportFlags & FUNCEXPORT_CustomThunk)) && !MetaData.Contains(NAME_CustomThunk))
|
|
{
|
|
MetaData.Add(NAME_CustomThunk, TEXT("true"));
|
|
}
|
|
|
|
if ((FuncInfo.FunctionFlags & FUNC_BlueprintPure) && OuterClassDef.HasAnyClassFlags(CLASS_Interface))
|
|
{
|
|
// Until pure interface casts are supported, we don't allow pures in interfaces
|
|
LogError(TEXT("BlueprintPure specifier is not allowed for interface functions"));
|
|
}
|
|
|
|
if (FuncInfo.FunctionFlags & FUNC_Net)
|
|
{
|
|
// Network replicated functions are always events, and are only final if sealed
|
|
TypeOfFunction = TEXT("event");
|
|
bAutomaticallyFinal = false;
|
|
}
|
|
|
|
if (FuncInfo.FunctionFlags & FUNC_BlueprintEvent)
|
|
{
|
|
TypeOfFunction = (FuncInfo.FunctionFlags & FUNC_Native) ? TEXT("BlueprintNativeEvent") : TEXT("BlueprintImplementableEvent");
|
|
bAutomaticallyFinal = false;
|
|
}
|
|
|
|
// Record the tokens so we can detect this function as a declaration later (i.e. RPC)
|
|
FRecordTokens RecordTokens(*this, &GetCurrentClassDef(), nullptr);
|
|
|
|
bool bSawVirtual = false;
|
|
|
|
if (MatchIdentifier(TEXT("virtual"), ESearchCase::CaseSensitive))
|
|
{
|
|
bSawVirtual = true;
|
|
}
|
|
|
|
FString* InternalPtr = MetaData.Find(NAME_BlueprintInternalUseOnly); // FBlueprintMetadata::MD_BlueprintInternalUseOnly
|
|
const bool bInternalOnly = InternalPtr && *InternalPtr == TEXT("true");
|
|
|
|
const bool bDeprecated = MetaData.Contains(NAME_DeprecatedFunction); // FBlueprintMetadata::MD_DeprecatedFunction
|
|
|
|
// If this function is blueprint callable or blueprint pure, require a category
|
|
if ((FuncInfo.FunctionFlags & (FUNC_BlueprintCallable | FUNC_BlueprintPure)) != 0)
|
|
{
|
|
const bool bBlueprintAccessor = MetaData.Contains(NAME_BlueprintSetter) || MetaData.Contains(NAME_BlueprintGetter); // FBlueprintMetadata::MD_BlueprintSetter, // FBlueprintMetadata::MD_BlueprintGetter
|
|
const bool bHasMenuCategory = MetaData.Contains(NAME_Category); // FBlueprintMetadata::MD_FunctionCategory
|
|
|
|
if (!bHasMenuCategory && !bInternalOnly && !bDeprecated && !bBlueprintAccessor)
|
|
{
|
|
// To allow for quick iteration, don't enforce the requirement that game functions have to be categorized
|
|
if (bIsCurrentModulePartOfEngine)
|
|
{
|
|
LogError(TEXT("An explicit Category specifier is required for Blueprint accessible functions in an Engine module."));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verify interfaces with respect to their blueprint accessible functions
|
|
if (OuterClassDef.HasAnyClassFlags(CLASS_Interface))
|
|
{
|
|
// Interface with blueprint data should declare explicitly Blueprintable or NotBlueprintable to be clear
|
|
// In the backward compatible case where they declare neither, both of these bools are false
|
|
const bool bCanImplementInBlueprints = OuterClassDef.GetBoolMetaData(NAME_IsBlueprintBase);
|
|
const bool bCannotImplementInBlueprints = (!bCanImplementInBlueprints && OuterClassDef.HasMetaData(NAME_IsBlueprintBase))
|
|
|| OuterClassDef.HasMetaData(NAME_CannotImplementInterfaceInBlueprint);
|
|
|
|
if((FuncInfo.FunctionFlags & FUNC_BlueprintEvent) != 0 && !bInternalOnly)
|
|
{
|
|
// Ensure that blueprint events are only allowed in implementable interfaces. Internal only functions allowed
|
|
if (bCannotImplementInBlueprints)
|
|
{
|
|
LogError(TEXT("Interfaces that are not implementable in blueprints cannot have Blueprint Event members."));
|
|
}
|
|
if (!bCanImplementInBlueprints)
|
|
{
|
|
// We do not currently warn about this case as there are a large number of existing interfaces that do not specify
|
|
// LogWarning(TEXT("Interfaces with Blueprint Events should declare Blueprintable on the interface."));
|
|
}
|
|
}
|
|
|
|
if (((FuncInfo.FunctionFlags & FUNC_BlueprintCallable) != 0) && (((~FuncInfo.FunctionFlags) & FUNC_BlueprintEvent) != 0))
|
|
{
|
|
// Ensure that if this interface contains blueprint callable functions that are not blueprint defined, that it must be implemented natively
|
|
if (bCanImplementInBlueprints)
|
|
{
|
|
LogError(TEXT("Blueprint implementable interfaces cannot contain BlueprintCallable functions that are not BlueprintImplementableEvents. Add NotBlueprintable to the interface if you wish to keep this function."));
|
|
}
|
|
if (!bCannotImplementInBlueprints)
|
|
{
|
|
// Lowered this case to a warning instead of error, they will not show up as blueprintable unless they also have events
|
|
LogWarning(TEXT("Interfaces with BlueprintCallable functions but no events should explicitly declare NotBlueprintable on the interface."));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Peek ahead to look for a CORE_API style DLL import/export token if present
|
|
FString APIMacroIfPresent;
|
|
{
|
|
FToken Token;
|
|
if (GetToken(Token, true))
|
|
{
|
|
bool bThrowTokenBack = true;
|
|
if (Token.IsIdentifier())
|
|
{
|
|
FString RequiredAPIMacroIfPresent(Token.Value);
|
|
if (RequiredAPIMacroIfPresent.EndsWith(TEXT("_API"), ESearchCase::CaseSensitive))
|
|
{
|
|
//@TODO: Validate the module name for RequiredAPIMacroIfPresent
|
|
bThrowTokenBack = false;
|
|
|
|
if (OuterClassDef.HasAnyClassFlags(CLASS_RequiredAPI))
|
|
{
|
|
Throwf(TEXT("'%s' must not be used on methods of a class that is marked '%s' itself."), *RequiredAPIMacroIfPresent, *RequiredAPIMacroIfPresent);
|
|
}
|
|
FuncInfo.FunctionFlags |= FUNC_RequiredAPI;
|
|
FuncInfo.FunctionExportFlags |= FUNCEXPORT_RequiredAPI;
|
|
|
|
APIMacroIfPresent = RequiredAPIMacroIfPresent;
|
|
}
|
|
}
|
|
|
|
if (bThrowTokenBack)
|
|
{
|
|
UngetToken(Token);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Look for static again, in case there was an ENGINE_API token first
|
|
if (!APIMacroIfPresent.IsEmpty() && MatchIdentifier(TEXT("static"), ESearchCase::CaseSensitive))
|
|
{
|
|
Throwf(TEXT("Unexpected API macro '%s'. Did you mean to put '%s' after the static keyword?"), *APIMacroIfPresent, *APIMacroIfPresent);
|
|
}
|
|
|
|
// Look for virtual again, in case there was an ENGINE_API token first
|
|
if (MatchIdentifier(TEXT("virtual"), ESearchCase::CaseSensitive))
|
|
{
|
|
bSawVirtual = true;
|
|
}
|
|
|
|
// Process the virtualness
|
|
if (bSawVirtual)
|
|
{
|
|
// Remove the implicit final, the user can still specifying an explicit final at the end of the declaration
|
|
bAutomaticallyFinal = false;
|
|
|
|
// if this is a BlueprintNativeEvent or BlueprintImplementableEvent in an interface, make sure it's not "virtual"
|
|
if (FuncInfo.FunctionFlags & FUNC_BlueprintEvent)
|
|
{
|
|
if (OuterClassDef.HasAnyClassFlags(CLASS_Interface))
|
|
{
|
|
Throwf(TEXT("BlueprintImplementableEvents in Interfaces must not be declared 'virtual'"));
|
|
}
|
|
|
|
// if this is a BlueprintNativeEvent, make sure it's not "virtual"
|
|
else if (FuncInfo.FunctionFlags & FUNC_Native)
|
|
{
|
|
LogError(TEXT("BlueprintNativeEvent functions must be non-virtual."));
|
|
}
|
|
|
|
else
|
|
{
|
|
LogWarning(TEXT("BlueprintImplementableEvents should not be virtual. Use BlueprintNativeEvent instead."));
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// if this is a function in an Interface, it must be marked 'virtual' unless it's an event
|
|
if (OuterClassDef.HasAnyClassFlags(CLASS_Interface) && !(FuncInfo.FunctionFlags & FUNC_BlueprintEvent))
|
|
{
|
|
Throwf(TEXT("Interface functions that are not BlueprintImplementableEvents must be declared 'virtual'"));
|
|
}
|
|
}
|
|
|
|
// Handle the initial implicit/explicit final
|
|
// A user can still specify an explicit final after the parameter list as well.
|
|
if (bAutomaticallyFinal || FuncInfo.bSealedEvent)
|
|
{
|
|
FuncInfo.FunctionFlags |= FUNC_Final;
|
|
FuncInfo.FunctionExportFlags |= FUNCEXPORT_Final;
|
|
|
|
if (OuterClassDef.HasAnyClassFlags(CLASS_Interface))
|
|
{
|
|
LogError(TEXT("Interface functions cannot be declared 'final'"));
|
|
}
|
|
}
|
|
|
|
EGetVarTypeOptions Options = EGetVarTypeOptions::None;
|
|
if (bDeprecated)
|
|
{
|
|
Options |= EGetVarTypeOptions::OuterTypeDeprecated;
|
|
}
|
|
if (EnumHasAllFlags(FuncInfo.FunctionFlags, FUNC_BlueprintEvent | FUNC_Native))
|
|
{
|
|
Options |= EGetVarTypeOptions::NoAutoConst;
|
|
}
|
|
|
|
// Get return type.
|
|
FPropertyBase ReturnType( CPT_None );
|
|
|
|
// C++ style functions always have a return value type, even if it's void
|
|
GetVarType(GetCurrentScope(), Options, ReturnType, CPF_None, EUHTPropertyType::None, CPF_None, EPropertyDeclarationStyle::None, EVariableCategory::Return);
|
|
bool bHasReturnValue = ReturnType.Type != CPT_None;
|
|
|
|
// Skip whitespaces to get InputPos exactly on beginning of function name.
|
|
SkipWhitespaceAndComments();
|
|
|
|
FuncInfo.InputPos = InputPos;
|
|
|
|
// Get function or operator name.
|
|
FToken FuncNameToken;
|
|
if (!GetIdentifier(FuncNameToken))
|
|
{
|
|
Throwf(TEXT("Missing %s name"), TypeOfFunction);
|
|
}
|
|
FString FuncName(FuncNameToken.Value);
|
|
|
|
const TCHAR* StartPos = &Input[InputPos];
|
|
|
|
if ( !MatchSymbol(TEXT('(')) )
|
|
{
|
|
Throwf(TEXT("Bad %s definition"), TypeOfFunction);
|
|
}
|
|
|
|
if (FuncInfo.FunctionFlags & FUNC_Net)
|
|
{
|
|
bool bIsNetService = !!(FuncInfo.FunctionFlags & (FUNC_NetRequest | FUNC_NetResponse));
|
|
if (bHasReturnValue && !bIsNetService)
|
|
{
|
|
Throwf(TEXT("Replicated functions can't have return values"));
|
|
}
|
|
|
|
if (FuncInfo.RPCId > 0)
|
|
{
|
|
if (FString* ExistingFunc = UsedRPCIds.Find(FuncInfo.RPCId))
|
|
{
|
|
Throwf(TEXT("Function %s already uses identifier %d"), **ExistingFunc, FuncInfo.RPCId);
|
|
}
|
|
|
|
UsedRPCIds.Add(FuncInfo.RPCId, FuncName);
|
|
if (FuncInfo.FunctionFlags & FUNC_NetResponse)
|
|
{
|
|
// Look for another function expecting this response
|
|
if (FString* ExistingFunc = RPCsNeedingHookup.Find(FuncInfo.RPCId))
|
|
{
|
|
// If this list isn't empty at end of class, throw error
|
|
RPCsNeedingHookup.Remove(FuncInfo.RPCId);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (FuncInfo.RPCResponseId > 0 && FuncInfo.EndpointName != TEXT("JSBridge"))
|
|
{
|
|
// Look for an existing response function
|
|
FString* ExistingFunc = UsedRPCIds.Find(FuncInfo.RPCResponseId);
|
|
if (ExistingFunc == NULL)
|
|
{
|
|
// If this list isn't empty at end of class, throw error
|
|
RPCsNeedingHookup.Add(FuncInfo.RPCResponseId, FuncName);
|
|
}
|
|
}
|
|
}
|
|
|
|
FUnrealFunctionDefinitionInfo& FuncDef = CreateFunction(*FuncName, MoveTemp(FuncInfo), EFunctionType::Function);
|
|
FuncDef.GetDefinitionRange().Start = StartPos;
|
|
const FFuncInfo& FuncDefFuncInfo = FuncDef.GetFunctionData();
|
|
FUnrealFunctionDefinitionInfo* SuperFuncDef = FuncDef.GetSuperFunction();
|
|
|
|
// Get parameter list.
|
|
ParseParameterList(FuncDef, false, &MetaData, Options);
|
|
|
|
// Get return type, if any.
|
|
if (bHasReturnValue)
|
|
{
|
|
ReturnType.PropertyFlags |= CPF_Parm | CPF_OutParm | CPF_ReturnParm;
|
|
FUnrealPropertyDefinitionInfo& PropDef = GetVarNameAndDim(FuncDef, ReturnType, EVariableCategory::Return);
|
|
}
|
|
|
|
// determine if there are any outputs for this function
|
|
bool bHasAnyOutputs = bHasReturnValue;
|
|
if (!bHasAnyOutputs)
|
|
{
|
|
for (TSharedRef<FUnrealPropertyDefinitionInfo> PropertyDef : FuncDef.GetProperties())
|
|
{
|
|
if (PropertyDef->HasSpecificPropertyFlags(CPF_ReturnParm | CPF_OutParm, CPF_OutParm))
|
|
{
|
|
bHasAnyOutputs = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check to see if there is a function in the super class with the same name
|
|
FUnrealStructDefinitionInfo* SuperStructDef = &GetCurrentClassDef();
|
|
if (SuperStructDef)
|
|
{
|
|
SuperStructDef = SuperStructDef->GetSuperStruct();
|
|
}
|
|
if (SuperStructDef)
|
|
{
|
|
if (FUnrealFunctionDefinitionInfo* OverriddenFunctionDef = FindFunction(*SuperStructDef, *FuncDef.GetNameCPP(), true, nullptr))
|
|
{
|
|
// Native function overrides should be done in CPP text, not in a UFUNCTION() declaration (you can't change flags, and it'd otherwise be a burden to keep them identical)
|
|
LogError(TEXT("%s: Override of UFUNCTION in parent class (%s) cannot have a UFUNCTION() declaration above it; it will use the same parameters as the original declaration."), *FuncDef.GetNameCPP(), *OverriddenFunctionDef->GetOuter()->GetName());
|
|
}
|
|
}
|
|
|
|
if (!bHasAnyOutputs && FuncDef.HasAnyFunctionFlags(FUNC_BlueprintPure))
|
|
{
|
|
LogError(TEXT("BlueprintPure specifier is not allowed for functions with no return value and no output parameters."));
|
|
}
|
|
|
|
|
|
// determine whether this function should be 'const'
|
|
if ( MatchIdentifier(TEXT("const"), ESearchCase::CaseSensitive) )
|
|
{
|
|
if (FuncDef.HasAnyFunctionFlags(FUNC_Native))
|
|
{
|
|
// @TODO: UCREMOVAL Reconsider?
|
|
//Throwf(TEXT("'const' may only be used for native functions"));
|
|
}
|
|
|
|
FuncDef.SetFunctionFlags(FUNC_Const);
|
|
|
|
// @todo: the presence of const and one or more outputs does not guarantee that there are
|
|
// no side effects. On GCC and clang we could use __attribure__((pure)) or __attribute__((const))
|
|
// or we could just rely on the use marking things BlueprintPure. Either way, checking the C++
|
|
// const identifier to determine purity is not desirable. We should remove the following logic:
|
|
|
|
// If its a const BlueprintCallable function with some sort of output and is not being marked as an BlueprintPure=false function, mark it as BlueprintPure as well
|
|
if (bHasAnyOutputs && FuncDef.HasAnyFunctionFlags(FUNC_BlueprintCallable) && !FuncDefFuncInfo.bForceBlueprintImpure)
|
|
{
|
|
FuncDef.SetFunctionFlags(FUNC_BlueprintPure);
|
|
}
|
|
}
|
|
|
|
// Try parsing metadata for the function
|
|
ParseFieldMetaData(MetaData, *FuncDef.GetName());
|
|
|
|
AddFormattedPrevCommentAsTooltipMetaData(MetaData);
|
|
|
|
FUHTMetaData::RemapAndAddMetaData(FuncDef, MoveTemp(MetaData));
|
|
|
|
// 'final' and 'override' can appear in any order before an optional '= 0' pure virtual specifier
|
|
bool bFoundFinal = MatchIdentifier(TEXT("final"), ESearchCase::CaseSensitive);
|
|
bool bFoundOverride = MatchIdentifier(TEXT("override"), ESearchCase::CaseSensitive);
|
|
if (!bFoundFinal && bFoundOverride)
|
|
{
|
|
bFoundFinal = MatchIdentifier(TEXT("final"), ESearchCase::CaseSensitive);
|
|
}
|
|
|
|
// Handle C++ style functions being declared as abstract
|
|
if (MatchSymbol(TEXT('=')))
|
|
{
|
|
int32 ZeroValue = 1;
|
|
bool bGotZero = GetConstInt(/*out*/ZeroValue);
|
|
bGotZero = bGotZero && (ZeroValue == 0);
|
|
|
|
if (!bGotZero)
|
|
{
|
|
Throwf(TEXT("Expected 0 to indicate function is abstract"));
|
|
}
|
|
}
|
|
|
|
// Look for the final keyword to indicate this function is sealed
|
|
if (bFoundFinal)
|
|
{
|
|
// This is a final (prebinding, non-overridable) function
|
|
FuncDef.SetFunctionFlags(FUNC_Final);
|
|
FuncDef.SetFunctionExportFlags(FUNCEXPORT_Final);
|
|
if (GetCurrentClassDef().HasAnyClassFlags(CLASS_Interface))
|
|
{
|
|
Throwf(TEXT("Interface functions cannot be declared 'final'"));
|
|
}
|
|
else if (FuncDef.HasAnyFunctionFlags(FUNC_BlueprintEvent))
|
|
{
|
|
Throwf(TEXT("Blueprint events cannot be declared 'final'"));
|
|
}
|
|
}
|
|
|
|
// Make sure that the replication flags set on an overridden function match the parent function
|
|
if (SuperFuncDef != nullptr)
|
|
{
|
|
if ((FuncDef.GetFunctionFlags() & FUNC_NetFuncFlags) != (SuperFuncDef->GetFunctionFlags() & FUNC_NetFuncFlags))
|
|
{
|
|
Throwf(TEXT("Overridden function '%s': Cannot specify different replication flags when overriding a function."), *FuncDef.GetName());
|
|
}
|
|
}
|
|
|
|
// if this function is an RPC in state scope, verify that it is an override
|
|
// this is required because the networking code only checks the class for RPCs when initializing network data, not any states within it
|
|
if (FuncDef.HasAnyFunctionFlags(FUNC_Net) && SuperFuncDef == nullptr && UHTCast<FUnrealClassDefinitionInfo>(FuncDef.GetOuter()) == nullptr)
|
|
{
|
|
Throwf(TEXT("Function '%s': Base implementation of RPCs cannot be in a state. Add a stub outside state scope."), *FuncDef.GetName());
|
|
}
|
|
|
|
FuncDef.GetDefinitionRange().End = &Input[InputPos];
|
|
|
|
// Just declaring a function, so end the nesting.
|
|
PostPopFunctionDeclaration(FuncDef);
|
|
|
|
// See what's coming next
|
|
FToken Token;
|
|
if (!GetToken(Token))
|
|
{
|
|
Throwf(TEXT("Unexpected end of file"));
|
|
}
|
|
|
|
// Optionally consume a semicolon
|
|
// This is optional to allow inline function definitions
|
|
if (Token.IsSymbol(TEXT(';')))
|
|
{
|
|
// Do nothing (consume it)
|
|
}
|
|
else if (Token.IsSymbol(TEXT('{')))
|
|
{
|
|
// Skip inline function bodies
|
|
UngetToken(Token);
|
|
SkipDeclaration(Token);
|
|
}
|
|
else
|
|
{
|
|
// Put the token back so we can continue parsing as normal
|
|
UngetToken(Token);
|
|
}
|
|
|
|
// perform documentation policy tests
|
|
CheckDocumentationPolicyForFunc(GetCurrentClassDef(), FuncDef);
|
|
|
|
return FuncDef;
|
|
}
|
|
|
|
/** Parses optional metadata text. */
|
|
void FHeaderParser::ParseFieldMetaData(TMap<FName, FString>& MetaData, const TCHAR* FieldName)
|
|
{
|
|
FTokenString PropertyMetaData;
|
|
bool bMetadataPresent = false;
|
|
if (MatchIdentifier(TEXT("UMETA"), ESearchCase::CaseSensitive))
|
|
{
|
|
auto ErrorMessageGetter = [FieldName]() { return FString::Printf(TEXT("' %s metadata'"), FieldName); };
|
|
|
|
bMetadataPresent = true;
|
|
RequireSymbol( TEXT('('), ErrorMessageGetter );
|
|
if (!GetRawStringRespectingQuotes(PropertyMetaData, TCHAR(')')))
|
|
{
|
|
Throwf(TEXT("'%s': No metadata specified"), FieldName);
|
|
}
|
|
RequireSymbol( TEXT(')'), ErrorMessageGetter);
|
|
}
|
|
|
|
if (bMetadataPresent)
|
|
{
|
|
// parse apart the string
|
|
TArray<FString> Pairs;
|
|
|
|
//@TODO: UCREMOVAL: Convert to property token reading
|
|
// break apart on | to get to the key/value pairs
|
|
FString NewData(PropertyMetaData.String);
|
|
bool bInString = false;
|
|
int32 LastStartIndex = 0;
|
|
int32 CharIndex;
|
|
for (CharIndex = 0; CharIndex < NewData.Len(); ++CharIndex)
|
|
{
|
|
TCHAR Ch = NewData.GetCharArray()[CharIndex];
|
|
if (Ch == '"')
|
|
{
|
|
bInString = !bInString;
|
|
}
|
|
|
|
if ((Ch == ',') && !bInString)
|
|
{
|
|
if (LastStartIndex != CharIndex)
|
|
{
|
|
Pairs.Add(NewData.Mid(LastStartIndex, CharIndex - LastStartIndex));
|
|
}
|
|
LastStartIndex = CharIndex + 1;
|
|
}
|
|
}
|
|
|
|
if (LastStartIndex != CharIndex)
|
|
{
|
|
Pairs.Add(MoveTemp(NewData).Mid(LastStartIndex, CharIndex - LastStartIndex));
|
|
}
|
|
|
|
// go over all pairs
|
|
for (int32 PairIndex = 0; PairIndex < Pairs.Num(); PairIndex++)
|
|
{
|
|
// break the pair into a key and a value
|
|
FString Token = MoveTemp(Pairs[PairIndex]);
|
|
FString Key;
|
|
// by default, not value, just a key (allowed)
|
|
FString Value;
|
|
|
|
// look for a value after an =
|
|
const int32 Equals = Token.Find(TEXT("="), ESearchCase::CaseSensitive);
|
|
// if we have an =, break up the string
|
|
if (Equals != INDEX_NONE)
|
|
{
|
|
Key = Token.Left(Equals);
|
|
Value = MoveTemp(Token);
|
|
Value.RightInline((Value.Len() - Equals) - 1, false);
|
|
}
|
|
else
|
|
{
|
|
Key = MoveTemp(Token);
|
|
}
|
|
|
|
InsertMetaDataPair(MetaData, MoveTemp(Key), MoveTemp(Value));
|
|
}
|
|
}
|
|
}
|
|
|
|
bool FHeaderParser::IsBitfieldProperty(ELayoutMacroType LayoutMacroType)
|
|
{
|
|
if (LayoutMacroType == ELayoutMacroType::Bitfield || LayoutMacroType == ELayoutMacroType::BitfieldEditorOnly)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
bool bIsBitfield = false;
|
|
|
|
// The current token is the property type (uin32, uint16, etc).
|
|
// Check the property name and then check for ':'
|
|
FToken TokenVarName;
|
|
if (GetToken(TokenVarName, /*bNoConsts=*/ true))
|
|
{
|
|
FToken Token;
|
|
if (GetToken(Token, /*bNoConsts=*/ true))
|
|
{
|
|
if (Token.IsSymbol(TEXT(':')))
|
|
{
|
|
bIsBitfield = true;
|
|
}
|
|
UngetToken(Token);
|
|
}
|
|
UngetToken(TokenVarName);
|
|
}
|
|
|
|
return bIsBitfield;
|
|
}
|
|
|
|
void FHeaderParser::ValidateTypeIsDeprecated(EVariableCategory VariableCategory, FUnrealTypeDefinitionInfo* TypeDef)
|
|
{
|
|
if (FUnrealClassDefinitionInfo* ClassDef = UHTCast<FUnrealClassDefinitionInfo>(TypeDef); ClassDef != nullptr && ClassDef->HasAnyClassFlags(CLASS_Deprecated))
|
|
{
|
|
if (VariableCategory == EVariableCategory::Member)
|
|
{
|
|
LogError(TEXT("Property is using a deprecated class: %s. Property should be marked deprecated as well."), *ClassDef->GetPathName());
|
|
}
|
|
else
|
|
{
|
|
LogError(TEXT("Function is using a deprecated class: %s. Function should be marked deprecated as well."), *ClassDef->GetPathName());
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FHeaderParser::ValidatePropertyIsDeprecatedIfNecessary(bool bOuterTypeDeprecated, EVariableCategory VariableCategory, const FPropertyBase& VarProperty, EUHTPropertyType OuterPropertyType, EPropertyFlags OuterPropertyFlags)
|
|
{
|
|
// If the outer object being parsed is deprecated, then we don't need to do any further tests
|
|
if (bOuterTypeDeprecated)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// If the property is in a container that has been deprecated, then we don't need to do any further tests
|
|
if (OuterPropertyType != EUHTPropertyType::None && (OuterPropertyFlags & CPF_Deprecated) != 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// If the property is already marked deprecated, then we don't need to do any further tests
|
|
if ((VarProperty.PropertyFlags & CPF_Deprecated) != 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ValidateTypeIsDeprecated(VariableCategory, VarProperty.MetaClassDef);
|
|
ValidateTypeIsDeprecated(VariableCategory, VarProperty.TypeDef);
|
|
}
|
|
|
|
bool FHeaderParser::ValidateScriptStructOkForNet(const FString& OriginStructName, FUnrealScriptStructDefinitionInfo& InStructDef)
|
|
{
|
|
if (ScriptStructsValidForNet.Contains(&InStructDef))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
bool bIsStructValid = true;
|
|
|
|
if (FUnrealScriptStructDefinitionInfo* SuperScriptStructDef = UHTCast<FUnrealScriptStructDefinitionInfo>(InStructDef.GetSuperStructInfo().Struct))
|
|
{
|
|
if (!ValidateScriptStructOkForNet(OriginStructName, *SuperScriptStructDef))
|
|
{
|
|
bIsStructValid = false;
|
|
}
|
|
}
|
|
|
|
for (TSharedRef<FUnrealPropertyDefinitionInfo> PropertyDef : InStructDef.GetProperties())
|
|
{
|
|
if (PropertyDef->IsSet())
|
|
{
|
|
if (!PropertyDef->HasAnyPropertyFlags(CPF_RepSkip))
|
|
{
|
|
bIsStructValid = false;
|
|
LogError(TEXT("Sets are not supported for Replication or RPCs. Set %s in %s. Origin %s"), *PropertyDef->GetName(), *PropertyDef->GetOuter()->GetName(), *OriginStructName);
|
|
}
|
|
}
|
|
else if (PropertyDef->IsMap())
|
|
{
|
|
if (!PropertyDef->HasAnyPropertyFlags(CPF_RepSkip))
|
|
{
|
|
bIsStructValid = false;
|
|
LogError(TEXT("Maps are not supported for Replication or RPCs. Map %s in %s. Origin %s"), *PropertyDef->GetName(), *PropertyDef->GetOuter()->GetName(), *OriginStructName);
|
|
}
|
|
}
|
|
else if (PropertyDef->IsStructOrStructStaticArray())
|
|
{
|
|
if (!ValidateScriptStructOkForNet(OriginStructName, *PropertyDef->GetPropertyBase().ScriptStructDef))
|
|
{
|
|
bIsStructValid = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bIsStructValid)
|
|
{
|
|
ScriptStructsValidForNet.Add(&InStructDef);
|
|
}
|
|
|
|
return bIsStructValid;
|
|
}
|
|
|
|
struct FExposeOnSpawnValidator
|
|
{
|
|
// Keep this function synced with UEdGraphSchema_K2::FindSetVariableByNameFunction
|
|
static bool IsSupported(const FPropertyBase& Property)
|
|
{
|
|
bool ProperNativeType = false;
|
|
switch (Property.Type)
|
|
{
|
|
case CPT_Int:
|
|
case CPT_Int64:
|
|
case CPT_Byte:
|
|
case CPT_Float:
|
|
case CPT_Bool:
|
|
case CPT_Bool8:
|
|
case CPT_ObjectReference:
|
|
case CPT_ObjectPtrReference:
|
|
case CPT_String:
|
|
case CPT_Text:
|
|
case CPT_Name:
|
|
case CPT_Interface:
|
|
case CPT_SoftObjectReference:
|
|
ProperNativeType = true;
|
|
}
|
|
|
|
if (!ProperNativeType && (CPT_Struct == Property.Type) && Property.ScriptStructDef)
|
|
{
|
|
ProperNativeType |= Property.ScriptStructDef->GetBoolMetaData(FHeaderParserNames::NAME_BlueprintType);
|
|
}
|
|
|
|
return ProperNativeType;
|
|
}
|
|
};
|
|
|
|
void FHeaderParser::CompileVariableDeclaration(FUnrealStructDefinitionInfo& StructDef)
|
|
{
|
|
EPropertyFlags DisallowFlags = CPF_ParmFlags;
|
|
EPropertyFlags EdFlags = CPF_None;
|
|
FUnrealScriptStructDefinitionInfo* ScriptStructDef = UHTCast< FUnrealScriptStructDefinitionInfo>(StructDef);
|
|
|
|
// Get variable type.
|
|
FPropertyBase OriginalPropertyBase(CPT_None);
|
|
FIndexRange TypeRange;
|
|
ELayoutMacroType LayoutMacroType = ELayoutMacroType::None;
|
|
GetVarType(&*StructDef.GetScope(), EGetVarTypeOptions::None, OriginalPropertyBase, DisallowFlags, EUHTPropertyType::None, CPF_None, EPropertyDeclarationStyle::UPROPERTY, EVariableCategory::Member, &TypeRange, &LayoutMacroType);
|
|
OriginalPropertyBase.PropertyFlags |= EdFlags;
|
|
|
|
FString* Category = OriginalPropertyBase.MetaData.Find(NAME_Category);
|
|
|
|
// First check if the category was specified at all and if the property was exposed to the editor.
|
|
if (!Category && (OriginalPropertyBase.PropertyFlags & (CPF_Edit|CPF_BlueprintVisible)))
|
|
{
|
|
if ((&StructDef.GetPackageDef() != nullptr) && !bIsCurrentModulePartOfEngine)
|
|
{
|
|
Category = &OriginalPropertyBase.MetaData.Add(NAME_Category, StructDef.GetName());
|
|
}
|
|
else
|
|
{
|
|
LogError(TEXT("An explicit Category specifier is required for any property exposed to the editor or Blueprints in an Engine module."));
|
|
}
|
|
}
|
|
|
|
// Validate that pointer properties are not interfaces (which are not GC'd and so will cause runtime errors)
|
|
if (OriginalPropertyBase.PointerType == EPointerType::Native && OriginalPropertyBase.ClassDef->AsClass() != nullptr && OriginalPropertyBase.ClassDef->IsInterface())
|
|
{
|
|
// Get the name of the type, removing the asterisk representing the pointer
|
|
FString TypeName = FString(TypeRange.Count, Input + TypeRange.StartIndex).TrimStartAndEnd().LeftChop(1).TrimEnd();
|
|
Throwf(TEXT("UPROPERTY pointers cannot be interfaces - did you mean TScriptInterface<%s>?"), *TypeName);
|
|
}
|
|
|
|
// If the category was specified explicitly, it wins
|
|
if (Category && !(OriginalPropertyBase.PropertyFlags & (CPF_Edit|CPF_BlueprintVisible|CPF_BlueprintAssignable|CPF_BlueprintCallable)))
|
|
{
|
|
LogWarning(TEXT("Property has a Category set but is not exposed to the editor or Blueprints with EditAnywhere, BlueprintReadWrite, VisibleAnywhere, BlueprintReadOnly, BlueprintAssignable, BlueprintCallable keywords.\r\n"));
|
|
}
|
|
|
|
// Make sure that editblueprint variables are editable
|
|
if(!(OriginalPropertyBase.PropertyFlags & CPF_Edit))
|
|
{
|
|
if (OriginalPropertyBase.PropertyFlags & CPF_DisableEditOnInstance)
|
|
{
|
|
LogError(TEXT("Property cannot have 'DisableEditOnInstance' without being editable"));
|
|
}
|
|
|
|
if (OriginalPropertyBase.PropertyFlags & CPF_DisableEditOnTemplate)
|
|
{
|
|
LogError(TEXT("Property cannot have 'DisableEditOnTemplate' without being editable"));
|
|
}
|
|
}
|
|
|
|
// Validate.
|
|
if (OriginalPropertyBase.PropertyFlags & CPF_ParmFlags)
|
|
{
|
|
Throwf(TEXT("Illegal type modifiers in member variable declaration") );
|
|
}
|
|
|
|
if (FString* ExposeOnSpawnValue = OriginalPropertyBase.MetaData.Find(NAME_ExposeOnSpawn))
|
|
{
|
|
if ((*ExposeOnSpawnValue == TEXT("true")) && !FExposeOnSpawnValidator::IsSupported(OriginalPropertyBase))
|
|
{
|
|
LogError(TEXT("ExposeOnSpawn - Property cannot be exposed"));
|
|
}
|
|
}
|
|
|
|
if (LayoutMacroType != ELayoutMacroType::None)
|
|
{
|
|
RequireSymbol(TEXT(','), GLayoutMacroNames[(int32)LayoutMacroType]);
|
|
}
|
|
|
|
// If Property is a Replicated Struct check to make sure there are no Properties that are not allowed to be Replicated in the Struct
|
|
if (OriginalPropertyBase.Type == CPT_Struct && OriginalPropertyBase.PropertyFlags & CPF_Net && OriginalPropertyBase.ScriptStructDef)
|
|
{
|
|
ValidateScriptStructOkForNet(OriginalPropertyBase.ScriptStructDef->GetName(), *OriginalPropertyBase.ScriptStructDef);
|
|
}
|
|
|
|
// Process all variables of this type.
|
|
TArray<FUnrealPropertyDefinitionInfo*> NewProperties;
|
|
for (;;)
|
|
{
|
|
FPropertyBase PropertyBase = OriginalPropertyBase;
|
|
FUnrealPropertyDefinitionInfo& NewPropDef = GetVarNameAndDim(StructDef, PropertyBase, EVariableCategory::Member, LayoutMacroType);
|
|
|
|
// Optionally consume the :1 at the end of a bitfield boolean declaration
|
|
if (PropertyBase.IsBool())
|
|
{
|
|
if (LayoutMacroType == ELayoutMacroType::Bitfield || LayoutMacroType == ELayoutMacroType::BitfieldEditorOnly || MatchSymbol(TEXT(':')))
|
|
{
|
|
int32 BitfieldSize = 0;
|
|
if (!GetConstInt(/*out*/ BitfieldSize) || (BitfieldSize != 1))
|
|
{
|
|
Throwf(TEXT("Bad or missing bitfield size for '%s', must be 1."), *NewPropDef.GetName());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Deprecation validation
|
|
ValidatePropertyIsDeprecatedIfNecessary(false, EVariableCategory::Member, PropertyBase, EUHTPropertyType::None, CPF_None);
|
|
|
|
if (TopNest->NestType != ENestType::FunctionDeclaration)
|
|
{
|
|
if (NewProperties.Num())
|
|
{
|
|
Throwf(TEXT("Comma delimited properties cannot be converted %s.%s\n"), *StructDef.GetName(), *NewPropDef.GetName());
|
|
}
|
|
}
|
|
|
|
NewProperties.Add(&NewPropDef);
|
|
|
|
// we'll need any metadata tags we parsed later on when we call ConvertEOLCommentToTooltip() so the tags aren't clobbered
|
|
OriginalPropertyBase.MetaData = PropertyBase.MetaData;
|
|
|
|
if (LayoutMacroType != ELayoutMacroType::None || !MatchSymbol(TEXT(',')))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Optional member initializer.
|
|
if (LayoutMacroType == ELayoutMacroType::FieldInitialized)
|
|
{
|
|
// Skip past the specified member initializer; we make no attempt to parse it
|
|
FToken SkipToken;
|
|
int Nesting = 1;
|
|
while (GetToken(SkipToken))
|
|
{
|
|
if (SkipToken.IsSymbol(TEXT('(')))
|
|
{
|
|
++Nesting;
|
|
}
|
|
else if (SkipToken.IsSymbol(TEXT(')')))
|
|
{
|
|
--Nesting;
|
|
if (Nesting == 0)
|
|
{
|
|
UngetToken(SkipToken);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (MatchSymbol(TEXT('=')))
|
|
{
|
|
// Skip past the specified member initializer; we make no attempt to parse it
|
|
FToken SkipToken;
|
|
while (GetToken(SkipToken))
|
|
{
|
|
if (SkipToken.IsSymbol(TEXT(';')))
|
|
{
|
|
// went too far
|
|
UngetToken(SkipToken);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// Using Brace Initialization
|
|
else if (MatchSymbol(TEXT('{')))
|
|
{
|
|
FToken SkipToken;
|
|
int BraceLevel = 1;
|
|
while (GetToken(SkipToken))
|
|
{
|
|
if (SkipToken.IsSymbol(TEXT('{')))
|
|
{
|
|
++BraceLevel;
|
|
}
|
|
else if (SkipToken.IsSymbol(TEXT('}')))
|
|
{
|
|
--BraceLevel;
|
|
if (BraceLevel == 0)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (LayoutMacroType == ELayoutMacroType::None)
|
|
{
|
|
// Expect a semicolon.
|
|
RequireSymbol(TEXT(';'), TEXT("'variable declaration'"));
|
|
}
|
|
else
|
|
{
|
|
// Expect a close bracket.
|
|
RequireSymbol(TEXT(')'), GLayoutMacroNames[(int32)LayoutMacroType]);
|
|
}
|
|
|
|
// Skip redundant semi-colons
|
|
for (;;)
|
|
{
|
|
int32 CurrInputPos = InputPos;
|
|
int32 CurrInputLine = InputLine;
|
|
|
|
FToken Token;
|
|
if (!GetToken(Token, /*bNoConsts=*/ true))
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (!Token.IsSymbol(TEXT(';')))
|
|
{
|
|
InputPos = CurrInputPos;
|
|
InputLine = CurrInputLine;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// Compile a statement: Either a declaration or a command.
|
|
// Returns 1 if success, 0 if end of file.
|
|
//
|
|
bool FHeaderParser::CompileStatement(TArray<FUnrealFunctionDefinitionInfo*>& DelegatesToFixup)
|
|
{
|
|
// Get a token and compile it.
|
|
FToken Token;
|
|
if( !GetToken(Token, true) )
|
|
{
|
|
// End of file.
|
|
return false;
|
|
}
|
|
else if (!CompileDeclaration(DelegatesToFixup, Token))
|
|
{
|
|
Throwf(TEXT("'%s': Bad command or expression"), *Token.GetTokenValue() );
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------
|
|
Code skipping.
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
/**
|
|
* Skip over code, honoring { and } pairs.
|
|
*
|
|
* @param NestCount number of nest levels to consume. if 0, consumes a single statement
|
|
* @param ErrorTag text to use in error message if EOF is encountered before we've done
|
|
*/
|
|
void FHeaderParser::SkipStatements( int32 NestCount, const TCHAR* ErrorTag )
|
|
{
|
|
FToken Token;
|
|
|
|
int32 OriginalNestCount = NestCount;
|
|
|
|
while( GetToken( Token, true ) )
|
|
{
|
|
if ( Token.IsSymbol(TEXT('{')) )
|
|
{
|
|
NestCount++;
|
|
}
|
|
else if ( Token.IsSymbol(TEXT('}')) )
|
|
{
|
|
NestCount--;
|
|
}
|
|
else if ( Token.IsSymbol(TEXT(';')) && OriginalNestCount == 0 )
|
|
{
|
|
break;
|
|
}
|
|
|
|
if ( NestCount < OriginalNestCount || NestCount < 0 )
|
|
break;
|
|
}
|
|
|
|
if( NestCount > 0 )
|
|
{
|
|
Throwf(TEXT("Unexpected end of file at end of %s"), ErrorTag );
|
|
}
|
|
else if ( NestCount < 0 )
|
|
{
|
|
Throwf(TEXT("Extraneous closing brace found in %s"), ErrorTag);
|
|
}
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------
|
|
Main script compiling routine.
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
// Parse Class's annotated headers and optionally its child classes.
|
|
static const FString ObjectHeader(TEXT("NoExportTypes.h"));
|
|
|
|
//
|
|
// Parses the header associated with the specified class.
|
|
// Returns result enumeration.
|
|
//
|
|
void FHeaderParser::ParseHeader()
|
|
{
|
|
// Reset the parser to begin a new class
|
|
bEncounteredNewStyleClass_UnmatchedBrackets = false;
|
|
bSpottedAutogeneratedHeaderInclude = false;
|
|
bHaveSeenUClass = false;
|
|
bClassHasGeneratedBody = false;
|
|
bClassHasGeneratedUInterfaceBody = false;
|
|
bClassHasGeneratedIInterfaceBody = false;
|
|
|
|
// Message.
|
|
UE_LOG(LogCompile, Verbose, TEXT("Parsing %s"), *SourceFile.GetFilename());
|
|
|
|
// Make sure that all of the requried include files are added to the outer scope
|
|
{
|
|
TArray<FUnrealSourceFile*> SourceFilesRequired;
|
|
for (FHeaderProvider& Include : SourceFile.GetIncludes())
|
|
{
|
|
if (Include.GetId() == ObjectHeader)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (FUnrealSourceFile* DepFile = Include.Resolve(SourceFile))
|
|
{
|
|
SourceFilesRequired.Add(DepFile);
|
|
}
|
|
}
|
|
|
|
for (const TSharedRef<FUnrealTypeDefinitionInfo>& ClassDataPair : SourceFile.GetDefinedClasses())
|
|
{
|
|
FUnrealClassDefinitionInfo& ClassDef = ClassDataPair->AsClassChecked();
|
|
for (FUnrealClassDefinitionInfo* ParentClassDef = ClassDef.GetSuperClass(); ParentClassDef; ParentClassDef = ParentClassDef->GetSuperClass())
|
|
{
|
|
if (ParentClassDef->IsParsed() || ParentClassDef->HasAnyClassFlags(CLASS_Intrinsic))
|
|
{
|
|
break;
|
|
}
|
|
SourceFilesRequired.Add(&ParentClassDef->GetUnrealSourceFile());
|
|
}
|
|
}
|
|
|
|
for (FUnrealSourceFile* RequiredFile : SourceFilesRequired)
|
|
{
|
|
SourceFile.GetScope()->IncludeScope(&RequiredFile->GetScope().Get());
|
|
}
|
|
}
|
|
|
|
// Init compiler variables.
|
|
ResetParser(*SourceFile.GetContent());
|
|
|
|
// Init nesting.
|
|
NestLevel = 0;
|
|
TopNest = NULL;
|
|
PushNest(ENestType::GlobalScope, nullptr, &SourceFile);
|
|
|
|
// C++ classes default to private access level
|
|
CurrentAccessSpecifier = ACCESS_Private;
|
|
|
|
// Try to compile it, and catch any errors.
|
|
bool bEmptyFile = true;
|
|
|
|
// Tells if this header defines no-export classes only.
|
|
bool bNoExportClassesOnly = true;
|
|
|
|
// Parse entire program.
|
|
TArray<FUnrealFunctionDefinitionInfo*> DelegatesToFixup;
|
|
while (CompileStatement(DelegatesToFixup))
|
|
{
|
|
bEmptyFile = false;
|
|
|
|
// Clear out the previous comment in anticipation of the next statement.
|
|
ClearComment();
|
|
StatementsParsed++;
|
|
}
|
|
|
|
PopNest(ENestType::GlobalScope, TEXT("Global scope"));
|
|
|
|
// now validate all delegate variables declared in the class
|
|
{
|
|
auto ScopeTypeIterator = SourceFile.GetScope()->GetTypeIterator();
|
|
while (ScopeTypeIterator.MoveNext())
|
|
{
|
|
FUnrealFieldDefinitionInfo* TypeDef = *ScopeTypeIterator;
|
|
if (TypeDef->AsScriptStruct() != nullptr || TypeDef->AsClass() != nullptr)
|
|
{
|
|
TMap<FName, FUnrealFunctionDefinitionInfo*> DelegateCache;
|
|
FixupDelegateProperties(TypeDef->AsStructChecked(), *TypeDef->GetScope(), DelegateCache);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fix up any delegates themselves, if they refer to other delegates
|
|
{
|
|
TMap<FName, FUnrealFunctionDefinitionInfo*> DelegateCache;
|
|
for (FUnrealFunctionDefinitionInfo* DelegateDef : DelegatesToFixup)
|
|
{
|
|
FixupDelegateProperties(*DelegateDef, SourceFile.GetScope().Get(), DelegateCache);
|
|
}
|
|
}
|
|
|
|
// Precompute info for runtime optimization.
|
|
LinesParsed += InputLine;
|
|
|
|
if (RPCsNeedingHookup.Num() > 0)
|
|
{
|
|
FString ErrorMsg(TEXT("Request functions missing response pairs:\r\n"));
|
|
for (TMap<int32, FString>::TConstIterator It(RPCsNeedingHookup); It; ++It)
|
|
{
|
|
ErrorMsg += FString::Printf(TEXT("%s missing id %d\r\n"), *It.Value(), It.Key());
|
|
}
|
|
|
|
RPCsNeedingHookup.Empty();
|
|
Throwf(TEXT("%s"), *ErrorMsg);
|
|
}
|
|
|
|
// Make sure the compilation ended with valid nesting.
|
|
if (bEncounteredNewStyleClass_UnmatchedBrackets)
|
|
{
|
|
Throwf(TEXT("Missing } at end of class") );
|
|
}
|
|
|
|
if (NestLevel == 1)
|
|
{
|
|
Throwf(TEXT("Internal nest inconsistency") );
|
|
}
|
|
else if (NestLevel > 2)
|
|
{
|
|
Throwf(TEXT("Unexpected end of script in '%s' block"), NestTypeName(TopNest->NestType) );
|
|
}
|
|
|
|
for (TSharedRef<FUnrealTypeDefinitionInfo>& TypeDef : SourceFile.GetDefinedClasses())
|
|
{
|
|
FUnrealClassDefinitionInfo& ClassDef = UHTCastChecked<FUnrealClassDefinitionInfo>(TypeDef);
|
|
PostParsingClassSetup(ClassDef);
|
|
|
|
bNoExportClassesOnly = bNoExportClassesOnly && ClassDef.IsNoExport();
|
|
}
|
|
|
|
if (!bSpottedAutogeneratedHeaderInclude && !bEmptyFile && !bNoExportClassesOnly)
|
|
{
|
|
const FString& ExpectedHeaderName = SourceFile.GetGeneratedHeaderFilename();
|
|
Throwf(TEXT("Expected an include at the top of the header: '#include \"%s\"'"), *ExpectedHeaderName);
|
|
}
|
|
|
|
for (const TSharedRef<FUnrealTypeDefinitionInfo>& ClassDef : SourceFile.GetDefinedClasses())
|
|
{
|
|
ClassDef->AsClassChecked().MarkParsed();
|
|
}
|
|
|
|
// Perform any final concurrent finalization
|
|
for (const TSharedRef<FUnrealTypeDefinitionInfo>& TypeDef : SourceFile.GetDefinedTypes())
|
|
{
|
|
TypeDef->ConcurrentPostParseFinalize();
|
|
}
|
|
|
|
// Remember stats about this file
|
|
SourceFile.SetLinesParsed(LinesParsed);
|
|
SourceFile.SetStatementsParsed(StatementsParsed);
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------
|
|
Global functions.
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
FHeaderParser::FHeaderParser(FUnrealPackageDefinitionInfo& InPackageDef, FUnrealSourceFile& InSourceFile)
|
|
: FBaseParser(InSourceFile)
|
|
, Filename(IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*InSourceFile.GetFilename()))
|
|
, PackageDef(InPackageDef)
|
|
{
|
|
|
|
const FManifestModule& Module = PackageDef.GetModule();
|
|
|
|
// Determine if the current module is part of the engine or a game (we are more strict about things for Engine modules)
|
|
switch (Module.ModuleType)
|
|
{
|
|
case EBuildModuleType::Program:
|
|
{
|
|
const FString AbsoluteEngineDir = FPaths::ConvertRelativePathToFull(FPaths::EngineDir());
|
|
const FString ModuleDir = FPaths::ConvertRelativePathToFull(Module.BaseDirectory);
|
|
bIsCurrentModulePartOfEngine = ModuleDir.StartsWith(AbsoluteEngineDir);
|
|
}
|
|
break;
|
|
case EBuildModuleType::EngineRuntime:
|
|
case EBuildModuleType::EngineUncooked:
|
|
case EBuildModuleType::EngineDeveloper:
|
|
case EBuildModuleType::EngineEditor:
|
|
case EBuildModuleType::EngineThirdParty:
|
|
bIsCurrentModulePartOfEngine = true;
|
|
break;
|
|
case EBuildModuleType::GameRuntime:
|
|
case EBuildModuleType::GameUncooked:
|
|
case EBuildModuleType::GameDeveloper:
|
|
case EBuildModuleType::GameEditor:
|
|
case EBuildModuleType::GameThirdParty:
|
|
bIsCurrentModulePartOfEngine = false;
|
|
break;
|
|
default:
|
|
bIsCurrentModulePartOfEngine = true;
|
|
check(false);
|
|
}
|
|
}
|
|
|
|
// Throws if a specifier value wasn't provided
|
|
void FHeaderParser::RequireSpecifierValue(const FUHTMessageProvider& Context, const FPropertySpecifier& Specifier, bool bRequireExactlyOne)
|
|
{
|
|
if (Specifier.Values.Num() == 0)
|
|
{
|
|
Context.Throwf(TEXT("The specifier '%s' must be given a value"), *Specifier.Key);
|
|
}
|
|
else if ((Specifier.Values.Num() != 1) && bRequireExactlyOne)
|
|
{
|
|
Context.Throwf(TEXT("The specifier '%s' must be given exactly one value"), *Specifier.Key);
|
|
}
|
|
}
|
|
|
|
// Throws if a specifier value wasn't provided
|
|
FString FHeaderParser::RequireExactlyOneSpecifierValue(const FUHTMessageProvider& Context, const FPropertySpecifier& Specifier)
|
|
{
|
|
RequireSpecifierValue(Context, Specifier, /*bRequireExactlyOne*/ true);
|
|
return Specifier.Values[0];
|
|
}
|
|
|
|
void FHeaderParser::Parse(
|
|
FUnrealPackageDefinitionInfo& PackageDef,
|
|
FUnrealSourceFile& SourceFile)
|
|
{
|
|
SCOPE_SECONDS_COUNTER_UHT(Parse);
|
|
FHeaderParser(PackageDef, SourceFile).ParseHeader();
|
|
}
|
|
|
|
/**
|
|
* Returns True if the given class name includes a valid Unreal prefix and matches up with the given original class.
|
|
*
|
|
* @param InNameToCheck - Name w/ potential prefix to check
|
|
* @param OriginalClassName - Name of class w/ no prefix to check against
|
|
*/
|
|
bool FHeaderParser::ClassNameHasValidPrefix(const FString& InNameToCheck, const FString& OriginalClassName)
|
|
{
|
|
bool bIsLabledDeprecated;
|
|
const FString ClassPrefix = GetClassPrefix( InNameToCheck, bIsLabledDeprecated );
|
|
|
|
// If the class is labeled deprecated, don't try to resolve it during header generation, valid results can't be guaranteed.
|
|
if (bIsLabledDeprecated)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (ClassPrefix.IsEmpty())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FString TestString = FString::Printf(TEXT("%s%s"), *ClassPrefix, *OriginalClassName);
|
|
|
|
const bool bNamesMatch = ( InNameToCheck == *TestString );
|
|
|
|
return bNamesMatch;
|
|
}
|
|
|
|
void FHeaderParser::ParseClassName(const TCHAR* Temp, FString& ClassName)
|
|
{
|
|
// Skip leading whitespace
|
|
while (FChar::IsWhitespace(*Temp))
|
|
{
|
|
++Temp;
|
|
}
|
|
|
|
// Run thru characters (note: relying on later code to reject the name for a leading number, etc...)
|
|
const TCHAR* StringStart = Temp;
|
|
while (FChar::IsAlnum(*Temp) || FChar::IsUnderscore(*Temp))
|
|
{
|
|
++Temp;
|
|
}
|
|
|
|
ClassName = FString(UE_PTRDIFF_TO_INT32(Temp - StringStart), StringStart);
|
|
if (ClassName.EndsWith(TEXT("_API"), ESearchCase::CaseSensitive))
|
|
{
|
|
// RequiresAPI token for a given module
|
|
|
|
//@TODO: UCREMOVAL: Validate the module name
|
|
FString RequiresAPISymbol = ClassName;
|
|
|
|
// Now get the real class name
|
|
ClassName.Empty();
|
|
ParseClassName(Temp, ClassName);
|
|
}
|
|
}
|
|
|
|
enum class EBlockDirectiveType
|
|
{
|
|
// We're in a CPP block
|
|
CPPBlock,
|
|
|
|
// We're in a !CPP block
|
|
NotCPPBlock,
|
|
|
|
// We're in a 0 block
|
|
ZeroBlock,
|
|
|
|
// We're in a 1 block
|
|
OneBlock,
|
|
|
|
// We're in a WITH_HOT_RELOAD block
|
|
WithHotReload,
|
|
|
|
// We're in a WITH_EDITOR block
|
|
WithEditor,
|
|
|
|
// We're in a WITH_EDITORONLY_DATA block
|
|
WithEditorOnlyData,
|
|
|
|
// We're in a block with an unrecognized directive
|
|
UnrecognizedBlock
|
|
};
|
|
|
|
bool ShouldKeepBlockContents(EBlockDirectiveType DirectiveType)
|
|
{
|
|
switch (DirectiveType)
|
|
{
|
|
case EBlockDirectiveType::NotCPPBlock:
|
|
case EBlockDirectiveType::OneBlock:
|
|
case EBlockDirectiveType::WithHotReload:
|
|
case EBlockDirectiveType::WithEditor:
|
|
case EBlockDirectiveType::WithEditorOnlyData:
|
|
return true;
|
|
|
|
case EBlockDirectiveType::CPPBlock:
|
|
case EBlockDirectiveType::ZeroBlock:
|
|
case EBlockDirectiveType::UnrecognizedBlock:
|
|
return false;
|
|
}
|
|
|
|
check(false);
|
|
UE_ASSUME(false);
|
|
}
|
|
|
|
bool ShouldKeepDirective(EBlockDirectiveType DirectiveType)
|
|
{
|
|
switch (DirectiveType)
|
|
{
|
|
case EBlockDirectiveType::WithHotReload:
|
|
case EBlockDirectiveType::WithEditor:
|
|
case EBlockDirectiveType::WithEditorOnlyData:
|
|
return true;
|
|
|
|
case EBlockDirectiveType::CPPBlock:
|
|
case EBlockDirectiveType::NotCPPBlock:
|
|
case EBlockDirectiveType::ZeroBlock:
|
|
case EBlockDirectiveType::OneBlock:
|
|
case EBlockDirectiveType::UnrecognizedBlock:
|
|
return false;
|
|
}
|
|
|
|
check(false);
|
|
UE_ASSUME(false);
|
|
}
|
|
|
|
EBlockDirectiveType ParseCommandToBlockDirectiveType(const TCHAR** Str)
|
|
{
|
|
if (FParse::Command(Str, TEXT("0")))
|
|
{
|
|
return EBlockDirectiveType::ZeroBlock;
|
|
}
|
|
|
|
if (FParse::Command(Str, TEXT("1")))
|
|
{
|
|
return EBlockDirectiveType::OneBlock;
|
|
}
|
|
|
|
if (FParse::Command(Str, TEXT("CPP")))
|
|
{
|
|
return EBlockDirectiveType::CPPBlock;
|
|
}
|
|
|
|
if (FParse::Command(Str, TEXT("!CPP")))
|
|
{
|
|
return EBlockDirectiveType::NotCPPBlock;
|
|
}
|
|
|
|
if (FParse::Command(Str, TEXT("WITH_HOT_RELOAD")))
|
|
{
|
|
return EBlockDirectiveType::WithHotReload;
|
|
}
|
|
|
|
if (FParse::Command(Str, TEXT("WITH_EDITOR")))
|
|
{
|
|
return EBlockDirectiveType::WithEditor;
|
|
}
|
|
|
|
if (FParse::Command(Str, TEXT("WITH_EDITORONLY_DATA")))
|
|
{
|
|
return EBlockDirectiveType::WithEditorOnlyData;
|
|
}
|
|
|
|
return EBlockDirectiveType::UnrecognizedBlock;
|
|
}
|
|
|
|
const TCHAR* GetBlockDirectiveTypeString(EBlockDirectiveType DirectiveType)
|
|
{
|
|
switch (DirectiveType)
|
|
{
|
|
case EBlockDirectiveType::CPPBlock: return TEXT("CPP");
|
|
case EBlockDirectiveType::NotCPPBlock: return TEXT("!CPP");
|
|
case EBlockDirectiveType::ZeroBlock: return TEXT("0");
|
|
case EBlockDirectiveType::OneBlock: return TEXT("1");
|
|
case EBlockDirectiveType::WithHotReload: return TEXT("WITH_HOT_RELOAD");
|
|
case EBlockDirectiveType::WithEditor: return TEXT("WITH_EDITOR");
|
|
case EBlockDirectiveType::WithEditorOnlyData: return TEXT("WITH_EDITORONLY_DATA");
|
|
case EBlockDirectiveType::UnrecognizedBlock: return TEXT("<unrecognized>");
|
|
}
|
|
|
|
check(false);
|
|
UE_ASSUME(false);
|
|
}
|
|
|
|
// Performs a preliminary parse of the text in the specified buffer, pulling out useful information for the header generation process
|
|
void FHeaderParser::SimplifiedClassParse(FUnrealSourceFile& SourceFile, const TCHAR* InBuffer, FStringOutputDevice& ClassHeaderTextStrippedOfCppText)
|
|
{
|
|
FHeaderPreParser Parser(SourceFile);
|
|
FString StrLine;
|
|
FString ClassName;
|
|
FString BaseClassName;
|
|
|
|
// Two passes, preprocessor, then looking for the class stuff
|
|
|
|
// The layer of multi-line comment we are in.
|
|
int32 CurrentLine = 0;
|
|
const TCHAR* Buffer = InBuffer;
|
|
|
|
// Preprocessor pass
|
|
while (FParse::Line(&Buffer, StrLine, true))
|
|
{
|
|
CurrentLine++;
|
|
const TCHAR* Str = *StrLine;
|
|
int32 BraceCount = 0;
|
|
|
|
bool bIf = FParse::Command(&Str,TEXT("#if"));
|
|
if( bIf || FParse::Command(&Str,TEXT("#ifdef")) || FParse::Command(&Str,TEXT("#ifndef")) )
|
|
{
|
|
EBlockDirectiveType RootDirective;
|
|
if (bIf)
|
|
{
|
|
RootDirective = ParseCommandToBlockDirectiveType(&Str);
|
|
}
|
|
else
|
|
{
|
|
// #ifdef or #ifndef are always treated as CPP
|
|
RootDirective = EBlockDirectiveType::UnrecognizedBlock;
|
|
}
|
|
|
|
TArray<EBlockDirectiveType, TInlineAllocator<8>> DirectiveStack;
|
|
DirectiveStack.Push(RootDirective);
|
|
|
|
bool bShouldKeepBlockContents = ShouldKeepBlockContents(RootDirective);
|
|
bool bIsZeroBlock = RootDirective == EBlockDirectiveType::ZeroBlock;
|
|
|
|
ClassHeaderTextStrippedOfCppText.Logf(TEXT("%s\r\n"), ShouldKeepDirective(RootDirective) ? *StrLine : TEXT(""));
|
|
|
|
while ((DirectiveStack.Num() > 0) && FParse::Line(&Buffer, StrLine, 1))
|
|
{
|
|
CurrentLine++;
|
|
Str = *StrLine;
|
|
|
|
bool bShouldKeepLine = bShouldKeepBlockContents;
|
|
|
|
bool bIsDirective = false;
|
|
if( FParse::Command(&Str,TEXT("#endif")) )
|
|
{
|
|
EBlockDirectiveType OldDirective = DirectiveStack.Pop();
|
|
|
|
bShouldKeepLine &= ShouldKeepDirective(OldDirective);
|
|
bIsDirective = true;
|
|
}
|
|
else if( FParse::Command(&Str,TEXT("#if")) || FParse::Command(&Str,TEXT("#ifdef")) || FParse::Command(&Str,TEXT("#ifndef")) )
|
|
{
|
|
EBlockDirectiveType Directive = ParseCommandToBlockDirectiveType(&Str);
|
|
DirectiveStack.Push(Directive);
|
|
|
|
bShouldKeepLine &= ShouldKeepDirective(Directive);
|
|
bIsDirective = true;
|
|
}
|
|
else if (FParse::Command(&Str,TEXT("#elif")))
|
|
{
|
|
EBlockDirectiveType NewDirective = ParseCommandToBlockDirectiveType(&Str);
|
|
EBlockDirectiveType OldDirective = DirectiveStack.Top();
|
|
|
|
// Check to see if we're mixing ignorable directive types - we don't support this
|
|
bool bKeepNewDirective = ShouldKeepDirective(NewDirective);
|
|
bool bKeepOldDirective = ShouldKeepDirective(OldDirective);
|
|
if (bKeepNewDirective != bKeepOldDirective)
|
|
{
|
|
FUHTMessage(SourceFile, CurrentLine).Throwf(
|
|
TEXT("Mixing %s with %s in an #elif preprocessor block is not supported"),
|
|
GetBlockDirectiveTypeString(OldDirective),
|
|
GetBlockDirectiveTypeString(NewDirective)
|
|
);
|
|
}
|
|
|
|
DirectiveStack.Top() = NewDirective;
|
|
|
|
bShouldKeepLine &= bKeepNewDirective;
|
|
bIsDirective = true;
|
|
}
|
|
else if (FParse::Command(&Str, TEXT("#else")))
|
|
{
|
|
switch (DirectiveStack.Top())
|
|
{
|
|
case EBlockDirectiveType::ZeroBlock:
|
|
DirectiveStack.Top() = EBlockDirectiveType::OneBlock;
|
|
break;
|
|
|
|
case EBlockDirectiveType::OneBlock:
|
|
DirectiveStack.Top() = EBlockDirectiveType::ZeroBlock;
|
|
break;
|
|
|
|
case EBlockDirectiveType::CPPBlock:
|
|
DirectiveStack.Top() = EBlockDirectiveType::NotCPPBlock;
|
|
break;
|
|
|
|
case EBlockDirectiveType::NotCPPBlock:
|
|
DirectiveStack.Top() = EBlockDirectiveType::CPPBlock;
|
|
break;
|
|
|
|
case EBlockDirectiveType::WithHotReload:
|
|
FUHTMessage(SourceFile, CurrentLine).Throwf(TEXT("Bad preprocessor directive in metadata declaration: %s; Only 'CPP', '1' and '0' can have #else directives"), *ClassName);
|
|
|
|
case EBlockDirectiveType::UnrecognizedBlock:
|
|
case EBlockDirectiveType::WithEditor:
|
|
case EBlockDirectiveType::WithEditorOnlyData:
|
|
// We allow unrecognized directives, WITH_EDITOR and WITH_EDITORONLY_DATA to have #else blocks.
|
|
// However, we don't actually change how UHT processes these #else blocks.
|
|
break;
|
|
}
|
|
|
|
bShouldKeepLine &= ShouldKeepDirective(DirectiveStack.Top());
|
|
bIsDirective = true;
|
|
}
|
|
else
|
|
{
|
|
// Check for UHT identifiers inside skipped blocks, unless it's a zero block, because the compiler is going to skip those anyway.
|
|
if (!bShouldKeepBlockContents && !bIsZeroBlock)
|
|
{
|
|
auto FindInitialStr = [](const TCHAR*& FoundSubstr, const FString& StrToSearch, const TCHAR* ConstructName) -> bool
|
|
{
|
|
if (StrToSearch.StartsWith(ConstructName, ESearchCase::CaseSensitive))
|
|
{
|
|
FoundSubstr = ConstructName;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
FString TrimmedStrLine = StrLine;
|
|
TrimmedStrLine.TrimStartInline();
|
|
|
|
const TCHAR* FoundSubstr = nullptr;
|
|
if (FindInitialStr(FoundSubstr, TrimmedStrLine, TEXT("UPROPERTY"))
|
|
|| FindInitialStr(FoundSubstr, TrimmedStrLine, TEXT("UCLASS"))
|
|
|| FindInitialStr(FoundSubstr, TrimmedStrLine, TEXT("USTRUCT"))
|
|
|| FindInitialStr(FoundSubstr, TrimmedStrLine, TEXT("UENUM"))
|
|
|| FindInitialStr(FoundSubstr, TrimmedStrLine, TEXT("UINTERFACE"))
|
|
|| FindInitialStr(FoundSubstr, TrimmedStrLine, TEXT("UDELEGATE"))
|
|
|| FindInitialStr(FoundSubstr, TrimmedStrLine, TEXT("UFUNCTION")))
|
|
{
|
|
FUHTMessage(SourceFile, CurrentLine).Throwf(TEXT("%s must not be inside preprocessor blocks, except for WITH_EDITORONLY_DATA"), FoundSubstr);
|
|
}
|
|
|
|
// Try and determine if this line contains something like a serialize function
|
|
if (TrimmedStrLine.Len() > 0)
|
|
{
|
|
static const FString Str_Void = TEXT("void");
|
|
static const FString Str_Serialize = TEXT("Serialize(");
|
|
static const FString Str_FArchive = TEXT("FArchive");
|
|
static const FString Str_FStructuredArchive = TEXT("FStructuredArchive::FSlot");
|
|
|
|
int32 Pos = 0;
|
|
if ((Pos = TrimmedStrLine.Find(Str_Void, ESearchCase::CaseSensitive, ESearchDir::FromStart, Pos)) != -1)
|
|
{
|
|
Pos += Str_Void.Len();
|
|
if ((Pos = TrimmedStrLine.Find(Str_Serialize, ESearchCase::CaseSensitive, ESearchDir::FromStart, Pos)) != -1)
|
|
{
|
|
Pos += Str_Serialize.Len();
|
|
|
|
if (((TrimmedStrLine.Find(Str_FArchive, ESearchCase::CaseSensitive, ESearchDir::FromStart, Pos)) != -1) ||
|
|
((TrimmedStrLine.Find(Str_FStructuredArchive, ESearchCase::CaseSensitive, ESearchDir::FromStart, Pos)) != -1))
|
|
{
|
|
FUHTMessage(SourceFile, CurrentLine).Throwf(TEXT("'%s' must not be inside preprocessor blocks, except for WITH_EDITORONLY_DATA"), *TrimmedStrLine);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ClassHeaderTextStrippedOfCppText.Logf(TEXT("%s\r\n"), bShouldKeepLine ? *StrLine : TEXT(""));
|
|
|
|
if (bIsDirective)
|
|
{
|
|
bShouldKeepBlockContents = Algo::AllOf(DirectiveStack, &ShouldKeepBlockContents);
|
|
bIsZeroBlock = DirectiveStack.Contains(EBlockDirectiveType::ZeroBlock);
|
|
}
|
|
}
|
|
}
|
|
else if ( FParse::Command(&Str,TEXT("#include")) )
|
|
{
|
|
ClassHeaderTextStrippedOfCppText.Logf( TEXT("%s\r\n"), *StrLine );
|
|
}
|
|
else
|
|
{
|
|
ClassHeaderTextStrippedOfCppText.Logf( TEXT("%s\r\n"), *StrLine );
|
|
}
|
|
}
|
|
|
|
// now start over go look for the class
|
|
|
|
bool bInComment = false;
|
|
CurrentLine = 0;
|
|
Buffer = *ClassHeaderTextStrippedOfCppText;
|
|
|
|
bool bFoundGeneratedInclude = false;
|
|
bool bGeneratedIncludeRequired = false;
|
|
|
|
for (const TCHAR* StartOfLine = Buffer; FParse::Line(&Buffer, StrLine, true); StartOfLine = Buffer)
|
|
{
|
|
CurrentLine++;
|
|
|
|
const TCHAR* Str = *StrLine;
|
|
bool bProcess = !bInComment; // for skipping nested multi-line comments
|
|
|
|
int32 BraceCount = 0;
|
|
if( bProcess && FParse::Command(&Str,TEXT("#if")) )
|
|
{
|
|
}
|
|
else if ( bProcess && FParse::Command(&Str,TEXT("#include")) )
|
|
{
|
|
// Handle #include directives as if they were 'dependson' keywords.
|
|
const FString& DependsOnHeaderName = Str;
|
|
|
|
if (DependsOnHeaderName != TEXT("\"UObject/DefineUPropertyMacros.h\"") && DependsOnHeaderName != TEXT("\"UObject/UndefineUPropertyMacros.h\""))
|
|
{
|
|
if (bFoundGeneratedInclude)
|
|
{
|
|
FUHTMessage(SourceFile, CurrentLine).Throwf(TEXT("#include found after .generated.h file - the .generated.h file should always be the last #include in a header"));
|
|
}
|
|
|
|
bFoundGeneratedInclude = DependsOnHeaderName.Contains(TEXT(".generated.h"));
|
|
if (!bFoundGeneratedInclude && DependsOnHeaderName.Len())
|
|
{
|
|
bool bIsQuotedInclude = DependsOnHeaderName[0] == '\"';
|
|
int32 HeaderFilenameEnd = DependsOnHeaderName.Find(bIsQuotedInclude ? TEXT("\"") : TEXT(">"), ESearchCase::CaseSensitive, ESearchDir::FromStart, 1);
|
|
|
|
if (HeaderFilenameEnd != INDEX_NONE)
|
|
{
|
|
// Include the extension in the name so that we later know where this entry came from.
|
|
SourceFile.GetIncludes().AddUnique(FHeaderProvider(EHeaderProviderSourceType::FileName, FPaths::GetCleanFilename(DependsOnHeaderName.Mid(1, HeaderFilenameEnd - 1))));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if ( bProcess && FParse::Command(&Str,TEXT("#else")) )
|
|
{
|
|
}
|
|
else if ( bProcess && FParse::Command(&Str,TEXT("#elif")) )
|
|
{
|
|
}
|
|
else if ( bProcess && FParse::Command(&Str,TEXT("#endif")) )
|
|
{
|
|
}
|
|
else
|
|
{
|
|
int32 Pos = INDEX_NONE;
|
|
int32 EndPos = INDEX_NONE;
|
|
int32 StrBegin = INDEX_NONE;
|
|
int32 StrEnd = INDEX_NONE;
|
|
|
|
bool bEscaped = false;
|
|
for ( int32 CharPos = 0; CharPos < StrLine.Len(); CharPos++ )
|
|
{
|
|
if ( bEscaped )
|
|
{
|
|
bEscaped = false;
|
|
}
|
|
else if ( StrLine[CharPos] == TEXT('\\') )
|
|
{
|
|
bEscaped = true;
|
|
}
|
|
else if ( StrLine[CharPos] == TEXT('\"') )
|
|
{
|
|
if ( StrBegin == INDEX_NONE )
|
|
{
|
|
StrBegin = CharPos;
|
|
}
|
|
else
|
|
{
|
|
StrEnd = CharPos;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find the first '/' and check for '//' or '/*' or '*/'
|
|
if (StrLine.FindChar(TEXT('/'), Pos))
|
|
{
|
|
if (Pos >= 0)
|
|
{
|
|
// Stub out the comments, ignoring anything inside literal strings.
|
|
Pos = StrLine.Find(TEXT("//"), ESearchCase::CaseSensitive, ESearchDir::FromStart, Pos);
|
|
|
|
// Check if first slash is end of multiline comment and adjust position if necessary.
|
|
if (Pos > 0 && StrLine[Pos - 1] == TEXT('*'))
|
|
{
|
|
++Pos;
|
|
}
|
|
|
|
if (Pos >= 0)
|
|
{
|
|
if (StrBegin == INDEX_NONE || Pos < StrBegin || Pos > StrEnd)
|
|
{
|
|
StrLine.LeftInline(Pos, false);
|
|
}
|
|
|
|
if (StrLine.IsEmpty())
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// look for a / * ... * / block, ignoring anything inside literal strings
|
|
Pos = StrLine.Find(TEXT("/*"), ESearchCase::CaseSensitive, ESearchDir::FromStart, Pos);
|
|
EndPos = StrLine.Find(TEXT("*/"), ESearchCase::CaseSensitive, ESearchDir::FromStart, FMath::Max(0, Pos - 1));
|
|
if (Pos >= 0)
|
|
{
|
|
if (StrBegin == INDEX_NONE || Pos < StrBegin || Pos > StrEnd)
|
|
{
|
|
if (EndPos != INDEX_NONE && (EndPos < StrBegin || EndPos > StrEnd))
|
|
{
|
|
StrLine = StrLine.Left(Pos) + StrLine.Mid(EndPos + 2);
|
|
EndPos = INDEX_NONE;
|
|
bInComment = false;
|
|
}
|
|
else
|
|
{
|
|
StrLine.LeftInline(Pos, false);
|
|
bInComment = true;
|
|
}
|
|
}
|
|
bProcess = !bInComment;
|
|
}
|
|
|
|
if (EndPos >= 0)
|
|
{
|
|
if (StrBegin == INDEX_NONE || EndPos < StrBegin || EndPos > StrEnd)
|
|
{
|
|
StrLine.MidInline(EndPos + 2, MAX_int32, false);
|
|
bInComment = false;
|
|
}
|
|
bProcess = !bInComment;
|
|
}
|
|
}
|
|
}
|
|
|
|
StrLine.TrimStartInline();
|
|
if (!bProcess || StrLine.IsEmpty())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Str = *StrLine;
|
|
|
|
// Get class or interface name
|
|
if (const TCHAR* UInterfaceMacroDecl = FCString::Strfind(Str, TEXT("UINTERFACE")))
|
|
{
|
|
if (UInterfaceMacroDecl == FCString::Strspn(Str, TEXT("\t ")) + Str)
|
|
{
|
|
if (UInterfaceMacroDecl[10] != TEXT('('))
|
|
{
|
|
FUHTMessage(SourceFile, CurrentLine).Throwf(TEXT("Missing open parenthesis after UINTERFACE"));
|
|
}
|
|
|
|
TSharedRef<FUnrealTypeDefinitionInfo> ClassDecl = Parser.ParseClassDeclaration(StartOfLine + (UInterfaceMacroDecl - Str), CurrentLine, true, TEXT("UINTERFACE"));
|
|
bGeneratedIncludeRequired |= !ClassDecl->AsClassChecked().IsNoExport();
|
|
SourceFile.AddDefinedClass(MoveTemp(ClassDecl));
|
|
}
|
|
}
|
|
|
|
else if (const TCHAR* UClassMacroDecl = FCString::Strfind(Str, TEXT("UCLASS")))
|
|
{
|
|
if (UClassMacroDecl == FCString::Strspn(Str, TEXT("\t ")) + Str)
|
|
{
|
|
if (UClassMacroDecl[6] != TEXT('('))
|
|
{
|
|
FUHTMessage(SourceFile, CurrentLine).Throwf(TEXT("Missing open parenthesis after UCLASS"));
|
|
}
|
|
|
|
TSharedRef<FUnrealTypeDefinitionInfo> ClassDecl = Parser.ParseClassDeclaration(StartOfLine + (UClassMacroDecl - Str), CurrentLine, false, TEXT("UCLASS"));
|
|
bGeneratedIncludeRequired |= !ClassDecl->AsClassChecked().IsNoExport();
|
|
SourceFile.AddDefinedClass(MoveTemp(ClassDecl));
|
|
}
|
|
}
|
|
|
|
else if (const TCHAR* UEnumMacroDecl = FCString::Strfind(Str, TEXT("UENUM")))
|
|
{
|
|
if (UEnumMacroDecl == FCString::Strspn(Str, TEXT("\t ")) + Str)
|
|
{
|
|
if (UEnumMacroDecl[5] != TEXT('('))
|
|
{
|
|
FUHTMessage(SourceFile, CurrentLine).Throwf(TEXT("Missing open parenthesis after UENUM"));
|
|
}
|
|
|
|
TSharedRef<FUnrealTypeDefinitionInfo> EnumDecl = Parser.ParseEnumDeclaration(StartOfLine + (UEnumMacroDecl - Str), CurrentLine);
|
|
SourceFile.AddDefinedEnum(MoveTemp(EnumDecl));
|
|
}
|
|
}
|
|
|
|
else if (const TCHAR* UDelegateMacroDecl = FCString::Strfind(Str, TEXT("UDELEGATE")))
|
|
{
|
|
if (UDelegateMacroDecl == FCString::Strspn(Str, TEXT("\t ")) + Str)
|
|
{
|
|
if (UDelegateMacroDecl[9] != TEXT('('))
|
|
{
|
|
FUHTMessage(SourceFile, CurrentLine).Throwf(TEXT("Missing open parenthesis after UDELEGATE"));
|
|
}
|
|
// We don't preparse the delegates, but we need to make sure the include is there
|
|
bGeneratedIncludeRequired = true;
|
|
}
|
|
}
|
|
|
|
else if (const TCHAR* UStructMacroDecl = FCString::Strfind(Str, TEXT("USTRUCT")))
|
|
{
|
|
if (UStructMacroDecl == FCString::Strspn(Str, TEXT("\t ")) + Str)
|
|
{
|
|
if (UStructMacroDecl[7] != TEXT('('))
|
|
{
|
|
FUHTMessage(SourceFile, CurrentLine).Throwf(TEXT("Missing open parenthesis after USTRUCT"));
|
|
}
|
|
|
|
TSharedRef<FUnrealTypeDefinitionInfo> StructDecl = Parser.ParseStructDeclaration(StartOfLine + (UStructMacroDecl - Str), CurrentLine);
|
|
bGeneratedIncludeRequired |= !StructDecl->AsScriptStructChecked().HasAnyStructFlags(STRUCT_NoExport);
|
|
SourceFile.AddDefinedStruct(MoveTemp(StructDecl));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bGeneratedIncludeRequired && !bFoundGeneratedInclude)
|
|
{
|
|
Parser.Throwf(TEXT("No #include found for the .generated.h file - the .generated.h file should always be the last #include in a header"));
|
|
}
|
|
}
|
|
|
|
/////////////////////////////////////////////////////
|
|
// FHeaderPreParser
|
|
|
|
TSharedRef<FUnrealTypeDefinitionInfo> FHeaderPreParser::ParseClassDeclaration(const TCHAR* InputText, int32 InLineNumber, bool bClassIsAnInterface, const TCHAR* StartingMatchID)
|
|
{
|
|
const TCHAR* ErrorMsg = TEXT("Class declaration");
|
|
|
|
ResetParser(InputText, InLineNumber);
|
|
|
|
// Require 'UCLASS' or 'UINTERFACE'
|
|
RequireIdentifier(StartingMatchID, ESearchCase::CaseSensitive, ErrorMsg);
|
|
|
|
// New-style UCLASS() syntax
|
|
TMap<FName, FString> MetaData;
|
|
TArray<FPropertySpecifier> SpecifiersFound;
|
|
ReadSpecifierSetInsideMacro(SpecifiersFound, ErrorMsg, MetaData);
|
|
|
|
// Require 'class'
|
|
RequireIdentifier(TEXT("class"), ESearchCase::CaseSensitive, ErrorMsg);
|
|
|
|
SkipAlignasAndDeprecatedMacroIfNecessary(*this);
|
|
|
|
// Read the class name
|
|
FString RequiredAPIMacroIfPresent;
|
|
FString ClassName;
|
|
ParseNameWithPotentialAPIMacroPrefix(/*out*/ ClassName, /*out*/ RequiredAPIMacroIfPresent, StartingMatchID);
|
|
|
|
FString ClassNameWithoutPrefixStr = GetClassNameWithPrefixRemoved(ClassName);
|
|
|
|
if (ClassNameWithoutPrefixStr.IsEmpty())
|
|
{
|
|
Throwf(TEXT("When compiling class definition for '%s', attempting to strip prefix results in an empty name. Did you leave off a prefix?"), *ClassNameWithoutPrefixStr);
|
|
}
|
|
|
|
// Skip optional final keyword
|
|
MatchIdentifier(TEXT("final"), ESearchCase::CaseSensitive);
|
|
|
|
|
|
// Create the definition
|
|
TSharedRef<FUnrealClassDefinitionInfo> ClassDef = MakeShareable(new FUnrealClassDefinitionInfo(SourceFile, InLineNumber, MoveTemp(ClassName), FName(ClassNameWithoutPrefixStr, FNAME_Add), bClassIsAnInterface));
|
|
|
|
// Parse the inheritance list
|
|
ParseInheritance(TEXT("class"), [this, &ClassNameWithoutPrefixStr, &ClassDef = *ClassDef](const TCHAR* ClassName, bool bIsSuperClass)
|
|
{
|
|
FString ClassNameStr(ClassName);
|
|
SourceFile.AddClassIncludeIfNeeded(*this, ClassNameWithoutPrefixStr, ClassNameStr);
|
|
if (bIsSuperClass)
|
|
{
|
|
ClassDef.GetSuperStructInfo().Name = MoveTemp(ClassNameStr);
|
|
}
|
|
else
|
|
{
|
|
ClassDef.GetBaseStructInfos().Emplace(FUnrealStructDefinitionInfo::FBaseStructInfo{ MoveTemp(ClassNameStr) });
|
|
}
|
|
}
|
|
);
|
|
|
|
ClassDef->GetParsedMetaData() = MoveTemp(MetaData);
|
|
ClassDef->ParseClassProperties(MoveTemp(SpecifiersFound), RequiredAPIMacroIfPresent);
|
|
return ClassDef;
|
|
}
|
|
|
|
//
|
|
// Compile an enumeration definition.
|
|
//
|
|
TSharedRef<FUnrealTypeDefinitionInfo> FHeaderPreParser::ParseEnumDeclaration(const TCHAR* InputText, int32 InLineNumber)
|
|
{
|
|
const TCHAR* ErrorMsg = TEXT("Enum declaration");
|
|
|
|
ResetParser(InputText, InLineNumber);
|
|
|
|
// Require 'UCLASS' or 'UINTERFACE'
|
|
RequireIdentifier(TEXT("UENUM"), ESearchCase::CaseSensitive, ErrorMsg);
|
|
|
|
// Get the enum specifier list
|
|
FToken EnumToken;
|
|
FMetaData EnumMetaData;
|
|
TArray<FPropertySpecifier> SpecifiersFound;
|
|
ReadSpecifierSetInsideMacro(SpecifiersFound, ErrorMsg, EnumMetaData);
|
|
|
|
// Check enum type. This can be global 'enum', 'namespace' or 'enum class' enums.
|
|
bool bReadEnumName = false;
|
|
UEnum::ECppForm CppForm = UEnum::ECppForm::Regular;
|
|
if (!GetIdentifier(EnumToken))
|
|
{
|
|
Throwf(TEXT("Missing identifier after UENUM()"));
|
|
}
|
|
|
|
if (EnumToken.IsValue(TEXT("namespace"), ESearchCase::CaseSensitive))
|
|
{
|
|
CppForm = UEnum::ECppForm::Namespaced;
|
|
|
|
SkipDeprecatedMacroIfNecessary(*this);
|
|
|
|
bReadEnumName = GetIdentifier(EnumToken);
|
|
}
|
|
else if (EnumToken.IsValue(TEXT("enum"), ESearchCase::CaseSensitive))
|
|
{
|
|
if (!GetIdentifier(EnumToken))
|
|
{
|
|
Throwf(TEXT("Missing identifier after enum"));
|
|
}
|
|
|
|
if (EnumToken.IsValue(TEXT("class"), ESearchCase::CaseSensitive) || EnumToken.IsValue(TEXT("struct"), ESearchCase::CaseSensitive))
|
|
{
|
|
CppForm = UEnum::ECppForm::EnumClass;
|
|
}
|
|
else
|
|
{
|
|
// Put whatever token we found back so that we can correctly skip below
|
|
UngetToken(EnumToken);
|
|
|
|
CppForm = UEnum::ECppForm::Regular;
|
|
}
|
|
|
|
SkipAlignasAndDeprecatedMacroIfNecessary(*this);
|
|
|
|
bReadEnumName = GetIdentifier(EnumToken);
|
|
}
|
|
else
|
|
{
|
|
Throwf(TEXT("UENUM() should be followed by \'enum\' or \'namespace\' keywords."));
|
|
}
|
|
|
|
// Get enumeration name.
|
|
if (!bReadEnumName)
|
|
{
|
|
Throwf(TEXT("Missing enumeration name"));
|
|
}
|
|
|
|
// Read base for enum class
|
|
EUnderlyingEnumType UnderlyingType = EUnderlyingEnumType::uint8;
|
|
if (CppForm == UEnum::ECppForm::EnumClass)
|
|
{
|
|
UnderlyingType = ParseUnderlyingEnumType();
|
|
}
|
|
|
|
TSharedRef<FUnrealEnumDefinitionInfo> EnumDef = MakeShareable(new FUnrealEnumDefinitionInfo(SourceFile, InLineNumber, FString(EnumToken.Value), FName(EnumToken.Value, FNAME_Add), CppForm, UnderlyingType));
|
|
return EnumDef;
|
|
}
|
|
|
|
//
|
|
// Compile an structure definition.
|
|
//
|
|
TSharedRef<FUnrealTypeDefinitionInfo> FHeaderPreParser::ParseStructDeclaration(const TCHAR* InputText, int32 InLineNumber)
|
|
{
|
|
const TCHAR* ErrorMsg = TEXT("Struct declaration");
|
|
|
|
ResetParser(InputText, InLineNumber);
|
|
|
|
// Require 'UCLASS' or 'UINTERFACE'
|
|
RequireIdentifier(TEXT("USTRUCT"), ESearchCase::CaseSensitive, ErrorMsg);
|
|
|
|
// Get the structure specifier list
|
|
FToken StructToken;
|
|
FMetaData StructMetaData;
|
|
TArray<FPropertySpecifier> SpecifiersFound;
|
|
ReadSpecifierSetInsideMacro(SpecifiersFound, ErrorMsg, StructMetaData);
|
|
|
|
// Consume the struct keyword
|
|
RequireIdentifier(TEXT("struct"), ESearchCase::CaseSensitive, TEXT("Struct declaration specifier"));
|
|
|
|
SkipAlignasAndDeprecatedMacroIfNecessary(*this);
|
|
|
|
// The struct name as parsed in script and stripped of it's prefix
|
|
FString StructNameInScript;
|
|
|
|
// The required API module for this struct, if any
|
|
FString RequiredAPIMacroIfPresent;
|
|
|
|
// Read the struct name
|
|
ParseNameWithPotentialAPIMacroPrefix(/*out*/ StructNameInScript, /*out*/ RequiredAPIMacroIfPresent, TEXT("struct"));
|
|
|
|
// The struct name stripped of it's prefix
|
|
FString StructNameStripped = GetClassNameWithPrefixRemoved(StructNameInScript);
|
|
|
|
if (StructNameStripped.IsEmpty())
|
|
{
|
|
Throwf(TEXT("When compiling struct definition for '%s', attempting to strip prefix results in an empty name. Did you leave off a prefix?"), *StructNameInScript);
|
|
}
|
|
|
|
|
|
// Create the structure definition
|
|
TSharedRef<FUnrealScriptStructDefinitionInfo> StructDef = MakeShareable(new FUnrealScriptStructDefinitionInfo(SourceFile, InLineNumber, *StructNameInScript, FName(StructNameStripped, FNAME_Add)));
|
|
|
|
// Parse the inheritance list
|
|
ParseInheritance(TEXT("struct"), [this, &StructNameStripped, &StructDef = *StructDef](const TCHAR* StructName, bool bIsSuperClass)
|
|
{
|
|
FString StructNameStr(StructName);
|
|
SourceFile.AddScriptStructIncludeIfNeeded(*this, StructNameStripped, StructNameStr);
|
|
if (bIsSuperClass)
|
|
{
|
|
StructDef.GetSuperStructInfo().Name = MoveTemp(StructNameStr);
|
|
}
|
|
else
|
|
{
|
|
StructDef.GetBaseStructInfos().Emplace(FUnrealStructDefinitionInfo::FBaseStructInfo{ MoveTemp(StructNameStr) });
|
|
}
|
|
}
|
|
);
|
|
|
|
// Initialize the structure flags
|
|
StructDef->SetStructFlags(STRUCT_Native);
|
|
|
|
// Record that this struct is RequiredAPI if the CORE_API style macro was present
|
|
if (!RequiredAPIMacroIfPresent.IsEmpty())
|
|
{
|
|
StructDef->SetStructFlags(STRUCT_RequiredAPI);
|
|
}
|
|
|
|
// Process the list of specifiers
|
|
for (const FPropertySpecifier& Specifier : SpecifiersFound)
|
|
{
|
|
switch ((EStructSpecifier)Algo::FindSortedStringCaseInsensitive(*Specifier.Key, GStructSpecifierStrings))
|
|
{
|
|
default:
|
|
{
|
|
Throwf(TEXT("Unknown struct specifier '%s'"), *Specifier.Key);
|
|
}
|
|
break;
|
|
|
|
case EStructSpecifier::NoExport:
|
|
{
|
|
StructDef->SetStructFlags(STRUCT_NoExport);
|
|
StructDef->ClearStructFlags(STRUCT_Native);
|
|
}
|
|
break;
|
|
|
|
case EStructSpecifier::Atomic:
|
|
{
|
|
StructDef->SetStructFlags(STRUCT_Atomic);
|
|
}
|
|
break;
|
|
|
|
case EStructSpecifier::Immutable:
|
|
{
|
|
StructDef->SetStructFlags(STRUCT_Immutable);
|
|
StructDef->SetStructFlags(STRUCT_Atomic);
|
|
}
|
|
break;
|
|
|
|
case EStructSpecifier::HasDefaults:
|
|
if (!SourceFile.IsNoExportTypes())
|
|
{
|
|
LogError(TEXT("The 'HasDefaults' struct specifier is only valid in the NoExportTypes.h file"));
|
|
}
|
|
break;
|
|
|
|
case EStructSpecifier::HasNoOpConstructor:
|
|
if (!SourceFile.IsNoExportTypes())
|
|
{
|
|
LogError(TEXT("The 'HasNoOpConstructor' struct specifier is only valid in the NoExportTypes.h file"));
|
|
}
|
|
break;
|
|
|
|
case EStructSpecifier::IsAlwaysAccessible:
|
|
if (!SourceFile.IsNoExportTypes())
|
|
{
|
|
LogError(TEXT("The 'IsAlwaysAccessible' struct specifier is only valid in the NoExportTypes.h file"));
|
|
}
|
|
break;
|
|
|
|
case EStructSpecifier::IsCoreType:
|
|
if (!SourceFile.IsNoExportTypes())
|
|
{
|
|
LogError(TEXT("The 'IsCoreType' struct specifier is only valid in the NoExportTypes.h file"));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Check to make sure the syntactic native prefix was set-up correctly.
|
|
// If this check results in a false positive, it will be flagged as an identifier failure.
|
|
FString DeclaredPrefix = GetClassPrefix(StructNameInScript);
|
|
if (DeclaredPrefix == StructDef->GetPrefixCPP() || DeclaredPrefix == TEXT("T"))
|
|
{
|
|
// Found a prefix, do a basic check to see if it's valid
|
|
const TCHAR* ExpectedPrefixCPP = UHTConfig.StructsWithTPrefix.Contains(StructNameStripped) ? TEXT("T") : StructDef->GetPrefixCPP();
|
|
FString ExpectedStructName = FString::Printf(TEXT("%s%s"), ExpectedPrefixCPP, *StructNameStripped);
|
|
if (StructNameInScript != ExpectedStructName)
|
|
{
|
|
StructDef->Throwf(TEXT("Struct '%s' has an invalid Unreal prefix, expecting '%s'"), *StructNameInScript, *ExpectedStructName);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
const TCHAR* ExpectedPrefixCPP = UHTConfig.StructsWithTPrefix.Contains(StructNameInScript) ? TEXT("T") : StructDef->GetPrefixCPP();
|
|
FString ExpectedStructName = FString::Printf(TEXT("%s%s"), ExpectedPrefixCPP, *StructNameInScript);
|
|
StructDef->Throwf(TEXT("Struct '%s' is missing a valid Unreal prefix, expecting '%s'"), *StructNameInScript, *ExpectedStructName);
|
|
}
|
|
|
|
return StructDef;
|
|
}
|
|
|
|
bool FHeaderParser::TryToMatchConstructorParameterList(FToken Token)
|
|
{
|
|
FToken PotentialParenthesisToken;
|
|
if (!GetToken(PotentialParenthesisToken))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!PotentialParenthesisToken.IsSymbol(TEXT('(')))
|
|
{
|
|
UngetToken(PotentialParenthesisToken);
|
|
return false;
|
|
}
|
|
|
|
FUnrealClassDefinitionInfo& ClassDef = GetCurrentClassDef();
|
|
|
|
bool bOICtor = false;
|
|
bool bVTCtor = false;
|
|
|
|
if (!ClassDef.IsDefaultConstructorDeclared() && MatchSymbol(TEXT(')')))
|
|
{
|
|
ClassDef.MarkDefaultConstructorDeclared();
|
|
}
|
|
else if (!ClassDef.IsObjectInitializerConstructorDeclared()
|
|
|| !ClassDef.IsCustomVTableHelperConstructorDeclared())
|
|
{
|
|
FToken ObjectInitializerParamParsingToken;
|
|
|
|
bool bIsConst = false;
|
|
bool bIsRef = false;
|
|
int32 ParenthesesNestingLevel = 1;
|
|
|
|
while (ParenthesesNestingLevel && GetToken(ObjectInitializerParamParsingToken))
|
|
{
|
|
// Template instantiation or additional parameter excludes ObjectInitializer constructor.
|
|
if (ObjectInitializerParamParsingToken.IsSymbol(TEXT(',')) || ObjectInitializerParamParsingToken.IsSymbol(TEXT('<')))
|
|
{
|
|
bOICtor = false;
|
|
bVTCtor = false;
|
|
break;
|
|
}
|
|
|
|
if (ObjectInitializerParamParsingToken.IsSymbol(TEXT('(')))
|
|
{
|
|
ParenthesesNestingLevel++;
|
|
continue;
|
|
}
|
|
|
|
if (ObjectInitializerParamParsingToken.IsSymbol(TEXT(')')))
|
|
{
|
|
ParenthesesNestingLevel--;
|
|
continue;
|
|
}
|
|
|
|
if (ObjectInitializerParamParsingToken.IsIdentifier(TEXT("const"), ESearchCase::CaseSensitive))
|
|
{
|
|
bIsConst = true;
|
|
continue;
|
|
}
|
|
|
|
if (ObjectInitializerParamParsingToken.IsSymbol(TEXT('&')))
|
|
{
|
|
bIsRef = true;
|
|
continue;
|
|
}
|
|
|
|
if (ObjectInitializerParamParsingToken.IsIdentifier(TEXT("FObjectInitializer"), ESearchCase::CaseSensitive)
|
|
|| ObjectInitializerParamParsingToken.IsIdentifier(TEXT("FPostConstructInitializeProperties"), ESearchCase::CaseSensitive) // Deprecated, but left here, so it won't break legacy code.
|
|
)
|
|
{
|
|
bOICtor = true;
|
|
}
|
|
|
|
if (ObjectInitializerParamParsingToken.IsIdentifier(TEXT("FVTableHelper"), ESearchCase::CaseSensitive))
|
|
{
|
|
bVTCtor = true;
|
|
}
|
|
}
|
|
|
|
// Parse until finish.
|
|
while (ParenthesesNestingLevel && GetToken(ObjectInitializerParamParsingToken))
|
|
{
|
|
if (ObjectInitializerParamParsingToken.IsSymbol(TEXT('(')))
|
|
{
|
|
ParenthesesNestingLevel++;
|
|
continue;
|
|
}
|
|
|
|
if (ObjectInitializerParamParsingToken.IsSymbol(TEXT(')')))
|
|
{
|
|
ParenthesesNestingLevel--;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (bOICtor && bIsRef && bIsConst)
|
|
{
|
|
ClassDef.MarkObjectInitializerConstructorDeclared();
|
|
}
|
|
if (bVTCtor && bIsRef)
|
|
{
|
|
ClassDef.MarkCustomVTableHelperConstructorDeclared();
|
|
}
|
|
}
|
|
|
|
if (!bVTCtor)
|
|
{
|
|
ClassDef.MarkConstructorDeclared();
|
|
}
|
|
|
|
// Optionally match semicolon.
|
|
if (!MatchSymbol(TEXT(';')))
|
|
{
|
|
// If not matched a semicolon, this is inline constructor definition. We have to skip it.
|
|
UngetToken(Token); // Resets input stream to the initial token.
|
|
GetToken(Token); // Re-gets the initial token to start constructor definition skip.
|
|
return SkipDeclaration(Token);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void FHeaderParser::CompileVersionDeclaration(FUnrealStructDefinitionInfo& StructDef)
|
|
{
|
|
// Do nothing if we're at the end of file.
|
|
FToken Token;
|
|
if (!GetToken(Token, true, ESymbolParseOption::Normal))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Default version based on config file.
|
|
EGeneratedCodeVersion Version = UHTConfig.DefaultGeneratedCodeVersion;
|
|
|
|
// Overwrite with module-specific value if one was specified.
|
|
if (PackageDef.GetModule().GeneratedCodeVersion != EGeneratedCodeVersion::None)
|
|
{
|
|
Version = PackageDef.GetModule().GeneratedCodeVersion;
|
|
}
|
|
|
|
if (Token.IsSymbol(TEXT(')')))
|
|
{
|
|
StructDef.SetGeneratedCodeVersion(Version);
|
|
UngetToken(Token);
|
|
return;
|
|
}
|
|
|
|
// Overwrite with version specified by macro.
|
|
Version = ToGeneratedCodeVersion(Token.Value);
|
|
StructDef.SetGeneratedCodeVersion(Version);
|
|
}
|
|
|
|
void FHeaderParser::ResetClassData()
|
|
{
|
|
FUnrealClassDefinitionInfo& CurrentClassDef = GetCurrentClassDef();
|
|
|
|
check(CurrentClassDef.GetClassWithin() == nullptr);
|
|
|
|
// Set class flags and within.
|
|
CurrentClassDef.ClearClassFlags(CLASS_RecompilerClear);
|
|
|
|
if (FUnrealClassDefinitionInfo* SuperClassDef = CurrentClassDef.GetSuperClass())
|
|
{
|
|
CurrentClassDef.SetClassFlags(SuperClassDef->GetClassFlags() & CurrentClassDef.GetInheritClassFlags());
|
|
CurrentClassDef.SetClassConfigName(SuperClassDef->GetClassConfigName());
|
|
|
|
check(SuperClassDef->GetClassWithin());
|
|
CurrentClassDef.SetClassWithin(SuperClassDef->GetClassWithin());
|
|
|
|
// Copy special categories from parent
|
|
if (SuperClassDef->HasMetaData(FHeaderParserNames::NAME_HideCategories))
|
|
{
|
|
CurrentClassDef.SetMetaData(FHeaderParserNames::NAME_HideCategories, *SuperClassDef->GetMetaData(FHeaderParserNames::NAME_HideCategories));
|
|
}
|
|
if (SuperClassDef->HasMetaData(FHeaderParserNames::NAME_ShowCategories))
|
|
{
|
|
CurrentClassDef.SetMetaData(FHeaderParserNames::NAME_ShowCategories, *SuperClassDef->GetMetaData(FHeaderParserNames::NAME_ShowCategories));
|
|
}
|
|
if (SuperClassDef->HasMetaData(FHeaderParserNames::NAME_SparseClassDataTypes))
|
|
{
|
|
CurrentClassDef.SetMetaData(FHeaderParserNames::NAME_SparseClassDataTypes, *SuperClassDef->GetMetaData(FHeaderParserNames::NAME_SparseClassDataTypes));
|
|
}
|
|
if (SuperClassDef->HasMetaData(FHeaderParserNames::NAME_HideFunctions))
|
|
{
|
|
CurrentClassDef.SetMetaData(FHeaderParserNames::NAME_HideFunctions, *SuperClassDef->GetMetaData(FHeaderParserNames::NAME_HideFunctions));
|
|
}
|
|
if (SuperClassDef->HasMetaData(FHeaderParserNames::NAME_AutoExpandCategories))
|
|
{
|
|
CurrentClassDef.SetMetaData(FHeaderParserNames::NAME_AutoExpandCategories, *SuperClassDef->GetMetaData(FHeaderParserNames::NAME_AutoExpandCategories));
|
|
}
|
|
if (SuperClassDef->HasMetaData(FHeaderParserNames::NAME_AutoCollapseCategories))
|
|
{
|
|
CurrentClassDef.SetMetaData(FHeaderParserNames::NAME_AutoCollapseCategories, *SuperClassDef->GetMetaData(FHeaderParserNames::NAME_AutoCollapseCategories));
|
|
}
|
|
if (SuperClassDef->HasMetaData(FHeaderParserNames::NAME_PrioritizeCategories))
|
|
{
|
|
CurrentClassDef.SetMetaData(FHeaderParserNames::NAME_PrioritizeCategories, *SuperClassDef->GetMetaData(FHeaderParserNames::NAME_PrioritizeCategories));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CurrentClassDef.SetClassWithin(GUObjectDef);
|
|
}
|
|
|
|
check(CurrentClassDef.GetClassWithin());
|
|
}
|
|
|
|
void FHeaderParser::PostPopNestClass(FUnrealClassDefinitionInfo& CurrentClassDef)
|
|
{
|
|
// Validate all the rep notify events here, to make sure they're implemented
|
|
VerifyPropertyMarkups(CurrentClassDef);
|
|
|
|
VerifyFunctionsMarkups(CurrentClassDef);
|
|
|
|
// Iterate over all the interfaces we claim to implement
|
|
for (const FUnrealStructDefinitionInfo::FBaseStructInfo& BaseClassInfo : CurrentClassDef.GetBaseStructInfos())
|
|
{
|
|
|
|
// Skip unknown base classes or things that aren't interfaces
|
|
if (BaseClassInfo.Struct == nullptr)
|
|
{
|
|
continue;
|
|
}
|
|
FUnrealClassDefinitionInfo* InterfaceDef = UHTCast<FUnrealClassDefinitionInfo>(BaseClassInfo.Struct);
|
|
if (InterfaceDef == nullptr || !InterfaceDef->IsInterface())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// And their super-classes
|
|
for (; InterfaceDef; InterfaceDef = InterfaceDef->GetSuperClass())
|
|
{
|
|
// If this interface is a common ancestor, skip it
|
|
if (CurrentClassDef.IsChildOf(*InterfaceDef))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// So iterate over all functions this interface declares
|
|
for (FUnrealFunctionDefinitionInfo* InterfaceFunctionDef : TUHTFieldRange<FUnrealFunctionDefinitionInfo>(*InterfaceDef, EFieldIteratorFlags::ExcludeSuper))
|
|
{
|
|
bool bImplemented = false;
|
|
|
|
// And try to find one that matches
|
|
for (FUnrealFunctionDefinitionInfo* ClassFunctionDef : TUHTFieldRange<FUnrealFunctionDefinitionInfo>(CurrentClassDef))
|
|
{
|
|
if (ClassFunctionDef->GetFName() != InterfaceFunctionDef->GetFName())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (InterfaceFunctionDef->HasAnyFunctionFlags(FUNC_Event) && !ClassFunctionDef->HasAnyFunctionFlags(FUNC_Event))
|
|
{
|
|
Throwf(TEXT("Implementation of function '%s::%s' must be declared as 'event' to match declaration in interface '%s'"), *ClassFunctionDef->GetOuter()->GetName(), *ClassFunctionDef->GetName(), *InterfaceDef->GetName());
|
|
}
|
|
|
|
if (InterfaceFunctionDef->HasAnyFunctionFlags(FUNC_Delegate) && !ClassFunctionDef->HasAnyFunctionFlags(FUNC_Delegate))
|
|
{
|
|
Throwf(TEXT("Implementation of function '%s::%s' must be declared as 'delegate' to match declaration in interface '%s'"), *ClassFunctionDef->GetOuter()->GetName(), *ClassFunctionDef->GetName(), *InterfaceDef->GetName());
|
|
}
|
|
|
|
// Making sure all the parameters match up correctly
|
|
bImplemented = true;
|
|
|
|
if (ClassFunctionDef->GetProperties().Num() != InterfaceFunctionDef->GetProperties().Num())
|
|
{
|
|
Throwf(TEXT("Implementation of function '%s' conflicts with interface '%s' - different number of parameters (%i/%i)"), *InterfaceFunctionDef->GetName(), *InterfaceDef->GetName(), ClassFunctionDef->GetProperties().Num(), InterfaceFunctionDef->GetProperties().Num());
|
|
}
|
|
|
|
for (int32 Index = 0, End = ClassFunctionDef->GetProperties().Num(); Index != End; ++Index)
|
|
{
|
|
FUnrealPropertyDefinitionInfo& InterfaceParamDef = *InterfaceFunctionDef->GetProperties()[Index];
|
|
FUnrealPropertyDefinitionInfo& ClassParamDef = *ClassFunctionDef->GetProperties()[Index];
|
|
if (!InterfaceParamDef.MatchesType(ClassParamDef, true))
|
|
{
|
|
if (InterfaceParamDef.HasAnyPropertyFlags(CPF_ReturnParm))
|
|
{
|
|
Throwf(TEXT("Implementation of function '%s' conflicts only by return type with interface '%s'"), *InterfaceFunctionDef->GetName(), *InterfaceDef->GetName());
|
|
}
|
|
else
|
|
{
|
|
Throwf(TEXT("Implementation of function '%s' conflicts with interface '%s' - parameter %i '%s'"), *InterfaceFunctionDef->GetName(), *InterfaceDef->GetName(), End, *InterfaceParamDef.GetName());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Delegate signature functions are simple stubs and aren't required to be implemented (they are not callable)
|
|
if (InterfaceFunctionDef->HasAnyFunctionFlags(FUNC_Delegate))
|
|
{
|
|
bImplemented = true;
|
|
}
|
|
|
|
// Verify that if this has blueprint-callable functions that are not implementable events, we've implemented them as a UFunction in the target class
|
|
if (!bImplemented
|
|
&& InterfaceFunctionDef->HasAnyFunctionFlags(FUNC_BlueprintCallable)
|
|
&& !InterfaceFunctionDef->HasAnyFunctionFlags(FUNC_BlueprintEvent)
|
|
&& !InterfaceDef->HasMetaData(NAME_CannotImplementInterfaceInBlueprint)) // FBlueprintMetadata::MD_CannotImplementInterfaceInBlueprint
|
|
{
|
|
Throwf(TEXT("Missing UFunction implementation of function '%s' from interface '%s'. This function needs a UFUNCTION() declaration."), *InterfaceFunctionDef->GetName(), *InterfaceDef->GetName());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FHeaderParser::PostPopFunctionDeclaration(FUnrealFunctionDefinitionInfo& PoppedFunctionDef)
|
|
{
|
|
//@TODO: UCREMOVAL: Move this code to occur at delegate var declaration, and force delegates to be declared before variables that use them
|
|
if (!GetCurrentScope()->IsFileScope() && GetCurrentClassDef().ContainsDelegates())
|
|
{
|
|
// now validate all delegate variables declared in the class
|
|
TMap<FName, FUnrealFunctionDefinitionInfo*> DelegateCache;
|
|
FixupDelegateProperties(PoppedFunctionDef, *GetCurrentScope(), DelegateCache);
|
|
}
|
|
}
|
|
|
|
void FHeaderParser::PostPopNestInterface(FUnrealClassDefinitionInfo& CurrentInterfaceDef)
|
|
{
|
|
if (CurrentInterfaceDef.ContainsDelegates())
|
|
{
|
|
TMap<FName, FUnrealFunctionDefinitionInfo*> DelegateCache;
|
|
FixupDelegateProperties(CurrentInterfaceDef, *CurrentInterfaceDef.GetScope(), DelegateCache);
|
|
}
|
|
}
|
|
|
|
FDocumentationPolicy FHeaderParser::GetDocumentationPolicyFromName(const FUHTMessageProvider& Context, const FString& PolicyName)
|
|
{
|
|
FDocumentationPolicy DocumentationPolicy;
|
|
if (FCString::Strcmp(*PolicyName, TEXT("Strict")) == 0)
|
|
{
|
|
DocumentationPolicy.bClassOrStructCommentRequired = true;
|
|
DocumentationPolicy.bFunctionToolTipsRequired = true;
|
|
DocumentationPolicy.bMemberToolTipsRequired = true;
|
|
DocumentationPolicy.bParameterToolTipsRequired = true;
|
|
DocumentationPolicy.bFloatRangesRequired = true;
|
|
}
|
|
else
|
|
{
|
|
Context.Throwf(TEXT("Documentation Policy '%s' not yet supported"), *PolicyName);
|
|
}
|
|
return DocumentationPolicy;
|
|
}
|
|
|
|
|
|
FDocumentationPolicy FHeaderParser::GetDocumentationPolicyForStruct(FUnrealStructDefinitionInfo& StructDef)
|
|
{
|
|
SCOPE_SECONDS_COUNTER_UHT(DocumentationPolicy);
|
|
|
|
FDocumentationPolicy DocumentationPolicy;
|
|
FString DocumentationPolicyName;
|
|
if (StructDef.GetStringMetaDataHierarchical(NAME_DocumentationPolicy, &DocumentationPolicyName))
|
|
{
|
|
DocumentationPolicy = GetDocumentationPolicyFromName(StructDef, DocumentationPolicyName);
|
|
}
|
|
return DocumentationPolicy;
|
|
}
|
|
|
|
void FHeaderParser::CheckDocumentationPolicyForEnum(FUnrealEnumDefinitionInfo& EnumDef, const TMap<FName, FString>& MetaData, const TArray<TMap<FName, FString>>& Entries)
|
|
{
|
|
SCOPE_SECONDS_COUNTER_UHT(DocumentationPolicy);
|
|
|
|
const FString* DocumentationPolicyName = MetaData.Find(NAME_DocumentationPolicy);
|
|
if (DocumentationPolicyName == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
check(!DocumentationPolicyName->IsEmpty());
|
|
|
|
FDocumentationPolicy DocumentationPolicy = GetDocumentationPolicyFromName(EnumDef, *DocumentationPolicyName);
|
|
if (DocumentationPolicy.bClassOrStructCommentRequired)
|
|
{
|
|
const FString* EnumToolTip = MetaData.Find(NAME_ToolTip);
|
|
if (EnumToolTip == nullptr)
|
|
{
|
|
LogError(TEXT("Enum '%s' does not provide a tooltip / comment (DocumentationPolicy)."), *EnumDef.GetName());
|
|
}
|
|
}
|
|
|
|
TMap<FString, FString> ToolTipToEntry;
|
|
for (const TMap<FName, FString>& Entry : Entries)
|
|
{
|
|
const FString* EntryName = Entry.Find(NAME_Name);
|
|
if (EntryName == nullptr)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const FString* ToolTip = Entry.Find(NAME_ToolTip);
|
|
if (ToolTip == nullptr)
|
|
{
|
|
LogError(TEXT("Enum entry '%s::%s' does not provide a tooltip / comment (DocumentationPolicy)."), *EnumDef.GetName(), *(*EntryName));
|
|
continue;
|
|
}
|
|
|
|
const FString* ExistingEntry = ToolTipToEntry.Find(*ToolTip);
|
|
if (ExistingEntry != nullptr)
|
|
{
|
|
LogError(TEXT("Enum entries '%s::%s' and '%s::%s' have identical tooltips / comments (DocumentationPolicy)."), *EnumDef.GetName(), *(*ExistingEntry), *EnumDef.GetName(), *(*EntryName));
|
|
}
|
|
ToolTipToEntry.Add(*ToolTip, *EntryName);
|
|
}
|
|
}
|
|
|
|
void FHeaderParser::CheckDocumentationPolicyForStruct(FUnrealStructDefinitionInfo& StructDef)
|
|
{
|
|
SCOPE_SECONDS_COUNTER_UHT(DocumentationPolicy);
|
|
|
|
FDocumentationPolicy DocumentationPolicy = GetDocumentationPolicyForStruct(StructDef);
|
|
if (DocumentationPolicy.bClassOrStructCommentRequired)
|
|
{
|
|
FString ClassTooltip;
|
|
if (const FString* ClassTooltipPtr = StructDef.FindMetaData(NAME_ToolTip))
|
|
{
|
|
ClassTooltip = *ClassTooltipPtr;
|
|
}
|
|
|
|
if (ClassTooltip.IsEmpty() || ClassTooltip.Equals(StructDef.GetName()))
|
|
{
|
|
LogError(TEXT("Struct '%s' does not provide a tooltip / comment (DocumentationPolicy)."), *StructDef.GetName());
|
|
}
|
|
}
|
|
|
|
if (DocumentationPolicy.bMemberToolTipsRequired)
|
|
{
|
|
TMap<FString, FName> ToolTipToPropertyName;
|
|
for (TSharedRef<FUnrealPropertyDefinitionInfo> PropertyDef : StructDef.GetProperties())
|
|
{
|
|
FString ToolTip = PropertyDef->GetToolTipText().ToString();
|
|
if (ToolTip.IsEmpty() || ToolTip.Equals(PropertyDef->GetDisplayNameText().ToString()))
|
|
{
|
|
LogError(TEXT("Property '%s::%s' does not provide a tooltip / comment (DocumentationPolicy)."), *StructDef.GetName(), *PropertyDef->GetName());
|
|
continue;
|
|
}
|
|
const FName* ExistingPropertyName = ToolTipToPropertyName.Find(ToolTip);
|
|
if (ExistingPropertyName != nullptr)
|
|
{
|
|
LogError(TEXT("Property '%s::%s' and '%s::%s' are using identical tooltips (DocumentationPolicy)."), *StructDef.GetName(), *ExistingPropertyName->ToString(), *StructDef.GetName(), *PropertyDef->GetName());
|
|
}
|
|
ToolTipToPropertyName.Add(MoveTemp(ToolTip), PropertyDef->GetFName());
|
|
}
|
|
}
|
|
|
|
if (DocumentationPolicy.bFloatRangesRequired)
|
|
{
|
|
for (TSharedRef<FUnrealPropertyDefinitionInfo> PropertyDef : StructDef.GetProperties())
|
|
{
|
|
if(DoesCPPTypeRequireDocumentation(PropertyDef->GetCPPType()))
|
|
{
|
|
const FString& UIMin = PropertyDef->GetMetaData(NAME_UIMin);
|
|
const FString& UIMax = PropertyDef->GetMetaData(NAME_UIMax);
|
|
|
|
if(!CheckUIMinMaxRangeFromMetaData(UIMin, UIMax))
|
|
{
|
|
LogError(TEXT("Property '%s::%s' does not provide a valid UIMin / UIMax (DocumentationPolicy)."), *StructDef.GetName(), *PropertyDef->GetName());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// also compare all tooltips to see if they are unique
|
|
if (DocumentationPolicy.bFunctionToolTipsRequired)
|
|
{
|
|
if (FUnrealClassDefinitionInfo* ClassDef = UHTCast<FUnrealClassDefinitionInfo>(StructDef))
|
|
{
|
|
TMap<FString, FName> ToolTipToFunc;
|
|
for (TSharedRef<FUnrealFunctionDefinitionInfo> FunctionDef : ClassDef->GetFunctions())
|
|
{
|
|
FString ToolTip = FunctionDef->GetToolTipText().ToString();
|
|
if (ToolTip.IsEmpty())
|
|
{
|
|
LogError(TEXT("Function '%s::%s' does not provide a tooltip / comment (DocumentationPolicy)."), *ClassDef->GetName(), *FunctionDef->GetName());
|
|
continue;
|
|
}
|
|
const FName* ExistingFuncName = ToolTipToFunc.Find(ToolTip);
|
|
if (ExistingFuncName != nullptr)
|
|
{
|
|
LogError(TEXT("Functions '%s::%s' and '%s::%s' uses identical tooltips / comments (DocumentationPolicy)."), *ClassDef->GetName(), *(*ExistingFuncName).ToString(), *ClassDef->GetName(), *FunctionDef->GetName());
|
|
}
|
|
ToolTipToFunc.Add(MoveTemp(ToolTip), FunctionDef->GetFName());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool FHeaderParser::DoesCPPTypeRequireDocumentation(const FString& CPPType)
|
|
{
|
|
return PropertyCPPTypesRequiringUIRanges.Contains(CPPType);
|
|
}
|
|
|
|
// Validates the documentation for a given method
|
|
void FHeaderParser::CheckDocumentationPolicyForFunc(FUnrealClassDefinitionInfo& ClassDef, FUnrealFunctionDefinitionInfo& FunctionDef)
|
|
{
|
|
SCOPE_SECONDS_COUNTER_UHT(DocumentationPolicy);
|
|
|
|
FDocumentationPolicy DocumentationPolicy = GetDocumentationPolicyForStruct(ClassDef);
|
|
if (DocumentationPolicy.bFunctionToolTipsRequired)
|
|
{
|
|
const FString* FunctionTooltip = FunctionDef.FindMetaData(NAME_ToolTip);
|
|
if (FunctionTooltip == nullptr)
|
|
{
|
|
LogError(TEXT("Function '%s::%s' does not provide a tooltip / comment (DocumentationPolicy)."), *ClassDef.GetName(), *FunctionDef.GetName());
|
|
}
|
|
}
|
|
|
|
if (DocumentationPolicy.bParameterToolTipsRequired)
|
|
{
|
|
const FString* FunctionComment = FunctionDef.FindMetaData(NAME_Comment);
|
|
if (FunctionComment == nullptr)
|
|
{
|
|
LogError(TEXT("Function '%s::%s' does not provide a comment (DocumentationPolicy)."), *ClassDef.GetName(), *FunctionDef.GetName());
|
|
return;
|
|
}
|
|
|
|
TMap<FName, FString> ParamToolTips = GetParameterToolTipsFromFunctionComment(*FunctionComment);
|
|
bool HasAnyParamToolTips = ParamToolTips.Num() > 0;
|
|
if (ParamToolTips.Num() == 0)
|
|
{
|
|
const FString* ReturnValueToolTip = ParamToolTips.Find(NAME_ReturnValue);
|
|
if (ReturnValueToolTip != nullptr)
|
|
{
|
|
HasAnyParamToolTips = false;
|
|
}
|
|
}
|
|
|
|
// only apply the validation for parameter tooltips if a function has any @param statements at all.
|
|
if (HasAnyParamToolTips)
|
|
{
|
|
// ensure each parameter has a tooltip
|
|
TSet<FName> ExistingFields;
|
|
for (TSharedRef<FUnrealPropertyDefinitionInfo> PropertyDef : FunctionDef.GetProperties())
|
|
{
|
|
FName ParamName = PropertyDef->GetFName();
|
|
if (ParamName == NAME_ReturnValue)
|
|
{
|
|
continue;
|
|
}
|
|
const FString* ParamToolTip = ParamToolTips.Find(ParamName);
|
|
if (ParamToolTip == nullptr)
|
|
{
|
|
LogError(TEXT("Function '%s::%s' doesn't provide a tooltip for parameter '%s' (DocumentationPolicy)."), *ClassDef.GetName(), *FunctionDef.GetName(), *ParamName.ToString());
|
|
}
|
|
ExistingFields.Add(ParamName);
|
|
}
|
|
|
|
// ensure we don't have parameter tooltips for parameters that don't exist
|
|
for (TPair<FName, FString>& Pair : ParamToolTips)
|
|
{
|
|
const FName& ParamName = Pair.Key;
|
|
if (ParamName == NAME_ReturnValue)
|
|
{
|
|
continue;
|
|
}
|
|
if (!ExistingFields.Contains(ParamName))
|
|
{
|
|
LogError(TEXT("Function '%s::%s' provides a tooltip for an unknown parameter '%s' (DocumentationPolicy)."), *ClassDef.GetName(), *FunctionDef.GetName(), *Pair.Key.ToString());
|
|
}
|
|
}
|
|
|
|
// check for duplicate tooltips
|
|
TMap<FString, FName> ToolTipToParam;
|
|
for (TPair<FName, FString>& Pair : ParamToolTips)
|
|
{
|
|
const FName& ParamName = Pair.Key;
|
|
if (ParamName == NAME_ReturnValue)
|
|
{
|
|
continue;
|
|
}
|
|
const FName* ExistingParam = ToolTipToParam.Find(Pair.Value);
|
|
if (ExistingParam != nullptr)
|
|
{
|
|
LogError(TEXT("Function '%s::%s' uses identical tooltips for parameters '%s' and '%s' (DocumentationPolicy)."), *ClassDef.GetName(), *FunctionDef.GetName(), *ExistingParam->ToString(), *Pair.Key.ToString());
|
|
}
|
|
ToolTipToParam.Add(MoveTemp(Pair.Value), Pair.Key);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool FHeaderParser::IsReservedTypeName(const FString& TypeName)
|
|
{
|
|
for(const FString& ReservedName : ReservedTypeNames)
|
|
{
|
|
if(TypeName == ReservedName)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool FHeaderParser::CheckUIMinMaxRangeFromMetaData(const FString& UIMin, const FString& UIMax)
|
|
{
|
|
if (UIMin.IsEmpty() || UIMax.IsEmpty())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
double UIMinValue = FCString::Atod(*UIMin);
|
|
double UIMaxValue = FCString::Atod(*UIMax);
|
|
if (UIMin > UIMax) // note that we actually allow UIMin == UIMax to disable the range manually.
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void FHeaderParser::ConditionalLogPointerUsage(EPointerMemberBehavior PointerMemberBehavior, const TCHAR* PointerTypeDesc, FString&& PointerTypeDecl, const TCHAR* AlternativeTypeDesc)
|
|
{
|
|
switch (PointerMemberBehavior)
|
|
{
|
|
case EPointerMemberBehavior::Disallow:
|
|
{
|
|
if (AlternativeTypeDesc)
|
|
{
|
|
LogError(TEXT("%s usage in member declaration detected [[[%s]]]. This is disallowed for the target/module, consider %s as an alternative."), PointerTypeDesc, *PointerTypeDecl, AlternativeTypeDesc);
|
|
}
|
|
else
|
|
{
|
|
LogError(TEXT("%s usage in member declaration detected [[[%s]]]."), PointerTypeDesc, *PointerTypeDecl);
|
|
}
|
|
}
|
|
break;
|
|
case EPointerMemberBehavior::AllowAndLog:
|
|
{
|
|
if (AlternativeTypeDesc)
|
|
{
|
|
UE_LOG(LogCompile, Log, TEXT("%s(%d): %s usage in member declaration detected [[[%s]]]. Consider %s as an alternative."), *Filename, InputLine, PointerTypeDesc, *PointerTypeDecl, AlternativeTypeDesc);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogCompile, Log, TEXT("%s(%d): usage in member declaration detected [[[%s]]]."), *Filename, InputLine, PointerTypeDesc, *PointerTypeDecl);
|
|
}
|
|
}
|
|
break;
|
|
case EPointerMemberBehavior::AllowSilently:
|
|
{
|
|
// Do nothing
|
|
}
|
|
break;
|
|
default:
|
|
{
|
|
checkf(false, TEXT("Unhandled case"));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
FUnrealFunctionDefinitionInfo& FHeaderParser::CreateFunction(const TCHAR* FuncName, FFuncInfo&& FuncInfo, EFunctionType InFunctionType) const
|
|
{
|
|
FScope* CurrentScope = GetCurrentScope();
|
|
FUnrealObjectDefinitionInfo& OuterDef = IsInAClass() ? static_cast<FUnrealObjectDefinitionInfo&>(GetCurrentClassDef()) : SourceFile.GetPackageDef();
|
|
|
|
|
|
// Allocate local property frame, push nesting level and verify
|
|
// uniqueness at this scope level.
|
|
{
|
|
auto TypeIterator = CurrentScope->GetTypeIterator();
|
|
while (TypeIterator.MoveNext())
|
|
{
|
|
FUnrealFieldDefinitionInfo* Type = *TypeIterator;
|
|
if (Type->GetFName() == FuncName)
|
|
{
|
|
Throwf(TEXT("'%s' conflicts with '%s'"), FuncName, *Type->GetFullName());
|
|
}
|
|
}
|
|
}
|
|
|
|
TSharedRef<FUnrealFunctionDefinitionInfo> FuncDefRef = MakeShared<FUnrealFunctionDefinitionInfo>(SourceFile, InputLine, FString(FuncName), FName(FuncName, FNAME_Add), OuterDef, MoveTemp(FuncInfo), InFunctionType);
|
|
FUnrealFunctionDefinitionInfo& FuncDef = *FuncDefRef;
|
|
FuncDef.GetFunctionData().SetFunctionNames(FuncDef);
|
|
|
|
CurrentScope->AddType(FuncDef);
|
|
|
|
if (!CurrentScope->IsFileScope())
|
|
{
|
|
FUnrealStructDefinitionInfo& StructDef = ((FStructScope*)CurrentScope)->GetStructDef();
|
|
StructDef.AddFunction(FuncDefRef);
|
|
}
|
|
else
|
|
{
|
|
SourceFile.AddDefinedFunction(FuncDefRef);
|
|
FScopeLock Lock(&GlobalDelegatesLock);
|
|
GlobalDelegates.Add(FuncDef.GetName(), &FuncDef);
|
|
}
|
|
|
|
return FuncDef;
|
|
}
|
|
|
|
FRecordTokens::FRecordTokens(FHeaderParser& InParser, FUnrealStructDefinitionInfo* InStructDef, FToken* InToken)
|
|
: Parser(InParser)
|
|
, StructDef(InStructDef)
|
|
, CurrentCompilerDirective(Parser.GetCurrentCompilerDirective())
|
|
{
|
|
if (StructDef != nullptr)
|
|
{
|
|
Parser.bRecordTokens = true;
|
|
if (InToken != nullptr)
|
|
{
|
|
Parser.RecordedTokens.Push(*InToken);
|
|
}
|
|
}
|
|
}
|
|
|
|
FRecordTokens::~FRecordTokens()
|
|
{
|
|
Stop();
|
|
}
|
|
|
|
bool FRecordTokens::Stop()
|
|
{
|
|
if (StructDef != nullptr)
|
|
{
|
|
Parser.bRecordTokens = false;
|
|
StructDef->AddDeclaration(CurrentCompilerDirective, MoveTemp(Parser.RecordedTokens));
|
|
StructDef = nullptr;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|