// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using EpicGames.Core;
using UnrealBuildBase;
using Microsoft.Extensions.Logging;
namespace AutomationTool
{
[Help("Audits the current branch for comments denoting a hack that was not meant to leave another branch, following a given format (\"BEGIN XXXX HACK\", where XXXX is one or more tags separated by spaces).")]
[Help("Allowed tags may be specified manually on the command line. At least one must match, otherwise it will print a warning.")]
[Help("The current branch name and fragments of the branch path will also be added by default, so running from //UE5/Main will add \"//UE5/Main\", \"UE5\", and \"Main\".")]
[Help("-Allow", "Specifies additional tags which are allowed in the BEGIN ... HACK tag list")]
class CheckForHacks : BuildCommand
{
///
/// List of file extensions to consider text, and search for hack lines
///
static readonly HashSet TextExtensions = new HashSet(StringComparer.InvariantCultureIgnoreCase)
{
".cpp",
".c",
".h",
".inl",
".m",
".mm",
".java",
".pl",
".pm",
".cs",
".sh",
".bat",
".xml",
".htm",
".html",
".xhtml",
".css",
".asp",
".aspx",
".js",
".py",
};
///
/// The pattern that should match hack comments (and captures a list of tags). Ignore anything with a semicolon between BEGIN and HACK to avoid matching statements with "HACK" as a comment (seen in LLVM).
///
static readonly Regex CompiledRegex = new Regex("(?
/// Executes the command
///
public override void ExecuteBuild()
{
// Build a list of all the allowed tags
HashSet AllowTags = new HashSet(StringComparer.InvariantCultureIgnoreCase);
foreach (string AllowTag in ParseParamValues("Allow"))
{
AllowTags.Add(AllowTag);
}
if(P4Enabled)
{
AllowTags.Add(P4Env.Branch);
AllowTags.Add(P4Env.Branch.Trim('/'));
AllowTags.UnionWith(P4Env.Branch.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries));
}
// Enumerate all the files in the workspace
Logger.LogInformation("Finding files in workspace...");
List FilesToCheck = new List();
using (ThreadPoolWorkQueue Queue = new ThreadPoolWorkQueue())
{
DirectoryInfo BaseDir = new DirectoryInfo(Unreal.EngineDirectory.FullName);
Queue.Enqueue(() => FindAllFiles(Queue, BaseDir, FilesToCheck));
Queue.Wait();
}
// Scan all of the files for invalid comments
Logger.LogInformation("Scanning files...");
using (ThreadPoolWorkQueue Queue = new ThreadPoolWorkQueue())
{
foreach(FileInfo File in FilesToCheck)
{
FileInfo FileCapture = File;
Queue.Enqueue(() => ScanSourceFile(FileCapture, AllowTags));
}
while(!Queue.Wait(5 * 1000))
{
lock(this)
{
Logger.LogInformation("Scanning files... [{Arg0}/{Arg1}]", FilesToCheck.Count - Queue.NumRemaining, FilesToCheck.Count);
}
}
}
}
///
/// Enumerates all files under the given directory
///
/// Queue to add additional subfolders to search to
/// Directory to search
/// Output list of files to check. Will be locked before adding items.
void FindAllFiles(ThreadPoolWorkQueue Queue, DirectoryInfo BaseDir, List FilesToCheck)
{
foreach(DirectoryInfo SubDir in BaseDir.EnumerateDirectories())
{
DirectoryInfo SubDirCapture = SubDir;
Queue.Enqueue(() => FindAllFiles(Queue, SubDirCapture, FilesToCheck));
}
foreach(FileInfo FileToCheck in BaseDir.EnumerateFiles())
{
if(TextExtensions.Contains(FileToCheck.Extension))
{
lock(FilesToCheck)
{
FilesToCheck.Add(FileToCheck);
}
}
}
}
///
/// Scans an individual source file for hack comments
///
/// The file to be checked
/// Set of tags which are allowed to appear in hack comments
void ScanSourceFile(FileInfo FileToCheck, HashSet AllowTags)
{
// Ignore the current file. We have comments that reference the desired format for hacks. :)
if(!String.Equals(FileToCheck.Name, "CheckForHacks.cs", StringComparison.InvariantCultureIgnoreCase))
{
using (FileStream Stream = FileToCheck.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
using (StreamReader Reader = new StreamReader(Stream, true))
{
for (int LineNumber = 1; ; LineNumber++)
{
string Line = Reader.ReadLine();
if (Line == null)
{
break;
}
Match Result = CompiledRegex.Match(Line);
if(Result.Success)
{
string[] Tags = Result.Groups[1].Value.Split(new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
if (!Tags.Any(Tag => AllowTags.Contains(Tag)))
{
lock (this)
{
EpicGames.Core.Log.WriteLine(EpicGames.Core.LogEventType.Warning, EpicGames.Core.LogFormatOptions.NoSeverityPrefix, "{0}({1}): warning: Code should not be in this branch: '{2}'", FileToCheck.FullName, LineNumber, Line.Trim());
}
}
}
}
}
}
}
}
}
}