// 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; } } }