// 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 #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 FHeaderParser::PropertyCPPTypesRequiringUIRanges = { TEXT("float"), TEXT("double") }; TArray FHeaderParser::ReservedTypeNames = { TEXT("none") }; // Busy wait support for holes in the include graph issues extern bool bGoWide; extern std::atomic GSourcesConcurrent; extern std::atomic GSourcesToParse; extern std::atomic GSourcesParsing; extern std::atomic GSourcesCompleted; extern std::atomic 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& 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& Specifiers, TMap& 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 GlobalDelegates; template 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& 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")); 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()); } } } } 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(Scope->GetOuter())) { for (FUnrealPropertyDefinitionInfo* PropertyDef : TUHTFieldRange(*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(*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(Scope->GetOuter())) { for (FUnrealFunctionDefinitionInfo* FunctionDef : TUHTFieldRange(*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(*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 &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 &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 &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 Scope = SourceFile.GetScope(); CheckAllow( TEXT("'Enum'"), ENestAllowFlags::TypeDecl ); // Get the enum specifier list FToken EnumToken; FMetaData EnumMetaData; TArray 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(*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 EnumValueMetaData = EnumMetaData; AddModuleRelativePathToMetadata(EnumDef, EnumValueMetaData); AddFormattedPrevCommentAsTooltipMetaData(EnumValueMetaData); // Parse all enums tags. FToken TagToken; FMetaData TagMetaData; TArray> EntryMetaData; bool bHasUnparsedValue = false; TArray> 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 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 FHeaderParser::GetParameterToolTipsFromFunctionComment(const FString& Input) { SCOPE_SECONDS_COUNTER_UHT(DocumentationPolicy); TMap Map; if (Input.IsEmpty()) { return Map; } TArray 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& 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 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 MetaData; // Get the struct specifier list TArray 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(*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(Scope->FindTypeByName(*ParentStructNameStripped)); } // If it wasn't found, try to find the literal name given if (BaseStructDef == nullptr) { BaseStructDef = UHTCast(Scope->FindTypeByName(*ParentStructNameInScript)); } // Try to locate globally //@TODO: UCREMOVAL: This seems extreme if (BaseStructDef == nullptr) { if (bOverrideParentStructName) { BaseStructDef = GTypeDefinitionInfoMap.FindByName(*ParentStructNameStripped); } if (BaseStructDef == nullptr) { BaseStructDef = GTypeDefinitionInfoMap.FindByName(*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 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& DelegateCache ) { for (TSharedRef 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 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 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(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 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(*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(*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 ParentSparseClassDataTypeNames; ClassDef->GetSuperClass()->GetSparseClassDataTypes(ParentSparseClassDataTypeNames); for (FString& ParentSparseClassDataTypeName : ParentSparseClassDataTypeNames) { if (FUnrealScriptStructDefinitionInfo* ParentSparseClassDataStructDef = GTypeDefinitionInfoMap.FindByName(*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>& 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>& 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>& 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&. 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&'"), *TargetFuncDef->GetName()); } } } else { // Couldn't find a valid function... LogError(TEXT("Replication notification function %s not found"), *PropertyDef.GetRepNotifyFunc().ToString() ); } } void FHeaderParser::VerifyGetterSetterAccessorProperties(const FUnrealClassDefinitionInfo& TargetClassDef, FUnrealPropertyDefinitionInfo& PropertyDef) { FPropertyBase& PropertyToken = PropertyDef.GetPropertyBase(); if (!PropertyToken.SetterName.IsEmpty()) { if (PropertyToken.SetterName != TEXT("None") && !PropertyToken.bSetterFunctionFound) { // The function can be a UFunction (not parsed the same way as the other accessors) const TSharedRef* FoundFunction = TargetClassDef.GetFunctions().FindByPredicate([&PropertyToken](const TSharedRef& Other) { return Other->GetName() == PropertyToken.SetterName; }); if (FoundFunction) { const FUnrealFunctionDefinitionInfo& Function = FoundFunction->Get(); if (!Function.HasAllFunctionFlags(FUNC_Native)) { LogError(TEXT("Property %s setter function %s has to be native."), *PropertyDef.GetName(), *PropertyToken.SetterName); } if (Function.HasAnyFunctionFlags(FUNC_Net | FUNC_Event)) { LogError(TEXT("Property %s setter function %s cannot be a net or an event"), *PropertyDef.GetName(), *PropertyToken.SetterName); } else if (Function.HasAllFunctionFlags(FUNC_EditorOnly) != PropertyDef.HasAllPropertyFlags(CPF_EditorOnly)) { LogError(TEXT("Property %s setter function %s must both be EditorOnly or both not EditorOnly"), *PropertyDef.GetName(), *PropertyToken.SetterName); } else if (Function.GetProperties().Num() != 1) { LogError(TEXT("Property %s setter function %s must have a single argument"), *PropertyDef.GetName(), *PropertyToken.SetterName); } else { TSharedPtr FoundArgument; for (const TSharedRef& Arguments : Function.GetProperties()) { if (Arguments->HasAllPropertyFlags(CPF_Parm) && !Arguments->HasAnyPropertyFlags(CPF_ReturnParm)) { FoundArgument = Arguments; break; } } if (!FoundArgument.IsValid()) { LogError(TEXT("Property %s setter function %s does not have a valid argument."), *PropertyDef.GetName(), *PropertyToken.SetterName); } else if (!FoundArgument->SameType(PropertyDef)) { LogError(TEXT("Property %s setter function %s argument is not the same type."), *PropertyDef.GetName(), *PropertyToken.SetterName); } else { PropertyToken.bSetterFunctionFound = true; } } } if (!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) { // The function can be a UFunction (not parsed the same way as the other accessors) const TSharedRef* FoundFunction = TargetClassDef.GetFunctions().FindByPredicate([&PropertyToken](const TSharedRef& Other) { return Other->GetName() == PropertyToken.GetterName; }); if (FoundFunction) { const FUnrealFunctionDefinitionInfo& Function = FoundFunction->Get(); if (!Function.HasAllFunctionFlags(FUNC_Native)) { LogError(TEXT("Property %s getter function %s has to be native."), *PropertyDef.GetName(), *PropertyToken.GetterName); } if (!Function.HasAllFunctionFlags(FUNC_Const)) { LogError(TEXT("Property %s getter function %s has to be const."), *PropertyDef.GetName(), *PropertyToken.GetterName); } if (Function.HasAnyFunctionFlags(FUNC_Net | FUNC_Event)) { LogError(TEXT("Property %s getter function %s cannot be a net or an event"), *PropertyDef.GetName(), *PropertyToken.GetterName); } else if (Function.HasAllFunctionFlags(FUNC_EditorOnly) != PropertyDef.HasAllPropertyFlags(CPF_EditorOnly)) { LogError(TEXT("Property %s getter function %s must both be EditorOnly or both not EditorOnly"), *PropertyDef.GetName(), *PropertyToken.GetterName); } else if (Function.GetProperties().Num() != 1) { LogError(TEXT("Property %s getter function %s must have a single return value"), *PropertyDef.GetName(), *PropertyToken.GetterName); } else { TSharedPtr FoundArgument; for (const TSharedRef& Arguments : Function.GetProperties()) { if (Arguments->HasAllPropertyFlags(CPF_Parm) && Arguments->HasAnyPropertyFlags(CPF_ReturnParm)) { FoundArgument = Arguments; break; } } if (!FoundArgument.IsValid()) { LogError(TEXT("Property %s getter function %s does not have a return value."), *PropertyDef.GetName(), *PropertyToken.GetterName); } else if (!FoundArgument->SameType(PropertyDef)) { LogError(TEXT("Property %s getter function %s return value is not the same type."), *PropertyDef.GetName(), *PropertyToken.GetterName); } else { PropertyToken.bGetterFunctionFound = true; } } } if (!PropertyToken.bGetterFunctionFound) { LogError(TEXT("Property %s getter function %s not found"), *PropertyDef.GetName(), *PropertyToken.GetterName); } } } } 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>& 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 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 FunctionDef : SearchClassDef->GetFunctions()) { if (FunctionDef->GetFName() == FuncName) { return &*FunctionDef; } } } return nullptr; }; TGuardValue GuardedInputPos(InputPos, PropertyDef->GetParsePosition()); TGuardValue 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 VerifyGetterSetterAccessorProperties(TargetClassDef, *PropertyDef); if (PropertyDef->GetPropertyBase().bFieldNotify) { VerifyNotifyValueChangedProperties(*PropertyDef); } } } void FHeaderParser::VerifyFunctionsMarkups(FUnrealClassDefinitionInfo& TargetClassDef) { for (TSharedRef 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(OwnerStructDef); FUnrealClassDefinitionInfo* OwnerClassDef = UHTCast(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 SpecifiersFound; TMap 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.")); } } // 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, ...>' works VarProperty.MapKeyProp = MakeShared(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(*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(*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(*VarType.GetTokenValue()); if (!StructDef) { StructDef = GTypeDefinitionInfoMap.FindByName(*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(*IdentifierStripped) != nullptr) { bHandledType = true; // Struct keyword in front of a struct is legal, we 'consume' it bUnconsumedStructKeyword = false; } else { FUnrealFunctionDefinitionInfo* DelegateFuncDef = UHTCast(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 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, ...>' and 'TMap>' 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; } // Setup additional property as well as script struct def flags // for structs / properties being used for the RigVM. // The Input / Output / Constant metadata tokens can be used to mark // up an input / output pin of a RigVMNode. To allow authoring of those // nodes we'll mark up the property as accessible in Blueprint / Python // as well as make the struct a blueprint type. if(OwnerScriptStructDef) { const EPropertyFlags OriginalFlags = VarProperty.PropertyFlags; if(VarProperty.MetaData.Contains(NAME_ConstantText)) { VarProperty.PropertyFlags |= CPF_Edit | CPF_EditConst | CPF_BlueprintVisible; } if(VarProperty.MetaData.Contains(NAME_InputText) || VarProperty.MetaData.Contains(NAME_VisibleText)) { VarProperty.PropertyFlags |= CPF_Edit | CPF_BlueprintVisible; } if(VarProperty.MetaData.Contains(NAME_OutputText)) { if((VarProperty.PropertyFlags & CPF_BlueprintVisible) == 0) { VarProperty.PropertyFlags |= CPF_BlueprintVisible | CPF_BlueprintReadOnly; } } if(OriginalFlags != VarProperty.PropertyFlags && (((VarProperty.PropertyFlags & CPF_BlueprintVisible) != 0) || ((VarProperty.PropertyFlags & CPF_BlueprintReadOnly) != 0))) { if(!OwnerScriptStructDef->GetBoolMetaDataHierarchical(FHeaderParserNames::NAME_BlueprintType)) { OwnerScriptStructDef->SetMetaData(FHeaderParserNames::NAME_BlueprintType, TEXT("true")); } if(!VarProperty.MetaData.Contains(NAME_Category)) { static constexpr TCHAR PinsCategory[] = TEXT("Pins"); VarProperty.MetaData.Add(NAME_Category, PinsCategory); } } } } 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 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& 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 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; // parse enum type declared as a namespace Tokens.GetToken(Token); while (Token.IsSymbol(TEXT("::"), ESearchCase::CaseSensitive)) { OutAccessorType += Token.Value; 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 '&', 'const' and retain '*' do { if (Token.TokenType == ETokenType::None) { return false; } if (Token.IsIdentifier()) { if (!Token.IsIdentifier(TEXT("const"), ESearchCase::CaseSensitive)) { break; } // else skip const } // Support for passing values by ref for setters else if (!Token.IsSymbol(TEXT("&"), ESearchCase::CaseSensitive)) { OutAccessorType += Token.Value; } } while (Tokens.GetToken(Token)); // 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; } FUnrealPropertyDefinitionInfo* PropertyWithSetter = nullptr; FString SetterFunctionName; TArray>& Properties = ClassDef.GetProperties(); bool bExplicitSetter = false; // First check if there are any properties that already specify this function name as a setter for (TSharedRef& 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 (!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 (PropertyWithSetter->GetArrayDimensions()) { PropertyType += TEXT("*"); } 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>& 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& 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 (!PropertyWithGetter) { return false; } FString ExtendedPropertyType; FString PropertyType = PropertyWithGetter->GetCPPType(&ExtendedPropertyType, CPPF_Implementation | CPPF_ArgumentOrReturnValue); PropertyType += ExtendedPropertyType; if (PropertyWithGetter->GetArrayDimensions()) { PropertyType += TEXT("*"); } 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(ClassDef->GetSuperStructInfo().Struct)) { ClassDef->SetClassCastFlags(SuperClassDef->GetClassCastFlags()); } // Add the class flags from the interfaces for (FUnrealStructDefinitionInfo::FBaseStructInfo& BaseClassInfo : ClassDef->GetBaseStructInfos()) { if (FUnrealClassDefinitionInfo* BaseClassDef = UHTCast(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 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 MetaData; TArray 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 MetaData; // Build up a list of interface specifiers TArray 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 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; } // look out for the next aggregate name method static const FString GetNextAggregateNameString = TEXT("GetNextAggregateName"); if(MethodInfo.Name == GetNextAggregateNameString) { FRigVMStructInfo& StructRigVMInfo = StructDef.GetRigVMInfo(); StructRigVMInfo.bHasGetNextAggregateNameMethod = 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"); const TCHAR* FHeaderParser::FTScriptInterfaceText = TEXT("TScriptInterface"); void FHeaderParser::ParseRigVMMethodParameters(FUnrealStructDefinitionInfo& StructDef) { FRigVMStructInfo& StructRigVMInfo = StructDef.GetRigVMInfo(); if (!StructRigVMInfo.bHasRigVM) { return; } // validate the property types for this struct for (FUnrealPropertyDefinitionInfo* PropertyDef : TUHTFieldRange(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().IsObject()) { LogError(TEXT("RigVM Struct '%s' - Member '%s' is a UObject - object types are not allowed on structs with RIGVM_METHOD."), *StructDef.GetName(), *Parameter.Name); } #endif #if !UE_RIGVM_UINTERFACE_PROPERTIES_ENABLED if (PropertyDef->GetPropertyBase().IsInterface()) { LogError(TEXT("RigVM Struct '%s' - Member '%s' is a UInterface - interface types are not allowed on structs with RIGVM_METHOD."), *StructDef.GetName(), *Parameter.Name); } #endif if (!ExtendedCPPType.IsEmpty()) { // we only support arrays & script interfaces - no maps or similar data structures if (MemberCPPType != TArrayText && MemberCPPType != TEnumAsByteText && MemberCPPType != FTScriptInterfaceText) { 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(""), *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* 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 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 MetaData; AddModuleRelativePathToMetadata(SourceFile, MetaData); FFuncInfo FuncInfo; // If this is a UDELEGATE, parse the specifiers first FString DelegateMacro; if (SpecifierAction == EDelegateSpecifierAction::Parse) { TArray 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(); 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 MetaData; AddModuleRelativePathToMetadata(SourceFile, MetaData); // New-style UFUNCTION() syntax TArray 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 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(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& 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 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(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(InStructDef.GetSuperStructInfo().Struct)) { if (!ValidateScriptStructOkForNet(OriginStructName, *SuperScriptStructDef)) { bIsStructValid = false; } } for (TSharedRef 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 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()); } } // If the accessor tag was found but need to be autogenerated. if (NewPropDef.GetPropertyBase().bSetterTagFound && NewPropDef.GetPropertyBase().SetterName.IsEmpty()) { NewPropDef.GetPropertyBase().SetterName = FString::Printf(TEXT("Set%s"), *NewPropDef.GetName()); } if (NewPropDef.GetPropertyBase().bGetterTagFound && NewPropDef.GetPropertyBase().GetterName.IsEmpty()) { NewPropDef.GetPropertyBase().GetterName = FString::Printf(TEXT("Get%s"), *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& 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 SourceFilesRequired; for (FHeaderProvider& Include : SourceFile.GetIncludes()) { if (Include.GetId() == ObjectHeader) { continue; } if (FUnrealSourceFile* DepFile = Include.Resolve(SourceFile)) { SourceFilesRequired.Add(DepFile); } } for (const TSharedRef& 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 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 DelegateCache; FixupDelegateProperties(TypeDef->AsStructChecked(), *TypeDef->GetScope(), DelegateCache); } } } // Fix up any delegates themselves, if they refer to other delegates { TMap 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::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& TypeDef : SourceFile.GetDefinedClasses()) { FUnrealClassDefinitionInfo& ClassDef = UHTCastChecked(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& ClassDef : SourceFile.GetDefinedClasses()) { ClassDef->AsClassChecked().MarkParsed(); } // Perform any final concurrent finalization for (const TSharedRef& 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(""); } 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> 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 (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 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 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 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 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 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 MetaData; TArray 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 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 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 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 EnumDef = MakeShareable(new FUnrealEnumDefinitionInfo(SourceFile, InLineNumber, FString(EnumToken.Value), FName(EnumToken.Value, FNAME_Add), CppForm, UnderlyingType)); return EnumDef; } // // Compile an structure definition. // TSharedRef 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 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 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(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; } const bool bCanImplementInBlueprints = InterfaceDef->GetBoolMetaData(NAME_IsBlueprintBase); const bool bCannotImplementInBlueprints = (!bCanImplementInBlueprints && InterfaceDef->HasMetaData(NAME_IsBlueprintBase)) || InterfaceDef->HasMetaData(NAME_CannotImplementInterfaceInBlueprint); // So iterate over all functions this interface declares for (FUnrealFunctionDefinitionInfo* InterfaceFunctionDef : TUHTFieldRange(*InterfaceDef, EFieldIteratorFlags::ExcludeSuper)) { bool bImplemented = false; // And try to find one that matches for (FUnrealFunctionDefinitionInfo* ClassFunctionDef : TUHTFieldRange(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 // This is only relevant for bp-implementable interfaces, for native interfaces the interface-defined function is sufficient if (!bImplemented && InterfaceFunctionDef->HasAnyFunctionFlags(FUNC_BlueprintCallable) && !InterfaceFunctionDef->HasAnyFunctionFlags(FUNC_BlueprintEvent) && !bCannotImplementInBlueprints) { 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 DelegateCache; FixupDelegateProperties(PoppedFunctionDef, *GetCurrentScope(), DelegateCache); } } void FHeaderParser::PostPopNestInterface(FUnrealClassDefinitionInfo& CurrentInterfaceDef) { if (CurrentInterfaceDef.ContainsDelegates()) { TMap 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& MetaData, const TArray>& 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 ToolTipToEntry; for (const TMap& 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 ToolTipToPropertyName; for (TSharedRef 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 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(StructDef)) { TMap ToolTipToFunc; for (TSharedRef 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 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 ExistingFields; for (TSharedRef 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& 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 ToolTipToParam; for (TPair& 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(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 FuncDefRef = MakeShared(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; }