public void RunModifier(ref List <Diff> diffs) { List <Diff> memberedClassDiffs = diffs .Where(diff => diff.Target is ClassDescriptor) .Where(diff => (diff.Target as ClassDescriptor).Members.Count > 0) .ToList(); List <Diff> newClassDiffs = memberedClassDiffs .Where(diff => diff.Type == DiffType.Add) .ToList(); List <Diff> oldClassDiffs = memberedClassDiffs .Where(diff => diff.Type == DiffType.Remove) .ToList(); if (oldClassDiffs.Count > 0 && newClassDiffs.Count > 0) { foreach (Diff newClassDiff in newClassDiffs) { // Ignore disposed diffs. if (newClassDiff.Disposed) { continue; } // Grab the summary version of the new diff. var newClass = newClassDiff.Target as ClassDescriptor; string newDiff = newClassDiff.WriteDiffTxt(false); foreach (Diff oldClassDiff in oldClassDiffs) { // Ignore disposed diffs. if (oldClassDiff.Disposed) { continue; } // Grab the summary version of the old diff. var oldClass = oldClassDiff.Target as ClassDescriptor; string oldDiff = oldClassDiff.WriteDiffTxt(false); // Try to convert the old diff into the new diff generated above. string nameChange = oldDiff .Replace(oldClass.Name, newClass.Name) .Replace("Removed", "Added"); // Intersect some of the old and new signatures to see if they are similar enough. List <string> oldLines = newDiff.Split('\r', '\n') .Select(oldLine => oldLine.Trim()) .Where(oldLine => oldLine.Length > 0) .ToList(); List <string> newLines = nameChange.Split('\r', '\n') .Select(newLine => newLine.Trim()) .Where(newLine => newLine.Length > 0) .ToList(); List <string> intersects = oldLines .Intersect(newLines) .ToList(); // If the signatures match, then this is probably a renamed class? if (intersects.Count >= ((oldLines.Count + newLines.Count) / 2)) { // HACK: To allow the members to be compared nicely, I need to change the name // of the old class to the name of the new class. However, I still want to // describe the target of the ClassName change with the old ClassName, so I // also have to create a dummy ClassDescriptor to serve as the target. ClassDescriptor dummy = new ClassDescriptor(); dummy.Name = oldClass.Name; // Create a diff describing the ClassName change. Diff nameChangeDiff = new Diff() { Type = DiffType.Rename, Field = "Class", Target = dummy, To = { newClass.Name } }; // Add this change to the diffs. diffs.Add(nameChangeDiff); // Remap the old class with the new class name. var oldClasses = oldClass.Database.Classes; var newName = newClass.Name; oldClasses.Remove(oldClass.Name); oldClass.Name = newName; oldClasses.Add(newName, oldClass); // Dispose the original class diffs. oldClassDiff.Disposed = true; newClassDiff.Disposed = true; } } } } }
private static void flagEntireClass(ClassDescriptor classDesc, DiffRecorder record, bool detailed) { Diff classDiff = record(classDesc, detailed); classDesc.Members.ForEach(memberDesc => record(memberDesc, detailed, classDiff)); }
public void RunModifier(ref List <Diff> diffs) { var merging = new Dictionary <MemberDescriptor, List <MemberDescriptor> >(); var added = collectMemberDiffs(diffs, DiffType.Add); var removed = collectMemberDiffs(diffs, DiffType.Remove); foreach (Diff targetDiff in removed) { var targetMember = targetDiff.Target as MemberDescriptor; string targetName = targetMember.Name; foreach (Diff otherDiff in added) { var otherMember = otherDiff.Target as MemberDescriptor; string otherName = otherMember.Name; if (targetName == otherName) { // Grab the classes of the two members we have selected. ClassDescriptor targetClass = targetMember.Class; ClassDescriptor otherClass = otherMember.Class; // Because the databases of these two members are different, this uses // the database of the other member, since it should be the newer one. var database = otherClass.Database; var classLookup = database.Classes; // Override targetClass with its corresponding entry in the classLookup. // This is necessary to have a snapshot of the newer class hierarchy. if (!classLookup.ContainsKey(targetClass.Name)) { continue; } targetClass = classLookup[targetClass.Name]; // Now test the ancestry of the two classes. if (otherClass.IsAncestorOf(targetClass)) { if (!merging.ContainsKey(otherMember)) { var mergers = new List <MemberDescriptor>(); merging.Add(otherMember, mergers); } merging[otherMember].Add(targetMember); targetDiff.Disposed = true; } else if (otherClass.Name != targetClass.Name) { if (targetDiff.Field != targetMember.DescriptorType) { continue; } if (otherDiff.Field != otherMember.DescriptorType) { continue; } // Maybe they're just moving? moveMember(ref diffs, targetMember, otherMember); } } } } // Process the diffs and try grouping any // that use the same member descriptor. foreach (MemberDescriptor member in merging.Keys) { List <MemberDescriptor> members = merging[member]; if (members.Count > 1) { Diff mergeDiff = new Diff(); mergeDiff.Type = DiffType.Merge; var mergeInto = new DiffChangeList(); mergeInto.Add(member); mergeDiff.To = mergeInto; var mergeFrom = new DiffChangeList(); mergeFrom.AddRange(members); mergeDiff.From = mergeFrom; mergeDiff.Target = member; diffs.Add(mergeDiff); } else { var target = members.First(); moveMember(ref diffs, target, member); var newClassDiff = diffs .Where(diff => diff.Type == DiffType.Add) .Where(diff => diff.Target == member.Class) .FirstOrDefault(); if (newClassDiff == null) { continue; } var newMemberDiff = diffs .SelectMany(diff => diff.Children) .Where(diff => diff.Target == member) .FirstOrDefault(); if (newMemberDiff == null) { continue; } newClassDiff.RemoveChild(newMemberDiff); } } }
public static async Task <string> CompareDatabases(ReflectionDatabase oldApi, ReflectionDatabase newApi, string format = "TXT", bool postProcess = true) { currentFormat = format.ToLower(); // For the purposes of the differ, treat png like html. // Its assumed that the result will be processed afterwards. if (currentFormat == "png") { currentFormat = "html"; } // Clean up old results. if (results.Count > 0) { results.Clear(); } // Grab the class lists. var oldClasses = oldApi.Classes; var newClasses = newApi.Classes; // Record classes that were added. foreach (string className in newClasses.Keys) { if (!oldClasses.ContainsKey(className)) { ClassDescriptor classDesc = newClasses[className]; flagEntireClass(classDesc, Added, true); } } // Record classes that were removed. foreach (string className in oldClasses.Keys) { if (!newClasses.ContainsKey(className)) { ClassDescriptor classDesc = oldClasses[className]; flagEntireClass(classDesc, Removed, false); } } // Run pre-member-diff modifier tasks. foreach (IDiffModifier preModifier in preModifiers) { preModifier.RunModifier(ref results); } // Compare class changes. foreach (string className in oldClasses.Keys) { ClassDescriptor oldClass = oldClasses[className]; if (newClasses.ContainsKey(className)) { ClassDescriptor newClass = newClasses[className]; // Capture the members of these classes. var oldMembers = createLookupTable(oldClass.Members); var newMembers = createLookupTable(newClass.Members); // Compare the classes directly. var classTagDiffs = CompareTags(oldClass, oldClass.Tags, newClass.Tags); Compare(oldClass, "superclass", oldClass.Superclass, newClass.Superclass, true); Compare(oldClass, "memory category", oldClass.MemoryCategory, newClass.MemoryCategory, true); // Record members that were added. foreach (string memberName in newMembers.Keys) { if (!oldMembers.ContainsKey(memberName)) { // Add New Member MemberDescriptor newMember = newMembers[memberName]; Added(newMember); } } // Record members that were changed or removed. foreach (string memberName in oldMembers.Keys) { MemberDescriptor oldMember = oldMembers[memberName]; if (newMembers.ContainsKey(memberName)) { MemberDescriptor newMember = newMembers[memberName]; if (oldMember.ThreadSafety.Type != ThreadSafetyType.Unknown) { Compare(newMember, "thread safety", oldMember.ThreadSafety, newMember.ThreadSafety); } // Check if any tags added to this member were also added to its parent class. var memberTagDiffs = CompareTags(newMember, oldMember.Tags, newMember.Tags); MergeTagDiffs(classTagDiffs, memberTagDiffs); // Compare the fields specific to these member types // TODO: I'd like to move these routines into their respective // members, but I'm not sure how to do so in a clean manner. if (newMember is PropertyDescriptor) { var oldProp = oldMember as PropertyDescriptor; var newProp = newMember as PropertyDescriptor; var oldMerged = oldProp.Security.Merged; var newMerged = newProp.Security.Merged; if (oldMerged && newMerged) { // Just compare them as a security change alone. var oldSecurity = oldProp.Security.Value; var newSecurity = newProp.Security.Value; Compare(newMember, "security", oldSecurity, newSecurity); } else { // Compare the read/write permissions individually. var oldSecurity = oldProp.Security; var newSecurity = newProp.Security; string oldRead = oldSecurity.Read.Value, newRead = newSecurity.Read.Value; string oldWrite = oldSecurity.Write.Value, newWrite = newSecurity.Write.Value; Compare(newMember, "read permissions", oldRead, newRead); Compare(newMember, "write permissions", oldWrite, newWrite); } var oldSerial = oldProp.Serialization.Describe(true); var newSerial = newProp.Serialization.Describe(true); Compare(newMember, "serialization", oldSerial, newSerial); Compare(newMember, "value-type", oldProp.ValueType, newProp.ValueType); Compare(newMember, "category", oldProp.Category, newProp.Category, true); } else if (newMember is FunctionDescriptor) { var oldFunc = oldMember as FunctionDescriptor; var newFunc = newMember as FunctionDescriptor; Compare(newMember, "security", oldFunc.Security, newFunc.Security); Compare(newMember, "parameters", oldFunc.Parameters, newFunc.Parameters); Compare(newMember, "return-type", oldFunc.ReturnType, newFunc.ReturnType); } else if (newMember is CallbackDescriptor) { var oldCall = oldMember as CallbackDescriptor; var newCall = newMember as CallbackDescriptor; Compare(newMember, "security", oldCall.Security, newCall.Security); Compare(newMember, "parameters", oldCall.Parameters, newCall.Parameters); Compare(newMember, "expected return-type", oldCall.ReturnType, newCall.ReturnType); } else if (newMember is EventDescriptor) { var oldEvent = oldMember as EventDescriptor; var newEvent = newMember as EventDescriptor; Compare(newMember, "security", oldEvent.Security, newEvent.Security); Compare(newMember, "parameters", oldEvent.Parameters, newEvent.Parameters); } } else { // Remove old member. Removed(oldMember, false); } } } } // Grab the enum lists. var oldEnums = oldApi.Enums; var newEnums = newApi.Enums; // Record enums that were added. foreach (string enumName in newEnums.Keys) { if (!oldEnums.ContainsKey(enumName)) { EnumDescriptor newEnum = newEnums[enumName]; flagEntireEnum(newEnum, Added, true); } } // Record enums that were changed or removed. foreach (string enumName in oldEnums.Keys) { EnumDescriptor oldEnum = oldEnums[enumName]; if (newEnums.ContainsKey(enumName)) { EnumDescriptor newEnum = newEnums[enumName]; var enumTagDiffs = CompareTags(newEnum, oldEnum.Tags, newEnum.Tags); // Grab the enum-item lists. var oldItems = createLookupTable(oldEnum.Items); var newItems = createLookupTable(newEnum.Items); // Record enum-items that were added. foreach (var itemName in newItems.Keys) { if (!oldItems.ContainsKey(itemName)) { EnumItemDescriptor item = newItems[itemName]; Added(item); } } foreach (var itemName in oldItems.Keys) { EnumItemDescriptor oldItem = oldItems[itemName]; if (newItems.ContainsKey(itemName)) { EnumItemDescriptor newItem = newItems[itemName]; Compare(newItem, "value", oldItem.Value, newItem.Value); // Check if any tags that were added to this item were also added to its parent enum. var itemTagDiffs = CompareTags(newItem, oldItem.Tags, newItem.Tags); MergeTagDiffs(enumTagDiffs, itemTagDiffs); } else { // Remove old enum-item. Removed(oldItem, false); } } } else { // Remove old enum. flagEntireEnum(oldEnum, Removed, false); } } // Exit early if no diffs were recorded. if (results.Count == 0) { return(""); } // Select diffs that are not parented to other diffs. List <Diff> diffs = results .Where(diff => !diff.HasParent) .ToList(); // Run post-member-diff modifier tasks. foreach (IDiffModifier postModifier in postModifiers) { postModifier.RunModifier(ref diffs); } // Remove diffs that were disposed during the modifier tasks, diffs = diffs .Where(diff => !diff.Disposed) .OrderBy(diff => diff) .ToList(); // Setup actions for generating the final result, based on the requested format. DiffResultLineAdder addLineToResults; DiffResultFinalizer finalizeResults; List <string> lines = diffs .Select(diff => diff.ToString()) .ToList(); if (currentFormat == "html") { var htmlDumper = new ReflectionDumper(); var diffLookup = diffs.ToDictionary(diff => diff.ToString()); addLineToResults = new DiffResultLineAdder((line, addBreak) => { if (addBreak) { htmlDumper.Write(HTML_BREAK); htmlDumper.NextLine(); } if (diffLookup.ContainsKey(line)) { Diff diff = diffLookup[line]; diff.WriteDiffHtml(htmlDumper); } if (line.EndsWith(NL)) { htmlDumper.Write(HTML_BREAK); htmlDumper.NextLine(); } }); finalizeResults = new DiffResultFinalizer(() => { if (newApi.Branch == "roblox") { htmlDumper.NextLine(); } string result = htmlDumper.ExportResults(); if (postProcess) { result = ApiDumpTool.PostProcessHtml(result); } return(result); }); if (newApi.Branch == "roblox") { var deployLog = await ApiDumpTool.GetLastDeployLog("roblox"); htmlDumper.OpenHtmlTag("h2"); htmlDumper.Write("Version " + deployLog.VersionId); htmlDumper.CloseHtmlTag("h2"); htmlDumper.NextLine(2); } } else { var final = new List <string>(); addLineToResults = new DiffResultLineAdder((line, addBreak) => { if (addBreak) { final.Add(""); } final.Add(line); }); finalizeResults = new DiffResultFinalizer(() => { string[] finalLines = final.ToArray(); return(string.Join(NL, finalLines).Trim()); }); } // Generate the final diff results string prevLead = ""; string lastLine = NL; foreach (string line in lines) { string[] words = line.Split(' '); if (words.Length >= 2) { // Capture the first two words in this line. string first = words[0]; string second = words[1]; if (second.ToLower() == "the" && words.Length > 2) { second = words[2]; } string lead = (first + ' ' + second).Trim(); bool addBreak = false; bool lastLineNoBreak = !lastLine.EndsWith(NL); // If the first two words of this line aren't the same as the last... if (lead != prevLead) { // Add a break if the last line doesn't have a break. // (and if there actually were two previous words) if (prevLead != "" && lastLineNoBreak) { addBreak = true; } prevLead = lead; } // If we didn't add a break, but this line has a break and the // previous line doesn't, then we will add a break. if (!addBreak && lastLineNoBreak && line.EndsWith(NL)) { addBreak = true; } // Handle writing this line depending on the format we're using. addLineToResults(line, addBreak); lastLine = line; } } return(finalizeResults()); }