// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Buffers; using System.Text; using EpicGames.Core; using EpicGames.UHT.Tables; using EpicGames.UHT.Tokenizer; using EpicGames.UHT.Types; using EpicGames.UHT.Utils; namespace EpicGames.UHT.Parsers { /// /// Nested structure of scopes being parsed /// public class UhtParsingScope : IDisposable { /// /// Header file parser /// public UhtHeaderFileParser HeaderParser { get; } /// /// Header file being parsed /// public UhtHeaderFile HeaderFile => HeaderParser.HeaderFile; /// /// Module owning the header file /// public UhtModule Module => HeaderFile.Module; /// /// Token reader /// public IUhtTokenReader TokenReader { get; } /// /// Parent scope /// public UhtParsingScope? ParentScope { get; } /// /// Type being parsed. /// public UhtType ScopeType { get; } /// /// Keyword table for the scope /// public UhtKeywordTable ScopeKeywordTable { get; } /// /// Current access specifier /// public UhtAccessSpecifier AccessSpecifier { get; set; } = UhtAccessSpecifier.Public; /// /// Current session /// public UhtSession Session => HeaderFile.Session; /// /// Return the current class scope being compiled /// public UhtParsingScope CurrentClassScope { get { UhtParsingScope? currentScope = this; while (currentScope != null) { if (currentScope.ScopeType is UhtClass) { return currentScope; } currentScope = currentScope.ParentScope; } throw new UhtIceException("Attempt to fetch the current class when a class isn't currently being parsed"); } } /// /// Construct a root/global scope /// /// Header parser /// Keyword table public UhtParsingScope(UhtHeaderFileParser headerParser, UhtKeywordTable keywordTable) { HeaderParser = headerParser; TokenReader = headerParser.TokenReader; ParentScope = null; ScopeType = headerParser.Module.ScriptPackage; // The default package for parsing will be the standard UE package ScopeKeywordTable = keywordTable; HeaderParser.PushScope(this); } /// /// Construct a scope for a type /// /// Parent scope /// Type being parsed /// Keyword table /// Current access specifier public UhtParsingScope(UhtParsingScope parentScope, UhtType scopeType, UhtKeywordTable keywordTable, UhtAccessSpecifier accessSpecifier) { HeaderParser = parentScope.HeaderParser; TokenReader = parentScope.TokenReader; ParentScope = parentScope; ScopeType = scopeType; ScopeKeywordTable = keywordTable; AccessSpecifier = accessSpecifier; HeaderParser.PushScope(this); } /// /// Dispose the scope /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Virtual method for disposing the object /// /// If true, we are disposing protected virtual void Dispose(bool disposing) { if (disposing) { HeaderParser.PopScope(this); } } /// /// Add the module's relative path to the type's meta data /// public void AddModuleRelativePathToMetaData() { AddModuleRelativePathToMetaData(ScopeType.MetaData, ScopeType.HeaderFile); } /// /// Add the module's relative path to the meta data /// /// The meta data to add the information to /// The header file currently being parsed public static void AddModuleRelativePathToMetaData(UhtMetaData metaData, UhtHeaderFile headerFile) { metaData.Add(UhtNames.ModuleRelativePath, headerFile.ModuleRelativeFilePath); } /// /// Format the current token reader comments and add it as meta data /// /// Index for the meta data key. This is used for enum values public void AddFormattedCommentsAsTooltipMetaData(int metaNameIndex = UhtMetaData.IndexNone) { AddFormattedCommentsAsTooltipMetaData(ScopeType, metaNameIndex); } /// /// Format the current token reader comments and add it as meta data /// /// The type to add the meta data to /// Index for the meta data key. This is used for enum values public void AddFormattedCommentsAsTooltipMetaData(UhtType type, int metaNameIndex = UhtMetaData.IndexNone) { // Don't add a tooltip if one already exists. if (type.MetaData.ContainsKey(UhtNames.ToolTip, metaNameIndex)) { return; } // Fetch the comments ReadOnlySpan comments = TokenReader.Comments; // If we don't have any comments, just return if (comments.Length == 0) { return; } // Set the comment as just a simple concatenation of all the strings string mergedString = String.Empty; if (comments.Length == 1) { mergedString = comments[0].ToString(); type.MetaData.Add(UhtNames.Comment, metaNameIndex, mergedString); } else { using BorrowStringBuilder borrower = new(StringBuilderCache.Small); StringBuilder builder = borrower.StringBuilder; foreach (StringView comment in comments) { builder.Append(comment); } mergedString = builder.ToString(); type.MetaData.Add(UhtNames.Comment, metaNameIndex, mergedString); } // Format the tooltip and set the metadata string toolTip = FormatCommentForToolTip(mergedString); if (!String.IsNullOrEmpty(toolTip)) { type.MetaData.Add(UhtNames.ToolTip, metaNameIndex, toolTip); //COMPATIBILITY-TODO - Old UHT would only clear the comments if there was some form of a tooltip TokenReader.ClearComments(); } //COMPATIBILITY-TODO // Clear the comments since they have been consumed //TokenReader.ClearComments(); } // We consider any alpha/digit or code point > 0xFF as a valid comment char /// /// Given a list of comments, check to see if any have alpha, numeric, or unicode code points with a value larger than 0xFF. /// /// Comments to search /// True is a character in question was found private static bool HasValidCommentChar(ReadOnlySpan comments) { foreach (char c in comments) { if (UhtFCString.IsAlnum(c) || c > 0xFF) { return true; } } return false; } /// /// Convert the given list of comments to a tooltip. Each string view is a comment where the // style comments also includes the trailing \r\n. /// /// The following style comments are supported: /// /// /* */ - C Style /// /** */ - C Style JavaDocs /// /*~ */ - C Style but ignore /// //\r\n - C++ Style /// ///\r\n - C++ Style JavaDocs /// //~\r\n - C++ Style bug ignore /// /// As per TokenReader, there will only be one C style comment ever present, and it will be the first one. When a C style comment is parsed, any prior comments /// are cleared. However, if a C++ style comment follows a C style comment (regardless of any intermediate blank lines), then both blocks of comments will be present. /// If any blank lines are encountered between blocks of C++ style comments, then any prior comments are cleared. /// /// Comments to be parsed /// The generated tooltip private static string FormatCommentForToolTip(string comments) { if (!HasValidCommentChar(comments)) { return String.Empty; } // Use the scratch characters to store the string as we process it in MANY passes char[] scratchChars = ArrayPool.Shared.Rent(comments.Length); comments.CopyTo(0, scratchChars, 0, comments.Length); // Remove ignore comments and strip the comment markers: // These are all issues with how the old UHT worked. // 1) Must be done in order // 2) Block comment markers are removed first so that '///**' is process by removing '/**' first and then '//' second // 3) We only remove block comments if we find the start of a comment. This means that if there is a '*/' in a line comment, it won't be removed. // 4) We must check to see if we have cppStyle prior to removing block style comments. int commentsLength = RemoveIgnoreComments(scratchChars, comments.Length); ReadOnlySpan span = scratchChars.AsSpan(0, commentsLength); bool javaDocStyle = span.Contains("/**", StringComparison.Ordinal); bool cStyle = javaDocStyle || span.Contains("/*", StringComparison.Ordinal); bool cppStyle = span.StartsWith("//", StringComparison.Ordinal); commentsLength = javaDocStyle || cStyle ? RemoveBlockCommentMarkers(scratchChars, commentsLength, javaDocStyle) : commentsLength; commentsLength = cppStyle ? RemoveLineCommentMarkers(scratchChars, commentsLength) : commentsLength; //wx widgets has a hard coded tab size of 8 { const int SpacesPerTab = 8; // If we have any tab characters, then we need to convert them to spaces span = scratchChars.AsSpan(0, commentsLength); int tabIndex = span.IndexOf('\t'); if (tabIndex != -1) { using BorrowStringBuilder tabsBorrower = new(StringBuilderCache.Small); StringBuilder tabsBuilder = tabsBorrower.StringBuilder; UhtFCString.TabsToSpaces(span, SpacesPerTab, true, tabIndex, tabsBuilder); commentsLength = tabsBuilder.Length; if (commentsLength > scratchChars.Length) { ArrayPool.Shared.Return(scratchChars); scratchChars = ArrayPool.Shared.Rent(commentsLength); } tabsBuilder.CopyTo(0, scratchChars, 0, commentsLength); } } static bool IsAllSameChar(ReadOnlySpan line, int startIndex, char testChar) { for (int index = startIndex, end = line.Length; index < end; ++index) { if (line[index] != testChar) { return false; } } return true; } static bool IsWhitespaceOrLineSeparator(ReadOnlySpan line) { // Skip any leading spaces int index = 0; int endPos = line.Length; for (; index < endPos && UhtFCString.IsWhitespace(line[index]); ++index) { } if (index == endPos) { return true; } // Check for the same character return IsAllSameChar(line, index, '-') || IsAllSameChar(line, index, '=') || IsAllSameChar(line, index, '*'); } // Loop while we have data span = scratchChars.AsSpan(0, commentsLength); bool firstLine = true; int maxNumWhitespaceToRemove = 0; int lastNonWhitespaceLength = 0; int outEndPos = 0; while (span.Length > 0) { // Extract the next line to process int eolIndex = span.IndexOf('\n'); ReadOnlySpan line = eolIndex != -1 ? span[..eolIndex] : span; span = eolIndex != -1 ? span[(eolIndex + 1)..] : new ReadOnlySpan(); line = line.TrimEnd(); // Remove leading "*" and "* " in javadoc comments. if (javaDocStyle) { // Find first non-whitespace character int pos = 0; while (pos < line.Length && UhtFCString.IsWhitespace(line[pos])) { ++pos; } // Is it a *? if (pos < line.Length && line[pos] == '*') { // Eat next space as well if (pos + 1 < line.Length && UhtFCString.IsWhitespace(line[pos + 1])) { ++pos; } line = line[(pos + 1)..]; } } // Test to see if this is whitespace or line separator. If also first line, then just skip bool isWhitespaceOrLineSeparator = IsWhitespaceOrLineSeparator(line); if (firstLine && isWhitespaceOrLineSeparator) { continue; } // Figure out how much whitespace is on the first line if (firstLine) { for (; maxNumWhitespaceToRemove < line.Length; maxNumWhitespaceToRemove++) { if (!UhtFCString.IsWhitespace(line[maxNumWhitespaceToRemove])) { break; } } line = line[maxNumWhitespaceToRemove..]; } else { // Trim any leading whitespace for (int i = 0; i < maxNumWhitespaceToRemove && line.Length > 0; i++) { if (!UhtFCString.IsWhitespace(line[0])) { break; } line = line[1..]; } scratchChars[outEndPos++] = '\n'; } if (line.Length > 0 && !IsAllSameChar(line, 0, '=')) { for (int i = 0; i < line.Length; i++) { scratchChars[outEndPos++] = line[i]; } } if (!isWhitespaceOrLineSeparator) { lastNonWhitespaceLength = outEndPos; } firstLine = false; } outEndPos = lastNonWhitespaceLength; //@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 int outStartPos = 0; if (outStartPos < outEndPos && scratchChars[outStartPos] == '\n') { outStartPos++; } // Make sure it doesn't end with a dead newline if (outStartPos < outEndPos && scratchChars[outEndPos - 1] == '\n') { outEndPos--; } string results = scratchChars.AsSpan(outStartPos, outEndPos - outStartPos).ToString(); ArrayPool.Shared.Return(scratchChars); return results; } /// /// Remove any comments marked to be ignored /// /// Buffer containing comments to be processed. Comments are removed inline /// Length of the comments /// New length of the comments private static int RemoveIgnoreComments(char[] comments, int inLength) { ReadOnlySpan span = comments.AsSpan(0, inLength); int commentStart, commentEnd; // Block comments go first while ((commentStart = span.IndexOf("/*~", StringComparison.Ordinal)) != -1) { commentEnd = span[commentStart..].IndexOf("*/", StringComparison.Ordinal); if (commentEnd != -1) { commentEnd += 2; Array.Copy(comments, commentStart + commentEnd, comments, commentStart, span.Length - (commentStart + commentEnd)); span = span[..(span.Length - commentEnd)]; } else { // This looks like an error - an unclosed block comment. break; } } // Leftover line comments go next while ((commentStart = span.IndexOf("//~", StringComparison.Ordinal)) != -1) { commentEnd = span[commentStart..].IndexOf("\n", StringComparison.Ordinal); if (commentEnd != -1) { commentEnd++; Array.Copy(comments, commentStart + commentEnd, comments, commentStart, span.Length - (commentStart + commentEnd)); span = span[..(span.Length - commentEnd)]; } else { span = span[..commentStart]; break; } } return span.Length; } /// /// Remove any block comment markers /// /// Buffer containing comments to be processed. Comments are removed inline /// Length of the comments /// If true, we are parsing both java and c style. This is a strange hack for //***__ comments which end up as __ /// New length of the comments private static int RemoveBlockCommentMarkers(char[] comments, int inLength, bool javaDocStyle) { int outPos = 0; int inPos = 0; while (inPos < inLength) { switch (comments[inPos]) { case '\r': inPos++; break; case '/': // This block of code is mimicking the old pattern of replacing "/**" with "" followed by "/*" with "". // Thus "//***" -> "/*" -> "" if (javaDocStyle && inPos + 4 < inLength && comments[inPos + 1] == '/' && comments[inPos + 2] == '*' && comments[inPos + 3] == '*' && comments[inPos + 4] == '*') { inPos += 5; } else if (inPos + 2 < inLength && comments[inPos + 1] == '*' && comments[inPos + 2] == '*') { inPos += 3; } else if (inPos + 1 < inLength && comments[inPos + 1] == '*') { inPos += 2; } else { comments[outPos++] = comments[inPos++]; } break; case '*': if (inPos + 1 < inLength && comments[inPos + 1] == '/') { inPos += 2; } else { comments[outPos++] = comments[inPos++]; } break; default: comments[outPos++] = comments[inPos++]; break; } } return outPos; } /// /// Remove any line comment markers /// /// Buffer containing comments to be processed. Comments are removed inline /// Length of the comments /// New length of the comments private static int RemoveLineCommentMarkers(char[] comments, int inLength) { ReadOnlySpan span = comments.AsSpan(0, inLength); int outPos = 0; int inPos = 0; while (inPos < inLength) { switch (comments[inPos]) { case '\r': inPos++; break; case '/': if (inPos + 1 < inLength && comments[inPos + 1] == '/') { if (inPos + 2 < inLength && comments[inPos + 2] == '/') { inPos += 3; } else { inPos += 2; } } else { comments[outPos++] = comments[inPos++]; } break; case '(': { if (span[inPos..].StartsWith("(cpptext)", StringComparison.Ordinal)) { inPos += 9; } else { comments[outPos++] = comments[inPos++]; } } break; default: comments[outPos++] = comments[inPos++]; break; } } return outPos; } } /// /// Token recorder /// public struct UhtTokenRecorder : IDisposable { private readonly UhtCompilerDirective _compilerDirective; private readonly UhtParsingScope _scope; private readonly UhtFunction? _function; private bool _flushed; /// /// Construct a new recorder /// /// Scope being parsed /// Initial toke nto add to the recorder public UhtTokenRecorder(UhtParsingScope scope, ref UhtToken initialToken) { _scope = scope; _compilerDirective = _scope.HeaderParser.GetCurrentCompositeCompilerDirective(); _function = null; _flushed = false; if (_scope.ScopeType is UhtClass) { _scope.TokenReader.EnableRecording(); _scope.TokenReader.RecordToken(ref initialToken); } } /// /// Create a new recorder /// /// Scope being parsed /// Function associated with the recorder public UhtTokenRecorder(UhtParsingScope scope, UhtFunction function) { _scope = scope; _compilerDirective = _scope.HeaderParser.GetCurrentCompositeCompilerDirective(); _function = function; _flushed = false; if (_scope.ScopeType is UhtClass) { _scope.TokenReader.EnableRecording(); } } /// /// Stop the recording /// public void Dispose() { Stop(); } /// /// Stop the recording /// /// True if the recorded content was added to a class public bool Stop() { if (!_flushed) { _flushed = true; if (_scope.ScopeType is UhtClass classObj) { classObj.AddDeclaration(_compilerDirective, _scope.TokenReader.RecordedTokens, _function); _scope.TokenReader.DisableRecording(); return true; } } return false; } } }