// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Threading;
using EpicGames.Core;
using EpicGames.UHT.Parsers;
using EpicGames.UHT.Tables;
using EpicGames.UHT.Tokenizer;
using EpicGames.UHT.Types;
using EpicGames.UHT.Utils;
namespace EpicGames.UHT.Parsers
{
///
/// Options that customize the parsing of properties.
///
[Flags]
public enum UhtPropertyParseOptions
{
///
/// No options
///
None = 0,
///
/// Don't automatically mark properties as CPF_Const
///
NoAutoConst = 1 << 0,
///
/// Parse for the layout macro
///
ParseLayoutMacro = 1 << 1,
///
/// If set, then the name of the property will be parsed with the type
///
FunctionNameIncluded = 1 << 2,
///
/// If set, then the name of the property will be parsed with the type
///
NameIncluded = 1 << 3,
///
/// When parsing delegates, the name is separated by commas
///
CommaSeparatedName = 1 << 4,
///
/// Multiple properties can be defined separated by commas
///
List = 1 << 5,
///
/// Don't add a return type to the property list (return values go at the end)
///
DontAddReturn = 1 << 6,
///
/// If set, add the module relative path to the parameter's meta data
///
AddModuleRelativePath = 1 << 7,
}
///
/// Helper methods for testing flags. These methods perform better than the generic HasFlag which hits
/// the GC and stalls.
///
public static class UhtParsePropertyOptionsExtensions
{
///
/// Test to see if any of the specified flags are set
///
/// Current flags
/// Flags to test for
/// True if any of the flags are set
public static bool HasAnyFlags(this UhtPropertyParseOptions inFlags, UhtPropertyParseOptions testFlags)
{
return (inFlags & testFlags) != 0;
}
///
/// Test to see if all of the specified flags are set
///
/// Current flags
/// Flags to test for
/// True if all the flags are set
public static bool HasAllFlags(this UhtPropertyParseOptions inFlags, UhtPropertyParseOptions testFlags)
{
return (inFlags & testFlags) == testFlags;
}
///
/// Test to see if a specific set of flags have a specific value.
///
/// Current flags
/// Flags to test for
/// Expected value of the tested flags
/// True if the given flags have a specific value.
public static bool HasExactFlags(this UhtPropertyParseOptions inFlags, UhtPropertyParseOptions testFlags, UhtPropertyParseOptions matchFlags)
{
return (inFlags & testFlags) == matchFlags;
}
}
///
/// Layout macro type
///
public enum UhtLayoutMacroType
{
///
/// None found
///
None,
///
/// Array
///
Array,
///
/// Editor only array
///
ArrayEditorOnly,
///
/// Bit field
///
Bitfield,
///
/// Editor only bit field
///
BitfieldEditorOnly,
///
/// Field
///
Field,
///
/// Editor only field
///
FieldEditorOnly,
///
/// Field with initializer
///
FieldInitialized,
}
///
/// Extensions for working with the layout macro type
///
public static class UhtLayoutMacroTypeExtensions
{
///
/// Return true if the type is editor only
///
/// Layout macro type
/// True if editor only
public static bool IsEditorOnly(this UhtLayoutMacroType layoutMacroType)
{
switch (layoutMacroType)
{
case UhtLayoutMacroType.ArrayEditorOnly:
case UhtLayoutMacroType.BitfieldEditorOnly:
case UhtLayoutMacroType.FieldEditorOnly:
return true;
default:
return false;
}
}
///
/// Return true if the type is a bit field
///
/// Layout macro type
/// True if bit field
public static bool IsBitfield(this UhtLayoutMacroType layoutMacroType)
{
switch (layoutMacroType)
{
case UhtLayoutMacroType.Bitfield:
case UhtLayoutMacroType.BitfieldEditorOnly:
return true;
default:
return false;
}
}
///
/// Return true if the type is an array
///
/// Layout macro type
/// True if array
public static bool IsArray(this UhtLayoutMacroType layoutMacroType)
{
switch (layoutMacroType)
{
case UhtLayoutMacroType.Array:
case UhtLayoutMacroType.ArrayEditorOnly:
return true;
default:
return false;
}
}
///
/// Return true if the type has an initializer
///
/// Layout macro type
/// True if it has an initializer
public static bool HasInitializer(this UhtLayoutMacroType layoutMacroType)
{
switch (layoutMacroType)
{
case UhtLayoutMacroType.FieldInitialized:
return true;
default:
return false;
}
}
///
/// Return the layout macro name
///
/// Type in question
/// Macro name
/// Thrown if the macro type is none or invalid
public static StringView MacroName(this UhtLayoutMacroType layoutMacroType)
{
switch (layoutMacroType)
{
default:
case UhtLayoutMacroType.None:
throw new UhtIceException("Invalid macro name for ELayoutMacroType");
case UhtLayoutMacroType.Array:
return "LAYOUT_ARRAY";
case UhtLayoutMacroType.ArrayEditorOnly:
return "LAYOUT_ARRAY_EDITORONLY";
case UhtLayoutMacroType.Bitfield:
return "LAYOUT_BITFIELD";
case UhtLayoutMacroType.BitfieldEditorOnly:
return "LAYOUT_BITFIELD_EDITORONLY";
case UhtLayoutMacroType.Field:
return "LAYOUT_FIELD";
case UhtLayoutMacroType.FieldEditorOnly:
return "LAYOUT_FIELD_EDITORONLY";
case UhtLayoutMacroType.FieldInitialized:
return "LAYOUT_FIELD_INITIALIZED";
}
}
///
/// Return the macro name and value
///
/// Macro name
/// Name and type
public static KeyValuePair MacroNameAndValue(this UhtLayoutMacroType layoutMacroType)
{
return new KeyValuePair(layoutMacroType.MacroName(), layoutMacroType);
}
}
///
/// Delegate invoked to handle a parsed property
///
/// Scope being parsed
/// Property just parsed
/// Name of the property
/// Layout macro type
public delegate void UhtPropertyDelegate(UhtParsingScope topScope, UhtProperty property, ref UhtToken nameToken, UhtLayoutMacroType layoutMacroType);
///
/// Context for property specifier parsing
///
public class UhtPropertySpecifierContext : UhtSpecifierContext
{
///
/// The property settings being parsed
///
public UhtPropertySettings PropertySettings { get; } = new UhtPropertySettings();
///
/// If true, editor specifier seen
///
public bool SeenEditSpecifier { get; set; } = false;
///
/// If true, blueprint write specifier seen
///
public bool SeenBlueprintWriteSpecifier { get; set; } = false;
///
/// If true, blueprint readonly specifier seen
///
public bool SeenBlueprintReadOnlySpecifier { get; set; } = false;
///
/// If true, blueprint getter specifier seen
///
public bool SeenBlueprintGetterSpecifier { get; set; } = false;
}
///
/// Helper class thread specified object cache
///
public readonly struct UhtThreadBorrower : IDisposable where T : new()
{
private static readonly ThreadLocal> s_tls = new(() => new());
private readonly T _instance;
///
/// The borrowed instance
///
public T Instance => _instance;
///
/// Request a context
///
public UhtThreadBorrower(bool _) // argument needed for vs2019
{
List cache = s_tls.Value!;
if (cache.Count == 0)
{
_instance = new();
}
else
{
_instance = cache[^1];
cache.RemoveAt(cache.Count - 1);
}
}
///
/// Return the borrowed buffer to the cache
///
public void Dispose()
{
s_tls.Value!.Add(_instance);
}
}
///
/// A parsed property is a property that was parsed but couldn't yet be resolved. It retains the list of tokens needed
/// to resolve the type of the property. It will be replaced with the resolved property type during property resolution.
///
public class UhtPreResolveProperty : UhtProperty
{
///
public override string EngineClassName => "UHTParsedProperty";
///
protected override string CppTypeText => "invalid";
///
protected override string PGetMacroText => "invalid";
///
/// Property settings being parsed
///
public UhtPropertySettings PropertySettings { get; set; }
///
/// Construct a new property to be resolved
///
/// Property settings
public UhtPreResolveProperty(UhtPropertySettings propertySettings) : base(propertySettings)
{
PropertySettings = propertySettings;
}
///
public override bool IsSameType(UhtProperty other)
{
throw new NotImplementedException();
}
///
public override StringBuilder AppendText(StringBuilder builder, UhtPropertyTextType textType, bool isTemplateArgument)
{
throw new NotImplementedException();
}
///
public override StringBuilder AppendMemberDecl(StringBuilder builder, IUhtPropertyMemberContext context, string name, string nameSuffix, int tabs)
{
throw new NotImplementedException();
}
///
public override StringBuilder AppendMemberDef(StringBuilder builder, IUhtPropertyMemberContext context, string name, string nameSuffix, string? offset, int tabs)
{
throw new NotImplementedException();
}
///
public override StringBuilder AppendFunctionThunkParameterGet(StringBuilder builder)
{
throw new NotImplementedException();
}
///
public override StringBuilder AppendNullConstructorArg(StringBuilder builder, bool isInitializer)
{
throw new NotImplementedException();
}
///
public override bool SanitizeDefaultValue(IUhtTokenReader defaultValueReader, StringBuilder innerDefaultValue)
{
throw new NotImplementedException();
}
}
internal record struct UhtPropertyCategoryExtraContext(UhtPropertyCategory Category) : IUhtMessageExtraContext
{
#region IMessageExtraContext implementation
///
public IEnumerable? MessageExtraContext
{
get
{
Stack extraContext = new(1);
extraContext.Push(Category.GetHintText());
return extraContext;
}
}
#endregion
}
///
/// Property parser
///
public static class UhtPropertyParser
{
private static readonly Dictionary s_layoutMacroTypes = new(new[]
{
UhtLayoutMacroType.Array.MacroNameAndValue(),
UhtLayoutMacroType.ArrayEditorOnly.MacroNameAndValue(),
UhtLayoutMacroType.Bitfield.MacroNameAndValue(),
UhtLayoutMacroType.BitfieldEditorOnly.MacroNameAndValue(),
UhtLayoutMacroType.Field.MacroNameAndValue(),
UhtLayoutMacroType.FieldEditorOnly.MacroNameAndValue(),
UhtLayoutMacroType.FieldInitialized.MacroNameAndValue(),
});
///
/// Parse the property
///
/// Current parsing scope
/// Flags to be disallowed
/// Parsing options
/// Property category
/// Delegate to be invoked after property has been parsed
/// The property parser
public static void Parse(UhtParsingScope topScope, EPropertyFlags disallowPropertyFlags, UhtPropertyParseOptions options, UhtPropertyCategory category, UhtPropertyDelegate propertyDelegate)
{
// Initialize the property context
using UhtThreadBorrower borrower = new(true);
UhtPropertySpecifierContext specifierContext = borrower.Instance;
specifierContext.Type = topScope.ScopeType;
specifierContext.TokenReader = topScope.TokenReader;
specifierContext.AccessSpecifier = topScope.AccessSpecifier;
specifierContext.MessageSite = topScope.TokenReader;
specifierContext.PropertySettings.Reset(specifierContext.Type, 0, category, disallowPropertyFlags);
specifierContext.MetaData = specifierContext.PropertySettings.MetaData;
specifierContext.MetaNameIndex = UhtMetaData.IndexNone;
specifierContext.SeenEditSpecifier = false;
specifierContext.SeenBlueprintWriteSpecifier = false;
specifierContext.SeenBlueprintReadOnlySpecifier = false;
specifierContext.SeenBlueprintGetterSpecifier = false;
UhtPropertyCategoryExtraContext extraContext = new(category);
using UhtMessageContext tokenContext = new(extraContext);
ParseInternal(topScope, options, specifierContext, propertyDelegate);
}
///
/// Resolve the given property. This method will resolve any immediate property during the parsing phase or
/// resolve any previously parsed property to the final version.
///
/// Used to detect if the property is being parsed or resolved
/// The property settings.
///
private static UhtProperty? ResolveProperty(UhtPropertyResolvePhase resolvePhase, UhtPropertySettings propertySettings)
{
Debug.Assert(propertySettings.TypeTokens.AllTokens.Length > 0);
using UhtTokenReplayReaderBorrower borrowedReader = new(propertySettings.Outer, propertySettings.Outer.HeaderFile.Data.Memory, propertySettings.TypeTokens.AllTokens, UhtTokenType.EndOfType);
UhtPropertyResolveArgs args = new(resolvePhase, propertySettings, borrowedReader.Reader);
return args.ResolveProperty();
}
private static readonly ThreadLocal s_tlsPropertySettings = new(() => { return new UhtPropertySettings(); });
///
/// Given a type with children, resolve any children that couldn't be resolved during the parsing phase.
///
/// The type with children
/// Parsing options
public static void ResolveChildren(UhtType type, UhtPropertyParseOptions options)
{
UhtPropertyOptions propertyOptions = UhtPropertyOptions.None;
if (options.HasAnyFlags(UhtPropertyParseOptions.NoAutoConst))
{
propertyOptions |= UhtPropertyOptions.NoAutoConst;
}
bool inSymbolTable = type.EngineType.AddChildrenToSymbolTable();
UhtPropertySettings? propertySettings = s_tlsPropertySettings.Value;
if (propertySettings == null)
{
throw new UhtIceException("Unable to acquire threaded property settings");
}
for (int index = 0; index < type.Children.Count; ++index)
{
if (type.Children[index] is UhtPreResolveProperty property)
{
propertySettings.Reset(property, propertyOptions);
UhtProperty? resolved = ResolveProperty(UhtPropertyResolvePhase.Resolving, propertySettings);
if (resolved != null)
{
if (inSymbolTable && resolved != property)
{
type.Session.ReplaceTypeInSymbolTable(property, resolved);
}
type.Children[index] = resolved;
}
}
}
}
///
/// Resolve the given property and finish parsing any postfix settings
///
/// Arguments for the resolution process
/// Property type used to resolve the property
/// Created property or null if it could not be resolved
///
public static UhtProperty? ResolvePropertyType(UhtPropertyResolveArgs args, UhtPropertyType propertyType)
{
UhtProperty? outProperty = propertyType.Delegate(args);
if (outProperty == null)
{
return null;
}
// If this is a simple type, skip the type
if (propertyType.Options.HasAnyFlags(UhtPropertyTypeOptions.Simple))
{
if (!args.SkipExpectedType())
{
return null;
}
}
// Handle any trailing const
if (outProperty.PropertyCategory == UhtPropertyCategory.Member)
{
//@TODO: UCREMOVAL: 'const' member variables that will get written post-construction by defaultproperties
UhtClass? outerClass = outProperty.Outer as UhtClass;
if (outerClass != null && outerClass.ClassFlags.HasAnyFlags(EClassFlags.Const))
{
// Eat a 'not quite truthful' const after the type; autogenerated for member variables of const classes.
if (args.TokenReader.TryOptional("const"))
{
outProperty.MetaData.Add(UhtNames.NativeConst, "");
}
}
}
else
{
if (args.TokenReader.TryOptional("const"))
{
outProperty.MetaData.Add(UhtNames.NativeConst, "");
outProperty.PropertyFlags |= EPropertyFlags.ConstParm;
}
}
// Check for unexpected '*'
if (args.TokenReader.TryOptional('*'))
{
args.TokenReader.LogError($"Inappropriate '*' on variable of type '{outProperty.GetUserFacingDecl()}', cannot have an exposed pointer to this type.");
}
// Arrays are passed by reference but are only implicitly so; setting it explicitly could cause a problem with replicated functions
if (args.TokenReader.TryOptional('&'))
{
switch (outProperty.PropertyCategory)
{
case UhtPropertyCategory.RegularParameter:
case UhtPropertyCategory.Return:
outProperty.PropertyFlags |= EPropertyFlags.OutParm;
//@TODO: UCREMOVAL: How to determine if we have a ref param?
if (outProperty.PropertyFlags.HasAnyFlags(EPropertyFlags.ConstParm))
{
outProperty.PropertyFlags |= EPropertyFlags.ReferenceParm;
}
break;
case UhtPropertyCategory.ReplicatedParameter:
outProperty.PropertyFlags |= EPropertyFlags.ReferenceParm;
break;
default:
break;
}
if (outProperty.PropertyFlags.HasAnyFlags(EPropertyFlags.ConstParm))
{
outProperty.RefQualifier = UhtPropertyRefQualifier.ConstRef;
}
else
{
outProperty.RefQualifier = UhtPropertyRefQualifier.NonConstRef;
}
}
if (!args.IsTemplate)
{
if (!args.TokenReader.IsEOF)
{
throw new UhtTokenException(args.TokenReader, args.TokenReader.PeekToken(), "end of type declaration");
}
}
else
{
ref UhtToken token = ref args.TokenReader.PeekToken();
if (!token.IsSymbol(',') && !token.IsSymbol('>'))
{
throw new UhtTokenException(args.TokenReader, args.TokenReader.PeekToken(), "end of type declaration");
}
}
return outProperty;
}
private static void ParseInternal(UhtParsingScope topScope, UhtPropertyParseOptions options, UhtPropertySpecifierContext specifierContext, UhtPropertyDelegate propertyDelegate)
{
UhtPropertySettings propertySettings = specifierContext.PropertySettings;
IUhtTokenReader tokenReader = topScope.TokenReader;
propertySettings.LineNumber = tokenReader.InputLine;
if (propertySettings.PropertyCategory == UhtPropertyCategory.Member)
{
UhtCompilerDirective compilerDirective = topScope.HeaderParser.GetCurrentCompositeCompilerDirective();
if (compilerDirective.HasAnyFlags(UhtCompilerDirective.WithEditorOnlyData))
{
propertySettings.PropertyFlags |= EPropertyFlags.EditorOnly;
propertySettings.DefineScope |= UhtDefineScope.EditorOnlyData;
}
else if (compilerDirective.HasAnyFlags(UhtCompilerDirective.WithEditor))
{
// 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
UHTManifest.Module module = topScope.Module.Module;
bool isEditorModule =
module.ModuleType == UHTModuleType.EngineEditor ||
module.ModuleType == UHTModuleType.GameEditor ||
module.ModuleType == UHTModuleType.EngineUncooked ||
module.ModuleType == UHTModuleType.GameUncooked;
if (!isEditorModule)
{
tokenReader.LogError("UProperties should not be wrapped by WITH_EDITOR, use WITH_EDITORONLY_DATA instead.");
}
}
propertySettings.DefineScope |= compilerDirective.GetDefaultDefineScopes();
}
// Parse type information including UPARAM that might appear in template arguments
PreParseType(specifierContext, false);
// Swallow inline keywords
if (propertySettings.PropertyCategory == UhtPropertyCategory.Return)
{
tokenReader
.Optional("inline")
.Optional("FORCENOINLINE")
.OptionalStartsWith("FORCEINLINE");
}
// Handle MemoryLayout.h macros
bool hasWrapperBrackets = false;
UhtLayoutMacroType layoutMacroType = UhtLayoutMacroType.None;
if (options.HasAnyFlags(UhtPropertyParseOptions.ParseLayoutMacro))
{
ref UhtToken layoutToken = ref tokenReader.PeekToken();
if (layoutToken.IsIdentifier())
{
if (s_layoutMacroTypes.TryGetValue(layoutToken.Value, out layoutMacroType))
{
tokenReader.ConsumeToken();
tokenReader.Require('(');
hasWrapperBrackets = tokenReader.TryOptional('(');
if (layoutMacroType.IsEditorOnly())
{
propertySettings.PropertyFlags |= EPropertyFlags.EditorOnly;
propertySettings.DefineScope |= UhtDefineScope.EditorOnlyData;
}
}
}
// This exists as a compatibility "shim" with UHT4/5.0. If the fetched token wasn't an identifier,
// it wasn't returned to the tokenizer. So, just consume the token here. In theory, this should be
// removed once we have a good deprecated system.
//@TODO - deprecate
else // if (LayoutToken.IsSymbol(';'))
{
tokenReader.ConsumeToken();
}
}
//@TODO: Should flag as settable from a const context, but this is at least good enough to allow use for C++ land
tokenReader.Optional("mutable");
// Gather the type tokens and possibly the property name.
UhtTypeTokens typeTokens = UhtTypeTokens.Gather(tokenReader);
// Verify we at least have one type
if (typeTokens.AllTokens.Length < 1)
{
throw new UhtException(tokenReader, $"{propertySettings.PropertyCategory.GetHintText()}: Missing variable type or name");
}
// Consume the wrapper brackets. This is just an extra set
if (hasWrapperBrackets)
{
tokenReader.Require(')');
}
// Check for any disallowed flags
if (propertySettings.PropertyFlags.HasAnyFlags(propertySettings.DisallowPropertyFlags))
{
EPropertyFlags extraFlags = propertySettings.PropertyFlags & propertySettings.DisallowPropertyFlags;
tokenReader.LogError($"Specified type modifiers not allowed here '{String.Join(" | ", extraFlags.ToStringList())}'");
}
if (options.HasAnyFlags(UhtPropertyParseOptions.AddModuleRelativePath))
{
UhtParsingScope.AddModuleRelativePathToMetaData(propertySettings.MetaData, topScope.HeaderFile);
}
// Fetch the name of the property, bitfield and array size
if (layoutMacroType != UhtLayoutMacroType.None)
{
tokenReader.Require(',');
UhtToken nameToken = tokenReader.GetIdentifier();
if (layoutMacroType.IsArray())
{
tokenReader.Require(',');
RequireArray(tokenReader, propertySettings, ref nameToken, ')');
tokenReader.Require(')');
}
else if (layoutMacroType.IsBitfield())
{
tokenReader.Require(',');
RequireBitfield(tokenReader, propertySettings, ref nameToken);
tokenReader.Require(')');
}
else if (layoutMacroType.HasInitializer())
{
tokenReader.Require(',');
tokenReader.SkipBrackets('(', ')', 1); // consumes ending ) too
}
else
{
tokenReader.Require(')');
}
Finalize(topScope, options, specifierContext, ref nameToken, typeTokens, layoutMacroType, propertyDelegate);
}
else if (options.HasAnyFlags(UhtPropertyParseOptions.List))
{
UhtToken nameToken = typeTokens.ExtractTrailingIdentifier(tokenReader, propertySettings);
CheckForOptionalParts(tokenReader, propertySettings, ref nameToken);
Finalize(topScope, options, specifierContext, ref nameToken, typeTokens, layoutMacroType, propertyDelegate);
// Check for unsupported comma delimited properties
if (tokenReader.TryOptional(','))
{
throw new UhtException(tokenReader, $"Comma delimited properties are not supported");
}
}
else if (options.HasAnyFlags(UhtPropertyParseOptions.CommaSeparatedName))
{
tokenReader.Require(',');
UhtToken nameToken = tokenReader.GetIdentifier();
CheckForOptionalParts(tokenReader, propertySettings, ref nameToken);
Finalize(topScope, options, specifierContext, ref nameToken, typeTokens, layoutMacroType, propertyDelegate);
}
else if (options.HasAnyFlags(UhtPropertyParseOptions.FunctionNameIncluded))
{
UhtToken nameToken = typeTokens.AllTokens.Span[^1];
nameToken.Value = new StringView("Function");
if (CheckForOptionalParts(tokenReader, propertySettings, ref nameToken))
{
nameToken = tokenReader.GetIdentifier("function name");
}
else
{
nameToken = typeTokens.ExtractTrailingIdentifier(tokenReader, propertySettings);
}
Finalize(topScope, options, specifierContext, ref nameToken, typeTokens, layoutMacroType, propertyDelegate);
}
else if (options.HasAnyFlags(UhtPropertyParseOptions.NameIncluded))
{
UhtToken nameToken = typeTokens.ExtractTrailingIdentifier(tokenReader, propertySettings);
CheckForOptionalParts(tokenReader, propertySettings, ref nameToken);
Finalize(topScope, options, specifierContext, ref nameToken, typeTokens, layoutMacroType, propertyDelegate);
}
else
{
UhtToken nameToken = new();
CheckForOptionalParts(tokenReader, propertySettings, ref nameToken);
Finalize(topScope, options, specifierContext, ref nameToken, typeTokens, layoutMacroType, propertyDelegate);
}
}
///
/// Parse the type elements excluding the type itself.
///
/// Context of what is being parsed
/// If true, this is part of a template argument
///
///
public static void PreParseType(UhtPropertySpecifierContext specifierContext, bool isTemplateArgument)
{
UhtPropertySettings propertySettings = specifierContext.PropertySettings;
IUhtTokenReader tokenReader = specifierContext.TokenReader;
UhtSession session = specifierContext.Type.Session;
// We parse specifiers when:
//
// 1. This is the start of a member property (but not a template)
// 2. The UPARAM identifier is found
bool isMember = propertySettings.PropertyCategory == UhtPropertyCategory.Member;
bool parseSpecifiers = (isMember && !isTemplateArgument) || tokenReader.TryOptional("UPARAM");
UhtSpecifierParser specifiers = UhtSpecifierParser.GetThreadInstance(specifierContext, "Variable",
isMember ? session.GetSpecifierTable(UhtTableNames.PropertyMember) : session.GetSpecifierTable(UhtTableNames.PropertyArgument));
if (parseSpecifiers)
{
specifiers.ParseSpecifiers();
}
if (propertySettings.PropertyCategory != UhtPropertyCategory.Member && !isTemplateArgument)
{
// const before the variable type support (only for params)
if (tokenReader.TryOptional("const"))
{
propertySettings.PropertyFlags |= EPropertyFlags.ConstParm;
propertySettings.MetaData.Add(UhtNames.NativeConst, "");
}
}
// Process the specifiers
if (parseSpecifiers)
{
specifiers.ParseDeferred();
}
// If we saw a BlueprintGetter but did not see BlueprintSetter or
// or BlueprintReadWrite then treat as BlueprintReadOnly
if (specifierContext.SeenBlueprintGetterSpecifier && !specifierContext.SeenBlueprintWriteSpecifier)
{
propertySettings.PropertyFlags |= EPropertyFlags.BlueprintReadOnly;
}
if (propertySettings.MetaData.ContainsKey(UhtNames.ExposeOnSpawn))
{
propertySettings.PropertyFlags |= EPropertyFlags.ExposeOnSpawn;
}
if (!isTemplateArgument)
{
UhtAccessSpecifier accessSpecifier = specifierContext.AccessSpecifier;
if (accessSpecifier == UhtAccessSpecifier.Public || propertySettings.PropertyCategory != UhtPropertyCategory.Member)
{
propertySettings.PropertyFlags &= ~EPropertyFlags.Protected;
propertySettings.PropertyExportFlags |= UhtPropertyExportFlags.Public;
propertySettings.PropertyExportFlags &= ~(UhtPropertyExportFlags.Private | UhtPropertyExportFlags.Protected);
propertySettings.PropertyFlags &= ~EPropertyFlags.NativeAccessSpecifiers;
propertySettings.PropertyFlags |= EPropertyFlags.NativeAccessSpecifierPublic;
}
else if (accessSpecifier == UhtAccessSpecifier.Protected)
{
propertySettings.PropertyFlags |= EPropertyFlags.Protected;
propertySettings.PropertyExportFlags |= UhtPropertyExportFlags.Protected;
propertySettings.PropertyExportFlags &= ~(UhtPropertyExportFlags.Public | UhtPropertyExportFlags.Private);
propertySettings.PropertyFlags &= ~EPropertyFlags.NativeAccessSpecifiers;
propertySettings.PropertyFlags |= EPropertyFlags.NativeAccessSpecifierProtected;
}
else if (accessSpecifier == UhtAccessSpecifier.Private)
{
propertySettings.PropertyFlags &= ~EPropertyFlags.Protected;
propertySettings.PropertyExportFlags |= UhtPropertyExportFlags.Private;
propertySettings.PropertyExportFlags &= ~(UhtPropertyExportFlags.Public | UhtPropertyExportFlags.Protected);
propertySettings.PropertyFlags &= ~EPropertyFlags.NativeAccessSpecifiers;
propertySettings.PropertyFlags |= EPropertyFlags.NativeAccessSpecifierPrivate;
}
else
{
throw new UhtIceException("Unknown access level");
}
}
}
///
/// Finish creating the property
///
/// Top most scope being parsed
/// Parsing options
/// Context of the property being parsed
/// The name of the property
/// Series of tokens that represent the type
/// Optional layout macro type being parsed
/// Delegate to invoke when processing has been completed
/// The newly created property. During the parsing phase, this will often be a temporary property if the type references engine types.
private static UhtProperty Finalize(UhtParsingScope topScope, UhtPropertyParseOptions options, UhtPropertySpecifierContext specifierContext, ref UhtToken nameToken,
UhtTypeTokens typeTokens, UhtLayoutMacroType layoutMacroType, UhtPropertyDelegate propertyDelegate)
{
UhtPropertySettings propertySettings = specifierContext.PropertySettings;
propertySettings.TypeTokens = new(typeTokens, 0);
IUhtTokenReader tokenReader = specifierContext.TokenReader;
propertySettings.SourceName = propertySettings.PropertyCategory == UhtPropertyCategory.Return ? "ReturnValue" : nameToken.Value.ToString();
// Try to resolve the property using any immediate mode property types
UhtProperty newProperty = ResolveProperty(UhtPropertyResolvePhase.Parsing, propertySettings) ?? new UhtPreResolveProperty(propertySettings);
// Force the category in non-engine projects
if (newProperty.PropertyCategory == UhtPropertyCategory.Member)
{
if (!newProperty.Module.IsPartOfEngine &&
newProperty.PropertyFlags.HasAnyFlags(EPropertyFlags.Edit | EPropertyFlags.BlueprintVisible) &&
!newProperty.MetaData.ContainsKey(UhtNames.Category))
{
newProperty.MetaData.Add(UhtNames.Category, newProperty.Outer!.EngineName);
}
}
// Check to see if the variable is deprecated, and if so set the flag
{
int deprecatedIndex = newProperty.SourceName.IndexOf("_DEPRECATED", StringComparison.Ordinal);
int nativizedPropertyPostfixIndex = newProperty.SourceName.IndexOf("__pf", StringComparison.Ordinal); //@TODO: check OverrideNativeName in Meta Data, to be sure it's not a random occurrence of the "__pf" string.
bool ignoreDeprecatedWord = (nativizedPropertyPostfixIndex != -1) && (nativizedPropertyPostfixIndex > deprecatedIndex);
if ((deprecatedIndex != -1) && !ignoreDeprecatedWord)
{
if (deprecatedIndex != newProperty.SourceName.Length - 11)
{
tokenReader.LogError("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
bool blueprintVisible = newProperty.PropertyFlags.HasAnyFlags(EPropertyFlags.BlueprintVisible);
bool warnOnGetter = blueprintVisible && !newProperty.MetaData.ContainsKey(UhtNames.BlueprintGetter);
bool warnOnSetter = blueprintVisible && !newProperty.PropertyFlags.HasAnyFlags(EPropertyFlags.BlueprintReadOnly) && !newProperty.MetaData.ContainsKey(UhtNames.BlueprintSetter);
if (warnOnGetter)
{
tokenReader.LogWarning($"{newProperty.PropertyCategory.GetHintText()}: Deprecated property '{newProperty.SourceName}' should not be marked as blueprint visible without having a BlueprintGetter");
}
if (warnOnSetter)
{
tokenReader.LogWarning($"{newProperty.PropertyCategory.GetHintText()}: Deprecated property '{newProperty.SourceName}' should not be marked as blueprint writable without having a BlueprintSetter");
}
// Warn if a deprecated property is visible
if (newProperty.PropertyFlags.HasAnyFlags(EPropertyFlags.Edit | EPropertyFlags.EditConst) || // Property is marked as editable
(!blueprintVisible && newProperty.PropertyFlags.HasAnyFlags(EPropertyFlags.BlueprintReadOnly) &&
!newProperty.PropertyExportFlags.HasAnyFlags(UhtPropertyExportFlags.ImpliedBlueprintPure))) // Is BPRO, but not via Implied Flags and not caught by Getter/Setter path above
{
tokenReader.LogWarning($"{newProperty.PropertyCategory.GetHintText()}: Deprecated property '{newProperty.SourceName}' should not be marked as visible or editable");
}
newProperty.PropertyFlags |= EPropertyFlags.Deprecated;
newProperty.EngineName = newProperty.SourceName[..deprecatedIndex];
}
}
// Try gathering metadata for member fields
if (newProperty.PropertyCategory == UhtPropertyCategory.Member)
{
UhtSpecifierParser specifiers = UhtSpecifierParser.GetThreadInstance(specifierContext, newProperty.SourceName,
specifierContext.Type.Session.GetSpecifierTable(UhtTableNames.PropertyMember));
specifiers.ParseFieldMetaData();
tokenReader.SkipWhitespaceAndComments(); //TODO - old UHT compatibility. Commented out initializers can cause comment/tooltip to be used as meta data.
tokenReader.CommitPendingComments(); //TODO - old UHT compatibility. Commented out initializers can cause comment/tooltip to be used as meta data.
topScope.AddFormattedCommentsAsTooltipMetaData(newProperty);
}
// Adjust the name for verse
(bool wasMangled, string result) = newProperty.GetMangledEngineName();
if (wasMangled)
{
if (!newProperty.MetaData.ContainsKey(UhtNames.DisplayName))
{
newProperty.MetaData.Add(UhtNames.DisplayName, newProperty.StrippedEngineName);
}
newProperty.EngineName = result;
}
propertyDelegate(topScope, newProperty, ref nameToken, layoutMacroType);
// Void properties don't get added when they are the return value
if (newProperty.PropertyCategory != UhtPropertyCategory.Return || !options.HasAnyFlags(UhtPropertyParseOptions.DontAddReturn))
{
topScope.ScopeType.AddChild(newProperty);
}
return newProperty;
}
private static bool CheckForOptionalParts(IUhtTokenReader tokenReader, UhtPropertySettings propertySettings, ref UhtToken nameToken)
{
bool gotOptionalParts = false;
if (tokenReader.TryOptional('['))
{
RequireArray(tokenReader, propertySettings, ref nameToken, ']');
tokenReader.Require(']');
gotOptionalParts = true;
}
if (tokenReader.TryOptional(':'))
{
RequireBitfield(tokenReader, propertySettings, ref nameToken);
gotOptionalParts = true;
}
return gotOptionalParts;
}
private static void RequireBitfield(IUhtTokenReader tokenReader, UhtPropertySettings propertySettings, ref UhtToken nameToken)
{
if (!tokenReader.TryOptionalConstInt(out int bitfieldSize) || bitfieldSize != 1)
{
throw new UhtException(tokenReader, $"Bad or missing bit field size for '{nameToken.Value}', must be 1.");
}
propertySettings.IsBitfield = true;
}
private static void RequireArray(IUhtTokenReader tokenReader, UhtPropertySettings propertySettings, ref UhtToken nameToken, char terminator)
{
// Ignore how the actual array dimensions are actually defined - we'll calculate those with the compiler anyway.
propertySettings.ArrayDimensions = tokenReader.GetRawString(terminator, UhtRawStringOptions.DontConsumeTerminator).ToString();
if (propertySettings.ArrayDimensions.Length == 0)
{
throw new UhtException(tokenReader, $"{propertySettings.PropertyCategory.GetHintText()} {nameToken.Value}: Missing array dimensions or terminating '{terminator}'");
}
}
}
[UnrealHeaderTool]
static class PropertyKeywords
{
#region Keywords
[UhtKeyword(Extends = UhtTableNames.Class)]
[UhtKeyword(Extends = UhtTableNames.ScriptStruct)]
[SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "Attribute accessed method")]
[SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Attribute accessed method")]
private static UhtParseResult UPROPERTYKeyword(UhtParsingScope topScope, UhtParsingScope actionScope, ref UhtToken token)
{
UhtPropertyParseOptions options = UhtPropertyParseOptions.ParseLayoutMacro | UhtPropertyParseOptions.List | UhtPropertyParseOptions.AddModuleRelativePath;
UhtPropertyParser.Parse(topScope, EPropertyFlags.ParmFlags, options, UhtPropertyCategory.Member, s_propertyDelegate);
topScope.TokenReader.Require(';');
// C++ UHT TODO - Skip any extra ';'. This can be removed if we remove UhtHeaderfileParser.ParserStatement generating errors
// when extra ';' are found. Oddly, UPROPERTY specifically skips extra ';'
while (true)
{
UhtToken nextToken = topScope.TokenReader.PeekToken();
if (!nextToken.IsSymbol(';'))
{
break;
}
topScope.TokenReader.ConsumeToken();
}
return UhtParseResult.Handled;
}
#endregion
private static readonly UhtPropertyDelegate s_propertyDelegate = PropertyParsed;
private static void PropertyParsed(UhtParsingScope topScope, UhtProperty property, ref UhtToken nameToken, UhtLayoutMacroType layoutMacroType)
{
IUhtTokenReader tokenReader = topScope.TokenReader;
// Skip any initialization
if (tokenReader.TryOptional('='))
{
tokenReader.SkipUntil(';');
}
else if (tokenReader.TryOptional('{'))
{
tokenReader.SkipBrackets('{', '}', 1);
}
}
}
[UnrealHeaderTool]
static class UhtVerseTypeParser
{
[UhtPropertyType(Keyword = "TVal", Options = UhtPropertyTypeOptions.Immediate)]
[SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "Attribute accessed method")]
[SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Attribute accessed method")]
private static UhtProperty? TValType(UhtPropertyResolveArgs args)
{
return args.ParseWrappedType(EPropertyFlags.None, UhtPropertyExportFlags.TVal);
}
[UhtPropertyType(Keyword = "verse::TPtr", Options = UhtPropertyTypeOptions.Immediate)]
[SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "Attribute accessed method")]
[SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Attribute accessed method")]
private static UhtProperty? VerseTPtrType(UhtPropertyResolveArgs args)
{
return args.ParseWrappedType(EPropertyFlags.None, UhtPropertyExportFlags.TPtr);
}
[UhtPropertyType(Keyword = "TVerseTask", Options = UhtPropertyTypeOptions.Immediate)]
[SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "Attribute accessed method")]
[SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Attribute accessed method")]
private static UhtProperty? VerseTaskType(UhtPropertyResolveArgs args)
{
return args.ParseWrappedType(EPropertyFlags.None, UhtPropertyExportFlags.TVerseTask);
}
}
[UnrealHeaderTool]
static class UhtDefaultPropertyParser
{
[UhtPropertyType(Options = UhtPropertyTypeOptions.Default)]
[SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "Attribute accessed method")]
[SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Attribute accessed method")]
private static UhtProperty? DefaultProperty(UhtPropertyResolveArgs args)
{
UhtProperty? property;
UhtPropertySettings propertySettings = args.PropertySettings;
IUhtTokenReader tokenReader = args.TokenReader;
int typeStartPos = tokenReader.PeekToken().InputStartPos;
bool gotConst = tokenReader.TryOptional("const");
UhtFindOptions findOptions = UhtFindOptions.DelegateFunction | UhtFindOptions.Enum | UhtFindOptions.Class | UhtFindOptions.ScriptStruct;
if (tokenReader.TryOptional("enum"))
{
if (propertySettings.PropertyCategory == UhtPropertyCategory.Member)
{
tokenReader.LogError($"Cannot declare enum at variable declaration");
}
findOptions = UhtFindOptions.Enum;
}
else if (tokenReader.TryOptional("class"))
{
findOptions = UhtFindOptions.Class;
}
else if (tokenReader.TryOptional("struct"))
{
findOptions = UhtFindOptions.ScriptStruct;
}
UhtTokenList identifiers = tokenReader.GetCppIdentifier();
UhtType? type = propertySettings.Outer.FindType(UhtFindOptions.SourceName | findOptions, identifiers, tokenReader);
if (type == null)
{
return null;
}
UhtTokenListCache.Return(identifiers);
if (type is UhtEnum enumObj)
{
if (propertySettings.PropertyCategory == UhtPropertyCategory.Member)
{
if (enumObj.CppForm != UhtEnumCppForm.EnumClass)
{
tokenReader.LogError("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.");
}
}
property = new UhtEnumProperty(propertySettings, enumObj);
}
else if (type is UhtScriptStruct scriptStruct)
{
property = new UhtStructProperty(propertySettings, scriptStruct);
}
else if (type is UhtFunction function)
{
if (!function.FunctionFlags.HasAnyFlags(EFunctionFlags.MulticastDelegate))
{
property = new UhtDelegateProperty(propertySettings, function);
}
else if (function.FunctionType == UhtFunctionType.SparseDelegate)
{
property = new UhtMulticastSparseDelegateProperty(propertySettings, function);
}
else
{
property = new UhtMulticastInlineDelegateProperty(propertySettings, function);
}
}
else if (type is UhtClass classObj)
{
if (gotConst)
{
propertySettings.MetaData.Add(UhtNames.NativeConst, "");
gotConst = false;
}
// Const after variable type but before pointer symbol
if (tokenReader.TryOptional("const"))
{
propertySettings.MetaData.Add(UhtNames.NativeConst, "");
}
// If we didn't parse a TNonNullPtr, then we need to parse a pointer
if (!propertySettings.PropertyExportFlags.HasAnyFlags(UhtPropertyExportFlags.TNonNullPtr))
{
tokenReader.Require('*');
}
// Optionally emit messages about native pointer members and swallow trailing 'const' after pointer properties
// NOTE: At this time we aren't checking TNonNullPtr declarations since the engine support isn't that good.
// First off, TOptional>> is 16 bytes. Second, the Init method when wrapped in a
// TVal seems to prevent the Init method from working.
if (!propertySettings.PropertyExportFlags.HasAnyFlags(UhtPropertyExportFlags.TNonNullPtr))
{
args.ConditionalLogPointerUsage(args.Config.EngineNativePointerMemberBehavior, args.Config.EnginePluginNativePointerMemberBehavior,
args.Config.NonEngineNativePointerMemberBehavior, "Native pointer", typeStartPos, "TObjectPtr");
}
if (propertySettings.PropertyCategory == UhtPropertyCategory.Member)
{
tokenReader.TryOptional("const");
}
propertySettings.PointerType = UhtPointerType.Native;
if (classObj.ClassFlags.HasAnyFlags(EClassFlags.Interface))
{
property = new UhtInterfaceProperty(propertySettings, classObj);
}
else if (classObj.IsChildOf(args.Session.UClass))
{
property = new UhtClassProperty(propertySettings, UhtObjectCppForm.NativeClass, classObj);
}
else
{
property = new UhtObjectProperty(propertySettings, UhtObjectCppForm.NativeObject, classObj);
}
}
else
{
throw new UhtIceException("Unexpected type found");
}
if (gotConst)
{
if (propertySettings.PropertyCategory == UhtPropertyCategory.Member)
{
tokenReader.LogError("Const properties are not supported.");
}
else
{
tokenReader.LogError($"Inappropriate keyword 'const' on variable of type {type.SourceName}");
}
}
return property;
}
}
}