// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using EpicGames.Core;
using Microsoft.Extensions.Logging;
using UnrealBuildBase;
// IWYUMode is a mode that can be used to clean up includes in source code. It uses the clang based tool include-what-you-use (IWYU) to figure out what is needed in each .h/.cpp file
// and then cleans up accordingly. This mode can be used to clean up unreal as well as plugins and projects on top of unreal.
// Note, IWYU is not perfect. There are still c++ features not supported so even though it will do a good job cleaning up it might require a little bit of hands on but not much.
// When iwyu for some reason removes includes you want to keep, there are ways to anotate the code. Look at the pragma link below.
//
// IWYU Github: https://github.com/include-what-you-use/include-what-you-use
// IWYU Pragmas: https://github.com/include-what-you-use/include-what-you-use/blob/master/docs/IWYUPragmas.md
//
// Here are examples of how a commandline could look (using ushell)
//
// 1. Will build lyra editor target using include-what-you-use.exe instead of clang.exe, and preview how iwyu would update module LyraGame and all the modules depending on it
//
// .build target LyraEditor linux development -- -Mode=IWYU -ModuleToUpdate=LyraGame -UpdateDependents
//
// 2. Same as 1 but will check out files from p4 and modify them
//
// .build target LyraEditor linux development -- -Mode=IWYU -ModuleToUpdate=LyraGame -UpdateDependents -Write
//
// 3. Will build and then update all modules that has Lyra/Plugins in its path. Note it will only include modules that are part of LyraEditor target.
//
// .build target LyraEditor linux development -- -Mode=IWYU -PathToUpdate=Lyra/Plugins -Write
//
// 4. Update Niagara plugins private code (not public api) with preview
//
// .build target UnrealEditor linux development -- -Mode=IWYU -PathToUpdate=Engine/Plugins/FX/Niagara -UpdateOnlyPrivate
namespace UnrealBuildTool
{
///
/// Representing an #include inside a code file.
///
class IWYUIncludeEntry
{
///
/// Full path using forward paths, eg: d:/dev/folder/file.h
///
public string Full { get; set; } = "";
///
/// Printable path for actual include. Does not include quotes or angle brackets. eg: folder/file.h
///
public string Printable { get; set; } = "";
///
/// Reference to file info. Not populated with .iwyu file
///
public IWYUInfo? Resolved;
///
/// Decides if printable should be #include quote or angle bracket
///
public bool System { get; set; }
///
/// If true #include was found inside a declaration such as a namespace or class/struct/function
///
public bool InsideDecl { get; set; }
}
///
/// Comparer for include entries using printable to compare
///
class IWYUIncludeEntryPrintableComparer : IEqualityComparer
{
public bool Equals(IWYUIncludeEntry? x, IWYUIncludeEntry? y)
{
return x!.Printable == y!.Printable;
}
public int GetHashCode([DisallowNull] IWYUIncludeEntry obj)
{
throw new NotImplementedException();
}
}
///
/// Representing a forward declaration inside a code file. eg: class MyClass;
///
struct IWYUForwardEntry
{
///
/// The string to add to the file
///
public string Printable { get; set; }
///
/// True if forward declaration has already been seen in the file
///
public bool Present { get; set; }
}
///
/// Representing an include that the code file needs that is missing in the include list
/// Note, in cpp files it is listing the includes that are in the matching h file
///
struct IWYUMissingInclude
{
///
/// Full path using forward paths, eg: d:/dev/folder/file.h
///
public string Full { get; set; }
}
///
/// Representing a file (header or source). Each .iwyu file has a list of these (most of the time only one)
///
class IWYUInfo
{
///
/// Full path of file using forward paths, eg: d:/dev/folder/file.h
///
public string File { get; set; } = "";
///
/// Includes that this file needs. This is what iwyu have decided is used.
///
public List Includes { get; set; } = new List();
///
/// Forward declarations that this file needs. This is what iwyu have decided is used.
///
public List ForwardDeclarations { get; set; } = new List();
///
/// Includes seen in the file. This is how it looked like on disk.
///
public List IncludesSeenInFile { get; set; } = new List();
///
/// Includes that didnt end up in the Includes list but is needed by the code file
///
public List MissingIncludes { get; set; } = new List();
///
/// Transitive includes. This is all the includes that someone gets for free when including this file
/// Note, this is based on what iwyu believes should be in all includes. So it will not look at "seen includes"
/// and only use its generated list.
///
public Dictionary TransitiveIncludes = new();
///
/// Transitive forward declarations. Same as transitive includes
///
public Dictionary TransitiveForwardDeclarations = new();
///
/// Which .iwyu file that produced this info. Note, it might be null for some special cases (like .generated.h files)
///
public IWYUFile? Source;
///
/// Module that this file belongs to
///
public UEBuildModule? Module;
///
/// Is true if this file was included inside a declaration such as a namespace or class/struct/function
///
public bool IncludedInsideDecl;
///
/// Is true if this file ends with .cpp
///
public bool IsCpp;
}
///
/// This is what include-what-you-use.exe produces for each build. a .iwyu file that is parsed into these instances
///
class IWYUFile
{
///
/// The different headers and source files that was covered by this execution of iwyu.
///
public List Files { get; set; } = new();
///
/// Name of .iwyu file
///
public string? Name;
}
///
/// Profiles different unity sizes and prints out the different size and its timings
///
[ToolMode("IWYU", ToolModeOptions.XmlConfig | ToolModeOptions.BuildPlatforms | ToolModeOptions.SingleInstance | ToolModeOptions.StartPrefetchingEngine | ToolModeOptions.ShowExecutionTime)]
class IWYUMode : ToolMode
{
///
/// Specifies the file to use for logging.
///
[XmlConfigFile(Category = "BuildConfiguration")]
public string? IWYUBaseLogFileName;
///
/// Will check out files from p4 and write to disk
///
[CommandLine("-Write")]
public bool bWrite = false;
///
/// Update only includes in cpp files and don't touch headers
///
[CommandLine("-UpdateOnlyPrivate")]
public bool bUpdateOnlyPrivate = false;
///
/// Which module to run IWYU on. Will also include all modules depending on this module
///
[CommandLine("-ModuleToUpdate")]
public string ModuleToUpdate = "";
///
/// Which directory to run IWYU on. Will search for modules that has module directory matching this string
/// If no module is found in -PathToUpdate it handles this for individual files instead.
/// Note this can be combined with -ModuleToUpdate to double filter
/// PathToUpdate supports multiple paths using semi colon separation
///
[CommandLine("-PathToUpdate")]
public string PathToUpdate = "";
///
/// Same as PathToUpdate but provide the list of paths in a text file instead.
/// Add all the desired paths on a separate line
///
[CommandLine("-PathToUpdateFile")]
public string PathToUpdateFile = "";
///
/// Also update modules that are depending on the module we are updating
///
[CommandLine("-UpdateDependents")]
public bool bUpdateDependents = false;
///
/// Will check out files from p4 and write to disk
///
[CommandLine("-NoP4")]
public bool bNoP4 = false;
///
/// Allow files to not add includes if they are transitively included by other includes
///
[CommandLine("-NoTransitive")]
public bool bNoTransitiveIncludes = false;
///
/// Will remove headers that are needed but redundant because they are included through other needed includes
///
[CommandLine("-RemoveRedundant")]
public bool bRemoveRedundantIncludes = false;
///
/// Will skip compiling before updating. Handle with care, this is dangerous since files might not match .iwyu files
///
[CommandLine("-NoCompile")]
public bool bNoCompile = false;
///
/// Will ignore update check that .iwyu is newer than source files.
///
[CommandLine("-IgnoreUpToDateCheck")]
public bool bIgnoreUpToDateCheck = false;
///
/// If set, this will keep removed includes in #if/#endif scope at the end of updated file.
/// Applied to non-private headers that are part of the Engine folder.
///
[CommandLine("-DeprecateTag")]
private string? HeaderDeprecationTagOverride;
public string HeaderDeprecationTag
{
get
{
if (!String.IsNullOrEmpty(HeaderDeprecationTagOverride))
{
return HeaderDeprecationTagOverride;
}
return EngineIncludeOrderHelper.GetLatestDeprecationDefine();
}
set => HeaderDeprecationTagOverride = value;
}
///
/// Compare current include structure with how it would look like if iwyu was applied on all files
///
[CommandLine("-Compare")]
public bool bCompare = false;
///
/// For Development only - Will write a toc referencing all .iwyu files. Toc can be used by -ReadToc
///
[CommandLine("-WriteToc")]
public bool bWriteToc = false;
///
/// For Development only - Will read a toc to find all .iwyu files instead of building the code.
///
[CommandLine("-ReadToc")]
public bool bReadToc = false;
private string? GetModuleToUpdateName(TargetDescriptor Descriptor)
{
if (!String.IsNullOrEmpty(ModuleToUpdate))
{
return ModuleToUpdate;
}
if (Descriptor.OnlyModuleNames.Count > 0)
{
return Descriptor.OnlyModuleNames.First();
}
return null;
}
private string AdjustModulePathForMatching(string ModulePath)
{
string NewModulePath = ModulePath.Replace('\\', '/');
if (!NewModulePath.EndsWith('/'))
{
NewModulePath += '/';
}
return NewModulePath;
}
///
/// Execute the command
///
/// Command line arguments
/// Exit code
///
public override async Task ExecuteAsync(CommandLineArguments Arguments, ILogger Logger)
{
Arguments.ApplyTo(this);
Logger.LogInformation($"====================================================");
Logger.LogInformation($"Running IWYU. {(bWrite ? "" : "(Preview mode. Add -Write to write modifications to files)")}");
Logger.LogInformation($"====================================================");
// Fixup the log path if it wasn't overridden by a config file
IWYUBaseLogFileName ??= FileReference.Combine(Unreal.EngineProgramSavedDirectory, "UnrealBuildTool", "IWYULog.txt").FullName;
// Create the log file, and flush the startup listener to it
if (!Arguments.HasOption("-NoLog") && !Log.HasFileWriter())
{
Log.AddFileWriter("DefaultLogTraceListener", FileReference.FromString(IWYUBaseLogFileName));
}
else
{
Log.RemoveStartupTraceListener();
}
// Create the build configuration object, and read the settings
CommandLineArguments BuildArguments = Arguments.Append(new[] { "-IWYU" }); // Add in case it is not added (it is needed for iwyu toolchain)
BuildConfiguration BuildConfiguration = new BuildConfiguration();
XmlConfig.ApplyTo(BuildConfiguration);
BuildArguments.ApplyTo(BuildConfiguration);
BuildConfiguration.MaxNestedPathLength = 220; // For now since the path is slightly longer
// Parse all the target descriptors
List TargetDescriptors = TargetDescriptor.ParseCommandLine(BuildArguments, BuildConfiguration, Logger);
if (TargetDescriptors.Count != 1)
{
Logger.LogError($"IWYUMode can only handle command lines that produce one target (Cmdline: {Arguments})");
return 0;
}
TargetDescriptors[0].IntermediateEnvironment = UnrealIntermediateEnvironment.IWYU;
string? ModuleToUpdateName = GetModuleToUpdateName(TargetDescriptors[0]);
if (!String.IsNullOrEmpty(ModuleToUpdateName))
{
foreach (string OnlyModuleName in TargetDescriptors[0].OnlyModuleNames)
{
if (!String.Equals(OnlyModuleName, ModuleToUpdateName, StringComparison.OrdinalIgnoreCase))
{
Logger.LogError($"ModuleToUpdate '{ModuleToUpdateName}' was not in list of specified modules: {String.Join(", ", TargetDescriptors[0].OnlyModuleNames)}");
return -1;
}
}
}
string[] PathToUpdateList = Array.Empty();
if (!String.IsNullOrEmpty(PathToUpdate))
{
PathToUpdateList = PathToUpdate.Split(";");
}
else if (!String.IsNullOrEmpty(PathToUpdateFile))
{
PathToUpdateList = await File.ReadAllLinesAsync(PathToUpdateFile);
}
if (PathToUpdateList.Length > 0)
{
for (int I = 0; I != PathToUpdateList.Length; ++I)
{
PathToUpdateList[I] = PathToUpdateList[I].Replace('\\', '/');
}
}
string TargetName = $"{TargetDescriptors[0].Name}_{TargetDescriptors[0].Configuration}_{TargetDescriptors[0].Platform}";
// Calculate file paths filter to figure out which files that IWYU will run on
HashSet ValidPaths = new();
Dictionary PathToModule = new();
Dictionary NameToModule = new();
{
// Create target to be able to traverse all existing modules
Logger.LogInformation($"Creating BuildTarget for {TargetName}...");
UEBuildTarget Target = UEBuildTarget.Create(TargetDescriptors[0], BuildConfiguration, Logger);
Logger.LogInformation($"Calculating file filter for IWYU...");
UEBuildModule? UEModuleToUpdate = null;
// Turn provided module string into UEBuildModule reference
if (!String.IsNullOrEmpty(ModuleToUpdateName))
{
UEModuleToUpdate = Target.GetModuleByName(ModuleToUpdateName);
if (UEModuleToUpdate == null)
{
Logger.LogError($"Can't find module with name {ModuleToUpdateName}");
return -1;
}
}
if (PathToUpdateList.Length == 0 && UEModuleToUpdate == null)
{
Logger.LogError($"Need to provide -ModuleToUpdate or -PathToUpdate to run IWYU.");
return -1;
}
int ModulesToUpdateCount = 0;
int ModulesSkippedCount = 0;
// Traverse all modules to figure out which ones should be updated. This is based on -ModuleToUpdate and -PathToUpdate
List ReferencedModules = new();
HashSet IgnoreReferencedModules = new();
foreach (UEBuildBinary Binary in Target.Binaries)
{
foreach (UEBuildModule Module in Binary.Modules)
{
if (IgnoreReferencedModules.Add(Module))
{
ReferencedModules.Add(Module);
Module.GetAllDependencyModules(ReferencedModules, IgnoreReferencedModules, true, false, false);
}
}
}
foreach (UEBuildModule Module in ReferencedModules)
{
NameToModule.TryAdd(Module.Name, Module);
foreach (UEBuildModule Module2 in Module.GetDependencies(true, true))
{
NameToModule.TryAdd(Module2.Name, Module2);
}
}
foreach (UEBuildModule Module in NameToModule.Values)
{
bool ShouldUpdate = false;
string ModuleDir = Module.ModuleDirectory.FullName.Replace('\\', '/');
if (!PathToModule.TryAdd(ModuleDir, Module))
{
continue;
}
foreach (DirectoryReference AdditionalDir in Module.ModuleDirectories)
{
if (AdditionalDir != Module.ModuleDirectory)
{
PathToModule.TryAdd(AdditionalDir.FullName.Replace('\\', '/'), Module);
}
}
if (Module.Rules.Type != ModuleRules.ModuleType.CPlusPlus)
{
continue;
}
if (UEModuleToUpdate != null)
{
bool DependsOnModule = Module == UEModuleToUpdate;
if (bUpdateDependents)
{
if (Module.PublicDependencyModules != null)
{
foreach (UEBuildModule Dependency in Module.PublicDependencyModules)
{
DependsOnModule = DependsOnModule || Dependency == UEModuleToUpdate;
}
}
if (Module.PrivateDependencyModules != null)
{
foreach (UEBuildModule Dependency in Module.PrivateDependencyModules!)
{
DependsOnModule = DependsOnModule || Dependency == UEModuleToUpdate;
}
}
}
ShouldUpdate = DependsOnModule;
}
if (PathToUpdateList.Length > 0)
{
bool Match = false;
for (int I = 0; I != PathToUpdateList.Length; ++I)
{
Match = Match || ModuleDir.Contains(PathToUpdateList[I], StringComparison.OrdinalIgnoreCase);
}
if (Match)
{
if (UEModuleToUpdate == null)
{
ShouldUpdate = true;
}
}
else
{
ShouldUpdate = false;
}
}
else if (UEModuleToUpdate == null)
{
ShouldUpdate = true;
}
if (!ShouldUpdate)
{
continue;
}
if (Module.Rules.IWYUSupport == IWYUSupport.None)
{
++ModulesSkippedCount;
continue;
}
++ModulesToUpdateCount;
// When adding to ValidPaths, make sure the Module directory ends in a / so we match the exact folder later
ValidPaths.Add(AdjustModulePathForMatching(ModuleDir));
foreach (DirectoryReference AdditionalDir in Module.ModuleDirectories)
{
if (AdditionalDir != Module.ModuleDirectory)
{
ValidPaths.Add(AdjustModulePathForMatching(AdditionalDir.FullName));
}
}
}
if (ValidPaths.Count == 0 && PathToUpdateList.Length > 0)
{
foreach (string Path in PathToUpdateList)
{
ValidPaths.Add(Path);
}
Logger.LogInformation($"Will update files matching {PathToUpdate}. Note, path is case sensitive");
}
else
{
Logger.LogInformation($"Will update {ModulesToUpdateCount} module(s) using IWYU. ({ModulesSkippedCount} skipped because of bEnforceIWYU=false)...");
}
}
Dictionary Infos = new();
HashSet GeneratedHeaderInfos = new();
List GeneratedCppInfos = new();
using (ISourceFileWorkingSet WorkingSet = new EmptySourceFileWorkingSet())
{
List? TocContent = null;
int ReadSuccess = 1;
if (bReadToc)
{
string TocName = TargetName + ".txt";
FileReference TocReference = FileReference.Combine(Unreal.EngineDirectory, "Intermediate", "IWYU", TocName);
Logger.LogInformation($"Reading TOC from {TocReference.FullName}...");
if (FileReference.Exists(TocReference))
{
TocContent = FileReference.ReadAllLines(TocReference).ToList();
}
}
if (TocContent == null)
{
TargetDescriptor Descriptor = TargetDescriptors.First();
// Create the make file that contains all the actions we will use to find .iwyu files.
Logger.LogInformation($"Creating MakeFile for target...");
TargetMakefile Makefile;
try
{
Makefile = await BuildMode.CreateMakefileAsync(BuildConfiguration, Descriptor, WorkingSet, Logger);
}
finally
{
SourceFileMetadataCache.SaveAll();
}
// Use the make file to build unless -NoCompile is set.
if (!bNoCompile)
{
try
{
await BuildMode.BuildAsync(new TargetMakefile[] { Makefile }, new List() { Descriptor }, BuildConfiguration, BuildOptions.None, null, Logger);
}
finally
{
CppDependencyCache.SaveAll();
}
}
HashSet OutputItems = new HashSet();
if (Descriptor.OnlyModuleNames.Count > 0)
{
foreach (string OnlyModuleName in Descriptor.OnlyModuleNames)
{
FileItem[]? OutputItemsForModule;
if (!Makefile.ModuleNameToOutputItems.TryGetValue(OnlyModuleName, out OutputItemsForModule))
{
throw new BuildException("Unable to find output items for module '{0}'", OnlyModuleName);
}
OutputItems.UnionWith(OutputItemsForModule);
}
}
else
{
// Use all the output items from the target
OutputItems.UnionWith(Makefile.OutputItems);
}
TocContent = new();
foreach (FileItem OutputItem in OutputItems)
{
if (OutputItem.Name.EndsWith(".iwyu"))
{
TocContent.Add(OutputItem.AbsolutePath);
}
}
}
// Time to parse all the .iwyu files generated from iwyu.
// We Do this in parallel since it involves reading a ton of .iwyu files from disk.
Logger.LogInformation($"Parsing {TocContent.Count} .iwyu files...");
Parallel.ForEach(TocContent, IWYUFilePath =>
{
string? JsonContent = File.ReadAllText(IWYUFilePath);
if (JsonContent == null)
{
Logger.LogError($"Failed to read file {IWYUFilePath}");
Interlocked.Exchange(ref ReadSuccess, 0);
return;
}
try
{
IWYUFile? IWYUFile = JsonSerializer.Deserialize(JsonContent);
IWYUFile!.Name = IWYUFilePath;
// Traverse the cpp/inl/h file entries inside the .iwyu file
foreach (IWYUInfo Info in IWYUFile!.Files)
{
Info.Source = IWYUFile;
Info.IsCpp = Info.File.EndsWith(".cpp");
// We track .gen.cpp in a special list, they need special treatment later
if (Info.File.Contains(".gen.cpp", StringComparison.Ordinal))
{
lock (GeneratedCppInfos)
{
GeneratedCppInfos.Add(Info);
}
continue;
}
// Ok, time to add file entry to lookup
lock (Infos)
{
if (!Infos.TryAdd(Info.File, Info))
{
// This is a valid scenario when Foo.cpp also registers Foo.h... and then Foo.h registers itself.
IWYUInfo ExistingEntry = Infos[Info.File];
if (IWYUFile.Files.Count == 1)
{
if (ExistingEntry.Source!.Files.Count == 1)
{
Logger.LogError($"{Info.File} - built twice somehow?");
return;
}
else
{
Infos[Info.File] = Info;
}
}
else
{
//bool Equals = Info.Includes.SequenceEqual(ExistingEntry.Includes, new IWYUIncludeEntryPrintableComparer());
//if (!Equals)
//{
// Logger.LogWarning($"{Info.File} - mismatch found in multiple .iwyu-files");
//}
}
}
}
// TODO: Fix bad formatting coming from iwyu
foreach (IWYUIncludeEntry Entry in Info.IncludesSeenInFile)
{
if (Entry.Printable[0] == '<')
{
Entry.Printable = Entry.Printable.Substring(1, Entry.Printable.Length - 2);
Entry.System = true;
}
}
Info.Module = GetModule(Info.File, PathToModule);
// Some special logic for headers.
if (Info.File.EndsWith(".h"))
{
// We need to add entries for .generated.h. They are not in the iwyu files
foreach (IWYUIncludeEntry Include in Info.Includes)
{
if (Include.Full.Contains("/UHT/"))
{
lock (GeneratedHeaderInfos)
{
GeneratedHeaderInfos.Add(Include.Full);
}
break;
}
else if (Include.Full.Contains("/VNI/"))
{
lock (GeneratedHeaderInfos)
{
GeneratedHeaderInfos.Add(Include.Full);
}
}
}
}
}
}
catch (Exception e)
{
Logger.LogError($"Failed to parse json {IWYUFilePath}: {e.Message} - File will be deleted");
File.Delete(IWYUFilePath);
Interlocked.Exchange(ref ReadSuccess, 0);
return;
}
});
// Something went wrong parsing iwyu files.
if (ReadSuccess == 0)
{
return -1;
}
if (bWriteToc)
{
Logger.LogInformation($"Writing TOC that references all .iwyu files ");
DirectoryReference TocDir = DirectoryReference.Combine(Unreal.EngineDirectory, "Intermediate", "IWYU");
DirectoryReference.CreateDirectory(TocDir);
string TocName = TargetName + ".txt";
FileReference TocReference = FileReference.Combine(TocDir, TocName);
FileReference.WriteAllLines(TocReference, TocContent);
}
}
KeyValuePair GetInfo(string Include, string Path)
{
string FullPath = DirectoryReference.Combine(Unreal.EngineDirectory, Path).FullName.Replace('\\', '/');
IWYUInfo? Out = null;
if (!Infos.TryGetValue(FullPath, out Out))
{
return new(Include, null);
}
IWYUIncludeEntry Entry = new();
Entry.Printable = Include;
Entry.Full = FullPath;
Entry.Resolved = Out;
return new(Include, Entry);
}
Dictionary SpecialIncludes = new Dictionary(
new[]
{
GetInfo("UObject/ObjectMacros.h", "Source/Runtime/CoreUObject/Public/UObject/ObjectMacros.h"),
GetInfo("UObject/ScriptMacros.h", "Source/Runtime/CoreUObject/Public/UObject/ScriptMacros.h"),
GetInfo("VerseInteropUtils.h", "Restricted/NotForLicensees/Plugins/Solaris/Source/VerseNative/Public/VerseInteropTypes.h"),
GetInfo("Containers/ContainersFwd.h", "Source/Runtime/Core/Public/Containers/ContainersFwd.h"),
GetInfo("Misc/OptionalFwd.h", "Source/Runtime/Core/Public/Misc/OptionalFwd.h"),
GetInfo("Templates/SharedPointerFwd.h", "Source/Runtime/Core/Public/Templates/SharedPointerFwd.h"),
GetInfo("Misc/ExpressionParserTypesFwd.h", "Source/Runtime/Core/Public/Misc/ExpressionParserTypesFwd.h"),
}
);
Dictionary ForwardingHeaders = new Dictionary()
{
{ "TMap", SpecialIncludes["Containers/ContainersFwd.h"] },
{ "TSet", SpecialIncludes["Containers/ContainersFwd.h"] },
{ "TArray", SpecialIncludes["Containers/ContainersFwd.h"] },
{ "TArrayView", SpecialIncludes["Containers/ContainersFwd.h"] },
{ "TOptional", SpecialIncludes["Misc/OptionalFwd.h"] },
{ "TSharedPtr", SpecialIncludes["Templates/SharedPointerFwd.h"] },
{ "TSharedRef", SpecialIncludes["Templates/SharedPointerFwd.h"] },
{ "TCompiledToken", SpecialIncludes["Misc/ExpressionParserTypesFwd.h"] },
{ "TExpressionToken", SpecialIncludes["Misc/ExpressionParserTypesFwd.h"] },
};
// Add all .generated.h files as entries in the lookup and explicitly add the includes they have which will never be removed
Logger.LogInformation($"Generating infos for .generated.h files...");
if (GeneratedHeaderInfos.Count > 0)
{
IWYUIncludeEntry? ObjectMacrosInclude = SpecialIncludes["UObject/ObjectMacros.h"];
IWYUIncludeEntry? ScriptMacrosInclude = SpecialIncludes["UObject/ScriptMacros.h"];
IWYUIncludeEntry? VerseInteropUtilsInclude = SpecialIncludes["VerseInteropUtils.h"];
foreach (string Gen in GeneratedHeaderInfos)
{
IWYUInfo GenInfo = new();
GenInfo.File = Gen;
if (Gen.EndsWith(".generated.h"))
{
if (ObjectMacrosInclude != null)
{
GenInfo.IncludesSeenInFile.Add(ObjectMacrosInclude);
GenInfo.Includes.Add(ObjectMacrosInclude);
}
if (ScriptMacrosInclude != null)
{
GenInfo.IncludesSeenInFile.Add(ScriptMacrosInclude);
GenInfo.Includes.Add(ScriptMacrosInclude);
}
}
else
{
if (VerseInteropUtilsInclude != null)
{
GenInfo.IncludesSeenInFile.Add(VerseInteropUtilsInclude);
GenInfo.Includes.Add(VerseInteropUtilsInclude);
}
}
Infos.Add(Gen, GenInfo);
}
}
Logger.LogInformation($"Found {Infos.Count} IWYU entries...");
Logger.LogInformation($"Resolving Includes...");
LinuxPlatformSDK PlatformSDK = new LinuxPlatformSDK(Logger);
DirectoryReference? BaseLinuxPath = PlatformSDK.GetBaseLinuxPathForArchitecture(LinuxPlatform.DefaultHostArchitecture);
string SystemPath = BaseLinuxPath!.FullName.Replace('\\', '/');
HashSet IncludedInsideDecl = new();
Dictionary SkippedHeaders = new();
Parallel.ForEach(Infos.Values, Info =>
{
// If KeepAsIs we transfer all "seen includes" into the include list.
if (Info.Module != null && Info.Module.Rules.IWYUSupport == IWYUSupport.KeepAsIs)
{
foreach (IWYUIncludeEntry Entry in Info.IncludesSeenInFile)
{
// Special hack, we don't want CoreMinimal to be the reason we remove includes transitively
if (!Entry.Printable.Contains("CoreMinimal.h", StringComparison.Ordinal))
{
Info.Includes.Add(Entry);
}
}
}
if (!Info.IsCpp)
{
// See if we need to replace forward declarations with includes
Info.ForwardDeclarations.RemoveAll(Entry =>
{
int LastSpace = Entry.Printable.LastIndexOf(' ');
string TypeName = Entry.Printable.Substring(LastSpace + 1, Entry.Printable.Length - LastSpace - 2);
IWYUIncludeEntry? IncludeEntry;
ForwardingHeaders.TryGetValue(TypeName, out IncludeEntry);
if (IncludeEntry == null)
{
return false;
}
Info.Includes.Add(IncludeEntry);
return true;
});
}
// We don't want to mess around with third party includes.. since we can't see the hierarchy we just assumes that they are optimally included
Info.Includes.RemoveAll(Entry => Entry.Full.Contains("/ThirdParty/", StringComparison.Ordinal) || Entry.Full.StartsWith(SystemPath));
foreach (IWYUIncludeEntry Entry in Info.IncludesSeenInFile)
{
if (Entry.Full.Contains("/ThirdParty/", StringComparison.Ordinal) || Entry.Full.StartsWith(SystemPath, StringComparison.Ordinal))
{
Info.Includes.Add(Entry);
}
if (Entry.InsideDecl && Infos.TryGetValue(Entry.Full, out Entry.Resolved))
{
lock (IncludedInsideDecl)
{
IncludedInsideDecl.Add(Entry.Resolved);
Info.Includes.Add(Entry);
}
}
}
/*
if (Info.IncludesSeenInFile.Count == 0)
{
if (Info.File.EndsWith(".inl"))
{
Info.Includes.Clear();
Info.ForwardDeclarations.Clear();
}
}
*/
// Definitions.h is automatically added in the reponse file and iwyu sees it as not included
// If there are no includes but we depend on Definitions.h (which is missing) we add Platform.h because it is most likely a _API entry in the file
/*
if (Info.Includes.Count == 0)
{
bool IsUsingDefinitionsH = false;
foreach (var Missing in Info.MissingIncludes)
{
IsUsingDefinitionsH = IsUsingDefinitionsH || Missing.Full.Contains("Definitions.h");
}
if (IsUsingDefinitionsH)
{
Info.Includes.Add(new IWYUIncludeEntry() { Full = PlatformInfo!.File, Printable = "HAL/Platform.h", Resolved = PlatformInfo });
}
}
*/
foreach (IWYUIncludeEntry Include in Info.Includes)
{
if (Include.Resolved == null)
{
if (!Infos.TryGetValue(Include.Full, out Include.Resolved))
{
if (!Include.Full.Contains(".gen.cpp") && !Include.Full.Contains("/ThirdParty/") && !Include.Full.Contains("/AutoSDK/") && !Include.Full.Contains("/VNI/"))
{
lock (SkippedHeaders)
{
if (!SkippedHeaders.TryGetValue(Include.Full, out Include.Resolved))
{
IWYUInfo NewInfo = new();
Include.Resolved = NewInfo;
NewInfo.File = Include.Full;
SkippedHeaders.Add(Include.Full, NewInfo);
}
}
}
}
}
}
});
int InfosCount = Infos.Count;
Logger.LogInformation($"Parsing included headers not supporting being compiled...");
ParseSkippedHeaders(SkippedHeaders, Infos, PathToModule, NameToModule);
Logger.LogInformation($"Added {Infos.Count - InfosCount} more read-only IWYU entries...");
foreach (IWYUInfo Info in IncludedInsideDecl)
{
if (Info.IncludesSeenInFile.Count != 0 && !Info.File.EndsWith("ScriptSerialization.h")) // Remove include in ScriptSerialization.h and remove this check
{
Logger.LogWarning($"{Info.File} - Included inside declaration in other file but has includes itself.");
}
Info.Includes.Clear();
Info.ForwardDeclarations.Clear();
}
Logger.LogInformation($"Generating transitive include lists and forward declaration lists...");
Parallel.ForEach(Infos.Values, Info =>
{
Stack Stack = new();
Info.TransitiveIncludes.EnsureCapacity(300);
Info.TransitiveForwardDeclarations.EnsureCapacity(300);
CalculateTransitive(Info, Info, Stack, Info.TransitiveIncludes, Info.TransitiveForwardDeclarations, false);
});
// If we have built .gen.cpp it means that it is not inlined in another cpp file
// And we need to promote the includes needed to the header since we can never modify the .gen.cpp file
Logger.LogInformation($"Transferring needed includes from .gen.cpp to owning file...");
Parallel.ForEach(GeneratedCppInfos, GeneratedCpp =>
{
// First, check which files .gen.cpp will see
HashSet SeenTransitiveIncludes = new();
foreach (IWYUIncludeEntry SeenInclude in GeneratedCpp.IncludesSeenInFile)
{
SeenTransitiveIncludes.Add(SeenInclude.Full);
foreach (IWYUIncludeEntry Include in GeneratedCpp.Includes)
{
IWYUInfo? IncludeInfo;
if (SeenInclude.Printable == Include.Printable && Infos.TryGetValue(Include.Full, out IncludeInfo))
{
foreach (string I in IncludeInfo.TransitiveIncludes.Keys)
{
SeenTransitiveIncludes.Add(I);
}
break;
}
}
}
IWYUInfo? IncluderInfo = null;
// If there is only one file in .iwyu it means that .gen.cpp was compiled separately
if (GeneratedCpp.Source!.Files.Count == 1)
{
int NameStart = GeneratedCpp.File.LastIndexOf('/');
string HeaderName = GeneratedCpp.File.Substring(NameStart + 1, GeneratedCpp.File.Length - NameStart - ".gen.cpp".Length) + "h";
foreach (IWYUIncludeEntry Include in GeneratedCpp.Includes)
{
NameStart = Include.Full.LastIndexOf('/');
string IncludeName = Include.Full.Substring(NameStart + 1);
if (HeaderName == IncludeName)
{
Infos.TryGetValue(Include.Full, out IncluderInfo);
break;
}
}
}
else // this .gen.cpp is inlined in a cpp..
{
IncluderInfo = GeneratedCpp.Source.Files.FirstOrDefault(I => I.File.Contains(".cpp") && !I.File.Contains(".gen"));
}
if (IncluderInfo == null)
{
return;
}
foreach (IWYUIncludeEntry Include in GeneratedCpp.Includes)
{
if (SeenTransitiveIncludes.Contains(Include.Full))
{
continue;
}
// TODO: Remove UObject check once we've added "IWYU pragma: keep" around the includes in ScriptMacros and ObjectMacros
if (Include.Full.Contains(".generated.h") || Include.Printable.StartsWith("UObject/"))
{
continue;
}
if (!IncluderInfo!.TransitiveIncludes.ContainsKey(Include.Full))
{
if (!Include.Full.Contains("/ThirdParty/") && !Include.Full.StartsWith(SystemPath))
{
IncluderInfo.Includes.Add(Include);
}
}
}
});
if (bCompare)
{
return CompareFiles(Infos, ValidPaths, PathToModule, NameToModule, Logger);
}
else
{
return UpdateFiles(Infos, ValidPaths, Logger);
}
}
static UEBuildModule? GetModule(string File, Dictionary PathToModule)
{
string FilePath = File;
while (true)
{
int LastIndexOfSlash = FilePath.LastIndexOf('/');
if (LastIndexOfSlash == -1)
{
break;
}
FilePath = FilePath.Substring(0, LastIndexOfSlash);
UEBuildModule? Module;
if (PathToModule.TryGetValue(FilePath, out Module))
{
return Module;
}
}
return null;
}
static string? GetFullName(string Include, string From, UEBuildModule? Module, Dictionary NameToModule, HashSet? Visited = null)
{
if (Module == null)
{
return null;
}
foreach (DirectoryReference? Dir in Module.PublicIncludePaths.Union(Module.PrivateIncludePaths).Union(Module.PublicSystemIncludePaths))
{
FileReference FileReference = FileReference.Combine(Dir, Include);
FileItem FileItem = FileItem.GetItemByFileReference(FileReference);
if (FileItem.Exists)
{
return FileItem.Location.FullName.Replace('\\', '/');
}
}
bool Nested = Visited != null;
if (!Nested)
{
FileReference FileReference = FileReference.Combine(FileReference.FromString(From).Directory, Include);
FileItem FileItem = FileItem.GetItemByFileReference(FileReference);
if (FileItem.Exists)
{
return FileItem.Location.FullName.Replace('\\', '/');
}
}
foreach (string? PublicModule in Module.Rules.PublicDependencyModuleNames.Union(Module.Rules.PublicIncludePathModuleNames).Union(Module.Rules.PrivateIncludePathModuleNames).Union(Module.Rules.PrivateDependencyModuleNames))
{
UEBuildModule? DependencyModule;
if (NameToModule.TryGetValue(PublicModule, out DependencyModule))
{
if (Visited == null)
{
Visited = new();
Visited.Add(Module);
Visited.Add(DependencyModule);
}
else if (!Visited.Add(DependencyModule))
{
continue;
}
string? Str = GetFullName(Include, From, DependencyModule, NameToModule, Visited);
if (Str != null)
{
return Str;
}
}
}
if (!Nested)
{
// This should only show system includes..
//Console.WriteLine($"Can't resolve {Include} from module {Module.Name}");
}
return null;
}
static void ParseSkippedHeaders(Dictionary SkippedHeaders, Dictionary Infos, Dictionary PathToModule, Dictionary NameToModule)
{
foreach (KeyValuePair Kvp in SkippedHeaders)
{
Infos.Add(Kvp.Key, Kvp.Value);
}
while (true)
{
Dictionary NewSkippedHeaders = new();
Parallel.ForEach(SkippedHeaders.Values, Info =>
{
Info.Module = GetModule(Info.File, PathToModule);
bool HasIncludeGuard = false;
int IfCount = 0;
string[] Lines = File.ReadAllLines(Info.File);
foreach (string Line in Lines)
{
ReadOnlySpan LineSpan = Line.AsSpan().TrimStart();
if (LineSpan.Length == 0 || LineSpan[0] != '#')
{
continue;
}
LineSpan = LineSpan.Slice(1).TrimStart();
if (LineSpan.StartsWith("if"))
{
// Include guards are special
if (!HasIncludeGuard)
{
if (LineSpan.StartsWith("ifndef ") && LineSpan.EndsWith("_H"))
{
HasIncludeGuard = true;
continue;
}
}
++IfCount;
}
else if (LineSpan.StartsWith("endif"))
{
--IfCount;
}
else if (LineSpan.StartsWith("include"))
{
ReadOnlySpan IncludeSpan = LineSpan.Slice("include".Length).TrimStart();
char LeadingIncludeChar = IncludeSpan[0];
if (LeadingIncludeChar == '"' || LeadingIncludeChar == '<')
{
ReadOnlySpan Start = IncludeSpan.Slice(1);
int EndIndex = Start.IndexOf(LeadingIncludeChar == '"' ? '"' : '>');
ReadOnlySpan FileSpan = Start.Slice(0, EndIndex);
string Include = FileSpan.ToString();
string? File = GetFullName(Include, Info.File, Info.Module, NameToModule);
if (File != null)
{
IWYUInfo? IncludeInfo;
lock (Infos)
{
if (!Infos.TryGetValue(File, out IncludeInfo))
{
if (!SkippedHeaders.TryGetValue(File, out IncludeInfo))
{
IncludeInfo = new();
IncludeInfo.File = File;
NewSkippedHeaders.Add(File, IncludeInfo);
Infos.Add(File, IncludeInfo);
}
}
}
if (IncludeInfo != null)
{
IWYUIncludeEntry Entry = new();
Entry.Full = File;
Entry.Printable = Include;
Entry.System = LeadingIncludeChar == '<';
Entry.Resolved = IncludeInfo;
Info.IncludesSeenInFile.Add(Entry);
Info.Includes.Add(Entry);
}
}
}
}
}
});
if (NewSkippedHeaders.Count == 0)
{
return;
}
SkippedHeaders = NewSkippedHeaders;
}
}
static bool IsValidForUpdate(IWYUInfo Info, HashSet ValidPaths, bool ObeyModuleRules)
{
if (Info.Source == null) // .generated.h is also in this list, ignore them
{
return false;
}
if (ObeyModuleRules && Info.Module?.Rules.IWYUSupport != IWYUSupport.Full)
{
return false;
}
// There are some codegen files with this name
if (Info.File.Contains(".gen.h"))
{
return false;
}
// Filter out files
foreach (string ValidPath in ValidPaths)
{
if (Info.File.Contains(ValidPath))
{
return true;
}
}
return false;
}
int UpdateFiles(Dictionary Infos, HashSet ValidPaths, ILogger Logger)
{
object? ShouldLog = bWrite ? null : new();
List>> UpdatedFiles = new(); // ,
HashSet SkippedFiles = new();
int SkippedCount = 0;
int OutOfDateCount = 0;
Logger.LogInformation($"Updating code files (in memory)...");
uint FilesParseCount = 0;
int ProcessSuccess = 1;
Parallel.ForEach(Infos.Values, Info =>
{
if (!IsValidForUpdate(Info, ValidPaths, true))
{
return;
}
bool IsCpp = Info.IsCpp;
bool IsPrivate = IsCpp || Info.File.Contains("/Private/");
bool IsInternal = Info.File.Contains("/Internal/");
// If we only want to update private files we early out for non-private headers
if (!IsPrivate && (bUpdateOnlyPrivate || (Info.Module?.Rules.IWYUSupport == IWYUSupport.KeepPublicAsIsForNow)))
{
return;
}
Interlocked.Increment(ref FilesParseCount);
string MatchingH = "";
if (IsCpp)
{
int LastSlash = Info.File.LastIndexOf('/') + 1;
MatchingH = Info.File.Substring(LastSlash, Info.File.Length - LastSlash - 4) + ".h";
}
Dictionary TransitivelyIncluded = new();
SortedSet CleanedupIncludes = new();
SortedSet ForwardDeclarationsToAdd = new();
foreach (IWYUIncludeEntry Include in Info.Includes)
{
// We never remove header with name matching cpp
string NameWithoutPath = Include.Printable;
int LastSlash = NameWithoutPath.LastIndexOf("/");
if (LastSlash != -1)
{
NameWithoutPath = NameWithoutPath.Substring(LastSlash + 1);
}
string QuotedPrintable = Include.System ? $"<{Include.Printable}>" : $"\"{Include.Printable}\"";
bool Keep = true;
if (Info.File == Include.Full) // Sometimes IWYU outputs include to the same file if .gen.cpp is inlined and includes file with slightly different path. just skip those
{
Keep = false;
}
else if (IsCpp && MatchingH == NameWithoutPath)
{
Keep = true;
}
else if (!bNoTransitiveIncludes)
{
foreach (IWYUIncludeEntry Include2 in Info.Includes)
{
if (Include2.Resolved != null && Include != Include2)
{
string Key = Include.Full;
string? TransitivePath;
if (Include2.Resolved.TransitiveIncludes!.TryGetValue(Key, out TransitivePath))
{
if (ShouldLog != null)
{
TransitivelyIncluded.TryAdd(QuotedPrintable, String.Join(" -> ", Include2.Printable, TransitivePath));
}
Keep = false;
break;
}
}
}
}
if (Keep)
{
CleanedupIncludes.Add(QuotedPrintable);
}
}
// We don't remove seen includes that are redundant because they are included through other includes that are needed.
if (!bRemoveRedundantIncludes)
{
List ToReadd = new();
foreach (IWYUIncludeEntry Seen in Info.IncludesSeenInFile)
{
string QuotedPrintable = Seen.System ? $"<{Seen.Printable}>" : $"\"{Seen.Printable}\"";
if (!CleanedupIncludes.Contains(QuotedPrintable) && Info.TransitiveIncludes.ContainsKey(Seen.Full))
{
CleanedupIncludes.Add(QuotedPrintable);
}
}
}
// Ignore forward declarations for cpp files. They are very rarely needed and we let the user add them manually instead
if (!IsCpp)
{
foreach (IWYUForwardEntry ForwardDeclaration in Info.ForwardDeclarations)
{
bool Add = ForwardDeclaration.Present == false;
if (!bNoTransitiveIncludes)
{
foreach (IWYUIncludeEntry Include2 in Info.Includes)
{
if (Include2.Resolved != null && Include2.Resolved.TransitiveForwardDeclarations!.ContainsKey(ForwardDeclaration.Printable))
{
Add = false;
break;
}
}
}
if (Add)
{
ForwardDeclarationsToAdd.Add(ForwardDeclaration.Printable);
}
}
}
// Read all lines of the header/source file
string[] ExistingLines = File.ReadAllLines(Info.File);
SortedDictionary LinesToRemove = new();
SortedSet IncludesToAdd = new(CleanedupIncludes);
bool HasIncludes = false;
string? FirstForwardDeclareLine = null;
HashSet SeenIncludes = new();
foreach (IWYUIncludeEntry SeenInclude in Info.IncludesSeenInFile)
{
if (SeenInclude.System)
{
SeenIncludes.Add($"<{SeenInclude.Printable}>");
}
else
{
SeenIncludes.Add($"\"{SeenInclude.Printable}\"");
}
}
bool ForceKeepScope = false;
bool ErrorOnMoreIncludes = false;
int LineIndex = -1;
// This makes sure that we have at least HAL/Platform.h included if the file contains XXX_API
bool Contains_API = false;
bool LookFor_API = false;
if (Info.Includes.Count == 0)
{
foreach (IWYUMissingInclude Missing in Info.MissingIncludes)
{
LookFor_API = LookFor_API || Missing.Full.Contains("Definitions.h");
}
}
// Traverse all lines in file and figure out which includes that should be added or removed
foreach (string Line in ExistingLines)
{
++LineIndex;
ReadOnlySpan LineSpan = Line.AsSpan().Trim();
bool StartsWithHash = LineSpan.Length > 0 && LineSpan[0] == '#';
if (StartsWithHash)
{
LineSpan = LineSpan.Slice(1).TrimStart();
}
if (!StartsWithHash || !LineSpan.StartsWith("include"))
{
// Might be forward declaration..
if (ForwardDeclarationsToAdd.Remove(Line)) // Skip adding the ones that already exists
{
FirstForwardDeclareLine ??= Line;
}
if (Line.Contains("IWYU pragma: "))
{
if (Line.Contains(": begin_keep"))
{
ForceKeepScope = true;
}
else if (Line.Contains(": end_keep"))
{
ForceKeepScope = false;
}
}
// File is autogenerated by some tool, don't mess with it
if (Line.Contains("AUTO GENERATED CONTENT, DO NOT MODIFY"))
{
return;
}
if (LookFor_API && !Contains_API && Line.Contains("_API", StringComparison.Ordinal))
{
Contains_API = true;
}
continue;
}
if (ErrorOnMoreIncludes)
{
Interlocked.Exchange(ref ProcessSuccess, 0);
Logger.LogError($"{Info.File} - Found special include using macro and did not expect more includes in this file");
return;
}
HasIncludes = true;
bool ForceKeep = false;
ReadOnlySpan IncludeSpan = LineSpan.Slice("include".Length).TrimStart();
char LeadingIncludeChar = IncludeSpan[0];
if (LeadingIncludeChar != '"' && LeadingIncludeChar != '<')
{
if (IncludeSpan.IndexOf("UE_INLINE_GENERATED_CPP_BY_NAME") != -1)
{
int Open = IncludeSpan.IndexOf('(') + 1;
int Close = IncludeSpan.IndexOf(')');
string ActualInclude = $"\"{IncludeSpan.Slice(Open, Close - Open).ToString()}.gen.cpp\"";
IncludesToAdd.Remove(ActualInclude);
}
else if (IncludeSpan.IndexOf("COMPILED_PLATFORM_HEADER") != -1)
{
int Open = IncludeSpan.IndexOf('(') + 1;
int Close = IncludeSpan.IndexOf(')');
string ActualInclude = $"\"Linux/Linux{IncludeSpan.Slice(Open, Close - Open).ToString()}\"";
IncludesToAdd.Remove(ActualInclude);
}
else
{
// TODO: These are includes made through defines. IWYU should probably report these in their original shape
// .. so a #include MY_SPECIAL_INCLUDE is actually reported as MY_SPECIAL_INCLUDE from IWYU instead of what the define expands to
// For now, let's assume that if there is one line in the file that is an include
if (IncludesToAdd.Count == 1)
{
//IncludesToAdd.Clear();
//ErrorOnMoreIncludes = true;
}
}
continue;
}
else
{
int Index = IncludeSpan.Slice(1).IndexOf(LeadingIncludeChar == '"' ? '"' : '>');
if (Index != -1)
{
IncludeSpan = IncludeSpan.Slice(0, Index + 2);
}
if (Line.Contains("IWYU pragma: ", StringComparison.Ordinal))
{
ForceKeep = true;
}
}
string Include = IncludeSpan.ToString();
// If Include is not seen it means that it is probably inside a #if/#endif with condition false. These includes we can't touch
if (!SeenIncludes.Contains(Include))
{
continue;
}
if (!ForceKeep && !ForceKeepScope && !CleanedupIncludes.Contains(Include))
{
LinesToRemove.TryAdd(Line, Include);
}
else
{
IncludesToAdd.Remove(Include);
}
}
if (Contains_API && LookFor_API)
{
//IncludesToAdd.Add(new IWYUIncludeEntry() { Full = PlatformInfo!.File, Printable = "HAL/Platform.h", Resolved = PlatformInfo });
if (!LinesToRemove.Remove("#include \"HAL/Platform.h\""))
{
IncludesToAdd.Add("\"HAL/Platform.h\"");
}
}
// Nothing has changed! early out of this file
if (IncludesToAdd.Count == 0 && LinesToRemove.Count == 0 && ForwardDeclarationsToAdd.Count == 0)
{
return;
}
// If code file last write time is newer than IWYU file this means that iwyu is not up to date and needs to compile before we can apply anything
if (!bIgnoreUpToDateCheck)
{
FileInfo IwyuFileInfo = new FileInfo(Info.Source!.Name!);
FileInfo CodeFileInfo = new FileInfo(Info.File!);
if (CodeFileInfo.LastWriteTime > IwyuFileInfo.LastWriteTime)
{
Interlocked.Increment(ref OutOfDateCount);
return;
}
}
SortedSet LinesToAdd = new();
foreach (string IncludeToAdd in IncludesToAdd)
{
LinesToAdd.Add("#include " + IncludeToAdd);
}
if (ShouldLog != null)
{
lock (ShouldLog)
{
System.Console.WriteLine(Info.File);
foreach (string I in LinesToAdd)
{
System.Console.WriteLine(" +" + I);
}
foreach (KeyValuePair Pair in LinesToRemove)
{
System.Console.Write(" -" + Pair.Key);
string? Reason;
if (TransitivelyIncluded.TryGetValue(Pair.Value, out Reason))
{
System.Console.Write(" (Transitively included from " + Reason + ")");
}
System.Console.WriteLine();
}
foreach (string I in ForwardDeclarationsToAdd)
{
System.Console.WriteLine(" +" + I);
}
System.Console.WriteLine();
}
}
List NewLines = new(ExistingLines.Length);
SortedSet LinesRemoved = new();
if (!HasIncludes)
{
LineIndex = 0;
foreach (string OldLine in ExistingLines)
{
NewLines.Add(OldLine);
++LineIndex;
if (!OldLine.TrimStart().StartsWith("#pragma"))
{
continue;
}
NewLines.Add("");
foreach (string Line in LinesToAdd)
{
NewLines.Add(Line);
}
NewLines.AddRange(ExistingLines.Skip(LineIndex));
break;
}
}
else
{
// This is a bit of a tricky problem to solve in a generic ways since there are lots of exceptions and hard to make assumptions
// Right now we make the assumption that if there are
// That will be the place where we add/remove our includes.
int ContiguousNonIncludeLineCount = 0;
bool IsInFirstIncludeBlock = true;
int LastSeenIncludeBeforeCode = -1;
int FirstForwardDeclareLineIndex = -1;
foreach (string OldLine in ExistingLines)
{
ReadOnlySpan OldLineSpan = OldLine.AsSpan().TrimStart();
bool StartsWithHash = OldLineSpan.Length > 0 && OldLineSpan[0] == '#';
if (StartsWithHash)
{
OldLineSpan = OldLineSpan.Slice(1).TrimStart();
}
bool IsInclude = StartsWithHash && OldLineSpan.StartsWith("include");
string OldLineTrimmedStart = OldLine.TrimStart();
if (!IsInclude)
{
if (IsInFirstIncludeBlock)
{
if (!String.IsNullOrEmpty(OldLineTrimmedStart))
{
++ContiguousNonIncludeLineCount;
}
if (LastSeenIncludeBeforeCode != -1 && ContiguousNonIncludeLineCount > 10)
{
IsInFirstIncludeBlock = false;
}
if (OldLineTrimmedStart.StartsWith("#if"))
{
// This logic is a bit shaky but handle the situations where file starts with #if and ends with #endif
if (LastSeenIncludeBeforeCode != -1)
{
IsInFirstIncludeBlock = false;
}
}
// This need to be inside "IsInFirstIncludeBlock" check because some files have forward declares far down in the file
if (FirstForwardDeclareLine == OldLine)
{
FirstForwardDeclareLineIndex = NewLines.Count;
}
}
NewLines.Add(OldLine);
continue;
}
ContiguousNonIncludeLineCount = 0;
if (!LinesToRemove.ContainsKey(OldLine))
{
// If we find #include SOME_DEFINE we assume that should be last and "end" the include block with that
if (!OldLineTrimmedStart.Contains('\"') && !OldLineTrimmedStart.Contains('<'))
{
IsInFirstIncludeBlock = false;
}
else if (LinesToAdd.Count != 0 && (!IsCpp || LastSeenIncludeBeforeCode != -1))
{
string LineToAdd = LinesToAdd.First();
if (LineToAdd.CompareTo(OldLine) < 0)
{
NewLines.Add(LineToAdd);
LinesToAdd.Remove(LineToAdd);
}
}
NewLines.Add(OldLine);
}
else
{
FirstForwardDeclareLineIndex = -1; // This should never happen, but just in case, reset since lines have changed
LinesRemoved.Add(OldLine);
}
if (IsInFirstIncludeBlock)
{
LastSeenIncludeBeforeCode = NewLines.Count - 1;
}
}
if (LinesToAdd.Count > 0)
{
int InsertPos = LastSeenIncludeBeforeCode + 1;
if (NewLines[LastSeenIncludeBeforeCode].Contains(".generated.h", StringComparison.Ordinal))
{
--InsertPos;
}
NewLines.InsertRange(InsertPos, LinesToAdd);
LastSeenIncludeBeforeCode += LinesToAdd.Count;
if (FirstForwardDeclareLineIndex != -1 && FirstForwardDeclareLineIndex > LastSeenIncludeBeforeCode)
{
FirstForwardDeclareLineIndex += LinesToAdd.Count;
}
}
if (ForwardDeclarationsToAdd.Count > 0)
{
int InsertPos;
if (FirstForwardDeclareLineIndex == -1)
{
InsertPos = LastSeenIncludeBeforeCode + 1;
if (!String.IsNullOrEmpty(NewLines[InsertPos]))
{
NewLines.Insert(InsertPos++, "");
}
NewLines.Insert(InsertPos + 1, "");
}
else
{
InsertPos = FirstForwardDeclareLineIndex;
}
NewLines.InsertRange(InsertPos + 1, ForwardDeclarationsToAdd);
}
}
// If file is public, in engine and we have a deprecation tag set we will
// add a deprecated include scope at the end of the file (unless scope already exists, then we'll add it inside that)
string EngineDir = Unreal.EngineDirectory.FullName.Replace('\\', '/');
if (!(IsPrivate || IsInternal) && Info.File.StartsWith(EngineDir) && !String.IsNullOrEmpty(HeaderDeprecationTag))
{
Dictionary PrintableToFull = new();
foreach (IWYUIncludeEntry Seen in Info.IncludesSeenInFile)
{
PrintableToFull.TryAdd(Seen.Printable, Seen.Full);
}
// Remove the includes in LinesRemoved
LinesRemoved.RemoveWhere(Line =>
{
ReadOnlySpan IncludeSpan = Line.AsSpan(8).TrimStart();
char LeadingIncludeChar = IncludeSpan[0];
if (LeadingIncludeChar != '"' && LeadingIncludeChar != '<')
{
return false;
}
int Index = IncludeSpan.Slice(1).IndexOf(LeadingIncludeChar == '"' ? '"' : '>');
if (Index == -1)
{
return false;
}
IncludeSpan = IncludeSpan.Slice(1, Index);
string? Full;
if (!PrintableToFull.TryGetValue(IncludeSpan.ToString(), out Full))
{
return false;
}
return Info.TransitiveIncludes.ContainsKey(Full);
});
if (LinesRemoved.Count > 0)
{
int IndexOfDeprecateScope = -1;
string Match = "#if " + HeaderDeprecationTag;
for (int I = NewLines.Count - 1; I != 0; --I)
{
if (NewLines[I] == Match)
{
IndexOfDeprecateScope = I + 1;
break;
}
}
if (IndexOfDeprecateScope == -1)
{
NewLines.Add("");
NewLines.Add(Match);
IndexOfDeprecateScope = NewLines.Count;
NewLines.Add("#endif");
}
else
{
// Scan the already added includes to prevent additional adds.
}
NewLines.InsertRange(IndexOfDeprecateScope, LinesRemoved);
}
}
lock (UpdatedFiles)
{
UpdatedFiles.Add(new(Info, NewLines));
}
});
// Something went wrong processing code files.
if (ProcessSuccess == 0)
{
return -1;
}
Logger.LogInformation($"Parsed {FilesParseCount} and updated {UpdatedFiles.Count} files (Found {OutOfDateCount} .iwyu files out of date)");
// Wooohoo, all files are up-to-date
if (UpdatedFiles.Count == 0)
{
Logger.LogInformation($"All files are up to date!");
return 0;
}
// If we have been logging we can exit now since we don't want to write any files to disk
if (ShouldLog != null)
{
return 0;
}
List P4Processes = new();
Action AddP4Process = (Arguments) =>
{
System.Diagnostics.Process Process = new System.Diagnostics.Process();
System.Diagnostics.ProcessStartInfo StartInfo = new System.Diagnostics.ProcessStartInfo();
Process.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
Process.StartInfo.CreateNoWindow = true;
Process.StartInfo.FileName = "p4.exe";
Process.StartInfo.Arguments = Arguments;
Process.Start();
P4Processes.Add(Process);
};
Func WaitForP4 = () =>
{
bool P4Success = true;
foreach (System.Diagnostics.Process P4Process in P4Processes)
{
P4Process.WaitForExit();
if (P4Process.ExitCode != 0)
{
P4Success = false;
Logger.LogError($"p4 edit failed - {P4Process.StartInfo.Arguments}");
}
P4Process.Close();
}
P4Processes.Clear();
return P4Success;
};
if (!bNoP4)
{
List ReadOnlyFileInfos = new();
foreach ((IWYUInfo Info, List NewLines) in UpdatedFiles)
{
if (new FileInfo(Info.File).IsReadOnly)
{
ReadOnlyFileInfos.Add(Info);
}
}
if (ReadOnlyFileInfos.Count > 0)
{
// Check out files in batches. This can go quite crazy if there are lots of files.
// Should probably revisit this code to prevent 100s of p4 processes to start at once
Logger.LogInformation($"Opening {ReadOnlyFileInfos.Count} files for edit in P4... ({SkippedCount} files skipped)");
int ShowCount = 8;
foreach (IWYUInfo Info in ReadOnlyFileInfos)
{
Logger.LogInformation($" edit {Info.File}");
if (--ShowCount == 0)
{
break;
}
}
if (ReadOnlyFileInfos.Count > 5)
{
Logger.LogInformation($" ... and {ReadOnlyFileInfos.Count - 5} more.");
}
StringBuilder P4Arguments = new();
int BatchSize = 10;
int BatchCount = 0;
int Index = 0;
foreach (IWYUInfo Info in ReadOnlyFileInfos)
{
if (!SkippedFiles.Contains(Info.Source!))
{
P4Arguments.Append(" \"").Append(Info.File).Append('\"');
++BatchCount;
}
++Index;
if (BatchCount == BatchSize || Index == ReadOnlyFileInfos.Count)
{
AddP4Process($"edit{P4Arguments}");
P4Arguments.Clear();
BatchCount = 0;
}
}
// Waiting for edit
if (!WaitForP4())
{
return -1;
}
}
}
bool WriteSuccess = true;
Logger.LogInformation($"Writing {UpdatedFiles.Count - SkippedCount} files to disk...");
foreach ((IWYUInfo Info, List NewLines) in UpdatedFiles)
{
if (SkippedFiles.Contains(Info.Source!))
{
continue;
}
try
{
File.WriteAllLines(Info.File, NewLines);
}
catch (Exception e)
{
Logger.LogError($"Failed to write {Info.File}: {e.Message} - File will be reverted");
SkippedFiles.Add(Info.Source!); // In case other entries from same file is queued
AddP4Process($"revert {String.Join(' ', Info.Source!.Files.Select(f => f.File))}");
WriteSuccess = false;
}
}
// Waiting for reverts
if (!WaitForP4())
{
return -1;
}
if (!WriteSuccess)
{
return -1;
}
Logger.LogInformation($"Done!");
return 0;
}
///
/// Calculate all indirect transitive includes for a file. This list contains does not contain itself and will handle circular dependencies
///
static void CalculateTransitive(IWYUInfo Root, IWYUInfo Info, Stack Stack, Dictionary TransitiveIncludes, Dictionary TransitiveForwardDeclarations, bool UseSeenIncludes)
{
List Includes = UseSeenIncludes ? Info.IncludesSeenInFile : Info.Includes;
foreach (IWYUIncludeEntry Include in Includes)
{
string Key = Include.Full;
if (TransitiveIncludes.ContainsKey(Key) || Include.Resolved == Root)
{
continue;
}
Stack.Push(Include.Printable);
string TransitivePath = String.Join(" -> ", Stack.Reverse());
TransitiveIncludes.Add(Key, TransitivePath);
if (Include.Resolved != null)
{
CalculateTransitive(Root, Include.Resolved, Stack, TransitiveIncludes, TransitiveForwardDeclarations, UseSeenIncludes);
}
Stack.Pop();
}
foreach (IWYUForwardEntry ForwardDeclaration in Info.ForwardDeclarations)
{
string Key = ForwardDeclaration.Printable;
TransitiveForwardDeclarations.TryAdd(Key, Info.File);
}
}
int CompareFiles(Dictionary Infos, HashSet ValidPaths, Dictionary PathToModule, Dictionary NameToModule, ILogger Logger)
{
Logger.LogInformation($"Comparing code files...");
Dictionary SkippedHeaders = new();
Logger.LogInformation($"Parsing seen headers not supporting being compiled...");
Parallel.ForEach(Infos.Values, Info =>
{
foreach (IWYUIncludeEntry Include in Info.IncludesSeenInFile)
{
if (Include.Resolved == null)
{
if (!Infos.TryGetValue(Include.Full, out Include.Resolved))
{
lock (SkippedHeaders)
{
if (!SkippedHeaders.TryGetValue(Include.Full, out Include.Resolved))
{
IWYUInfo NewInfo = new();
Include.Resolved = NewInfo;
NewInfo.File = Include.Full;
SkippedHeaders.Add(Include.Full, NewInfo);
}
}
}
}
}
});
ParseSkippedHeaders(SkippedHeaders, Infos, PathToModule, NameToModule);
//List> RatioList = new();
ConcurrentDictionary AllSeenIncludes = new();
List>> InfosToCompare = new();
Logger.LogInformation($"Generating transitive seen include lists...");
Parallel.ForEach(Infos.Values, Info =>
{
if (!IsValidForUpdate(Info, ValidPaths, false) || Info.File.EndsWith(".h"))
{
return;
}
Dictionary SeenTransitiveIncludes = new();
Stack Stack = new();
CalculateTransitive(Info, Info, Stack, SeenTransitiveIncludes, new Dictionary(), true);
lock (InfosToCompare)
{
InfosToCompare.Add(new(Info, SeenTransitiveIncludes));
}
foreach (KeyValuePair Include in SeenTransitiveIncludes.Union(Info.TransitiveIncludes))
{
AllSeenIncludes.TryAdd(Include.Key, 0);
}
/*
if (Info.TransitiveIncludes.Count < SeenTransitiveIncludes.Count)
{
float Ratio = (float)Info.TransitiveIncludes.Count / SeenTransitiveIncludes.Count;
lock (RatioList)
RatioList.Add(new(Ratio, Info));
}
if (SeenTransitiveIncludes.Count < Info.TransitiveIncludes.Count)
Console.WriteLine($"{Info.File} - Now: {SeenTransitiveIncludes.Count} Optimized: {Info.TransitiveIncludes.Count}");
*/
});
Dictionary FileToSize = new Dictionary(AllSeenIncludes);
Logger.LogInformation($"Reading file sizes...");
Parallel.ForEach(AllSeenIncludes, Info =>
{
FileToSize[Info.Key] = (new FileInfo(Info.Key)).Length;
});
Logger.LogInformation($"Calculate total amount of bytes included per file...");
List> ByteSizeDiffList = new();
Parallel.ForEach(InfosToCompare, Kvp =>
{
long OptimizedSize = 0;
long SeenSize = 0;
foreach (string Include in Kvp.Item1.TransitiveIncludes.Keys)
{
OptimizedSize += FileToSize[Include];
}
foreach (string? Include in Kvp.Item2.Keys)
{
SeenSize += FileToSize[Include];
}
long Diff = SeenSize - OptimizedSize;
lock (ByteSizeDiffList)
{
ByteSizeDiffList.Add(new(Diff, Kvp.Item1));
}
});
ByteSizeDiffList.Sort((a, b) =>
{
if (a.Item1 == b.Item1)
{
return 0;
}
if (a.Item1 < b.Item1)
{
return 1;
}
return -1;
});
Func PrettySize = (Size) =>
{
if (Size > 1024 * 1024)
{
return (((float)Size) / (1024 * 1024)).ToString("0.00") + "mb";
}
if (Size > 1024)
{
return (((float)Size) / (1024)).ToString("0.00") + "kb";
}
return Size + "b";
};
Logger.LogInformation("");
Logger.LogInformation("Top 20 most reduction in bytes parsed by compiler frontend");
Logger.LogInformation("----------------------------------------------------------");
for (int I = 0; I != Math.Min(20, ByteSizeDiffList.Count); ++I)
{
long Saved = ByteSizeDiffList[I].Item1;
IWYUInfo File = ByteSizeDiffList[I].Item2;
long OptimizedSize = 0;
foreach (string Include in File.TransitiveIncludes.Keys)
{
OptimizedSize += FileToSize[Include];
}
float Percent = 100.0f - (((float)OptimizedSize) / (OptimizedSize + Saved) * 100.0f);
Logger.LogInformation($"{Path.GetFileName(File.File)} {PrettySize(OptimizedSize + Saved)} -> {PrettySize(OptimizedSize)} (Saved {PrettySize(Saved)} or {Percent:0.0}%)");
}
Logger.LogInformation("");
/*
RatioList.Sort((a, b) =>
{
if (a.Item1 == b.Item1)
return 0;
if (a.Item1 < b.Item1)
return -1;
return 1;
});
*/
return 0;
}
}
}