// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IncludeTool
{
///
/// Stores information about a circular include path. All headers processed by the tool should form a direct hierarchy, whereby one
/// header always includes another. If two headers include each other, we can't reason about what the IWYU derivation for each should
/// be in case they're each included by different files.
///
class IncludeCycle
{
///
/// Array of files in this cycle
///
public SourceFile[] Files;
///
/// List of observed starting points for this cycle, along with a counter for its number of appearances.
///
public List> StartingPoints;
///
/// Constructor.
///
/// List of files in this cycle
public IncludeCycle(IEnumerable InFiles)
{
Files = InFiles.ToArray();
StartingPoints = new List>();
StartingPoints.Add(new KeyValuePair(Files[0], 1));
}
///
/// Adds a new starting point for this cycle. If the starting point already exists, increases the counter for number of times it's been seen.
///
/// The first file in the cycle.
public void AddStartingPoint(SourceFile File)
{
// Increment the counter for this starting point
for(int Idx = 0; ; Idx++)
{
if(Idx == StartingPoints.Count)
{
StartingPoints.Add(new KeyValuePair(File, 1));
break;
}
else if(StartingPoints[Idx].Key == File)
{
StartingPoints[Idx] = new KeyValuePair(StartingPoints[Idx].Key, StartingPoints[Idx].Value + 1);
break;
}
}
// Find the new largest starting point
int LargestIdx = 0;
for(int Idx = 1; Idx < StartingPoints.Count; Idx++)
{
if(StartingPoints[Idx].Value > StartingPoints[LargestIdx].Value)
{
LargestIdx = Idx;
}
}
// If it's not the same as the current one, reorder the sequence
SourceFile NewStartingPoint = StartingPoints[LargestIdx].Key;
if(NewStartingPoint != Files[0])
{
int Offset = Array.IndexOf(Files, NewStartingPoint);
SourceFile[] NewFiles = new SourceFile[Files.Length];
for(int Idx = 0; Idx < Files.Length; Idx++)
{
NewFiles[Idx] = Files[(Offset + Idx) % Files.Length];
}
Files = NewFiles;
}
}
///
/// Checks whether an include cycle matches another include cycle, ignoring the start location.
///
/// The cycle to compare against
/// True if the cycles are identical, false otherwise
public bool Matches(IncludeCycle Other)
{
if(Other.Files.Length != Files.Length)
{
return false;
}
int Offset = Array.IndexOf(Other.Files, Files[0]);
if(Offset == -1)
{
return false;
}
for(int Idx = 0; Idx < Files.Length; Idx++)
{
if(Files[Idx] != Other.Files[(Offset + Idx) % Files.Length])
{
return false;
}
}
return true;
}
///
/// Returns a hash code for the current cycle, independent of the cycle's starting point.
///
/// Hash code for the cycle
public override int GetHashCode()
{
int HashCode = 0;
for(int Idx = 0; Idx < Files.Length; Idx++)
{
// Note: we deliberately do not mutate the hash code in any way that is not commutative if we start
// at any point in the array. Summing doesn't provide a huge amount of entropy, but it's safe.
HashCode += Files[Idx].Location.GetHashCode();
}
return HashCode;
}
///
/// Creates a human-readable representation of the cycle
///
/// String representation of the cycle
public override string ToString()
{
return String.Join(" -> ", Files.Concat(Files).Take(Files.Length + 1).Select(x => x.Location.GetFileName()));
}
}
///
/// Functions to find include cycles
///
static class IncludeCycles
{
///
/// Finds all include cycles from the given set of source files
///
/// Sequence of source files to check
/// List of cycles
public static List FindAll(IEnumerable Files)
{
List Cycles = new List();
foreach(SourceFile File in Files)
{
FindCyclesRecursive(File, new List(), new HashSet(), Cycles);
}
return Cycles;
}
///
/// Recurses through the given file and all of its includes, attempting to identify cycles.
///
/// The file to search through
/// Current include stack of the preprocessor
/// Set of files that have already been checked for cycles
/// List which receives any cycles that are found
static void FindCyclesRecursive(SourceFile File, List Includes, HashSet VisitedFiles, List Cycles)
{
// Check if this include forms a cycle
int IncludeIdx = Includes.IndexOf(File);
if(IncludeIdx != -1)
{
IncludeCycle NewCycle = new IncludeCycle(Includes.Skip(IncludeIdx));
for(int Idx = 0;;Idx++)
{
if(Idx == Cycles.Count)
{
Cycles.Add(NewCycle);
break;
}
else if(Cycles[Idx].Matches(NewCycle))
{
Cycles[Idx].AddStartingPoint(NewCycle.Files[0]);
break;
}
}
}
// If we haven't already looked from cycles from this include, search now
if(!VisitedFiles.Contains(File))
{
VisitedFiles.Add(File);
Includes.Add(File);
foreach(PreprocessorMarkup Markup in File.Markup)
{
if(Markup.Type == PreprocessorMarkupType.Include && Markup.IncludedFile != null)
{
SourceFile IncludedFile = Markup.IncludedFile;
if(!IncludedFile.Flags.HasFlag(SourceFileFlags.External))
{
FindCyclesRecursive(Markup.IncludedFile, Includes, VisitedFiles, Cycles);
}
}
}
Includes.RemoveAt(Includes.Count - 1);
}
}
}
}