[mono-api-html] New interface members are only breaking if the interface existed.
[mono.git] / mcs / tools / corcompare / mono-api-html / MemberComparer.cs
index 0a646c7a2a26f392cacef89ef548c252f4aa28c1..d12d86625c2ea7a0af1b5d4bbb8067bd32ed04e1 100644 (file)
@@ -27,6 +27,7 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Reflection;
 using System.Text;
 using System.Xml.Linq;
 
@@ -40,6 +41,11 @@ namespace Xamarin.ApiDiff {
                public abstract string GroupName { get; }
                public abstract string ElementName { get; }
 
+               protected virtual bool IsBreakingRemoval (XElement e)
+               {
+                       return true;
+               }
+
                public void Compare (XElement source, XElement target)
                {
                        var s = source.Element (GroupName);
@@ -60,6 +66,16 @@ namespace Xamarin.ApiDiff {
                {
                }
 
+               string GetContainingType (XElement el)
+               {
+                       return el.Ancestors ("class").First ().Attribute ("type").Value;
+               }
+
+               bool IsInInterface (XElement el)
+               {
+                       return GetContainingType (el) == "interface";
+               }
+
                public XElement Source { get; set; }
 
                public virtual bool Find (XElement e)
@@ -75,7 +91,7 @@ namespace Xamarin.ApiDiff {
                public override void Compare (IEnumerable<XElement> source, IEnumerable<XElement> target)
                {
                        removed.Clear ();
-                       obsoleted.Clear ();
+                       modified.Clear ();
 
                        foreach (var s in source) {
                                SetContext (s);
@@ -85,40 +101,21 @@ namespace Xamarin.ApiDiff {
                                        // not in target, it was removed
                                        removed.Add (s);
                                } else {
+                                       t.Remove ();
                                        // possibly modified
-                                       if (Equals (s, t)) {
-                                               if (IsNowObsoleted (s, t)) {
-                                                       obsoleted.Add (t);
-                                               }
-                                               t.Remove ();
+                                       if (Equals (s, t, modified))
                                                continue;
-                                       }
 
-                                       // still in target so will be part of Added
-                                       removed.Add (s);
-                                       Modified (s, t);
+                                       Modified (s, t, modified);
                                }
                        }
                        // delayed, that way we show "Modified", "Added" and then "Removed"
                        Remove (removed);
 
+                       Modify (modified);
+
                        // remaining == newly added in target
                        Add (target);
-
-                       // obsolete (considering as added, they were not obsolete before, wrt regex)
-                       bool o = false;
-                       foreach (var item in obsoleted) {
-                               SetContext (item);
-                               if (State.IgnoreAdded.Any (re => re.IsMatch (GetDescription (item))))
-                                       continue;
-                               if (!o) {
-                                       BeforeObsoleting (obsoleted);
-                                       o = true;
-                               }
-                               Obsoleted (item);
-                       }
-                       if (o)
-                               AfterObsoleting ();
                }
 
                void Add (IEnumerable<XElement> elements)
@@ -132,12 +129,28 @@ namespace Xamarin.ApiDiff {
                                        BeforeAdding (elements);
                                        a = true;
                                }
-                               Added (item);
+                               Added (item, false);
                        }
                        if (a)
                                AfterAdding ();
                }
 
+               void Modify (ApiChanges modified)
+               {
+                       foreach (var changes in modified) {
+                               Output.WriteLine ("<p>{0}:</p>", changes.Key);
+                               Output.WriteLine ("<pre>");
+                               foreach (var element in changes.Value) {
+                                       Output.Write ("<div {0}>", element.Breaking ? "data-is-breaking" : "data-is-non-breaking");
+                                       foreach (var line in element.Member.ToString ().Split ('\n'))
+                                               Output.WriteLine ("\t{0}", line);
+                                       Output.Write ("</div>");
+
+                               }
+                               Output.WriteLine ("</pre>");
+                       }
+               }
+
                void Remove (IEnumerable<XElement> elements)
                {
                        bool r = false;
@@ -154,7 +167,7 @@ namespace Xamarin.ApiDiff {
                        if (r)
                                AfterRemoving ();
                }
-
+                       
                public abstract string GetDescription (XElement e);
 
                protected StringBuilder GetObsoleteMessage (XElement e)
@@ -172,79 +185,424 @@ namespace Xamarin.ApiDiff {
                        return sb;
                }
 
-               public override bool Equals (XElement source, XElement target)
+               public override bool Equals (XElement source, XElement target, ApiChanges changes)
                {
-                       if (base.Equals (source, target))
-                               return true;
+                       RenderAttributes (source, target, changes);
 
-                       return GetDescription (source) == GetDescription (target);
-               }
+                       // We don't want to compare attributes.
+                       RemoveAttributes (source);
+                       RemoveAttributes (target);
 
-               bool IsNowObsoleted (XElement source, XElement target)
-               {
-                       var s = GetObsoleteMessage (source).ToString ();
-                       var t = GetObsoleteMessage (target).ToString ();
-                       // true if it was no [Obsolete] in the source but now is [Obsolete] in the target
-                       return (s.Length == 0 && t.Length > 0);
+                       return base.Equals (source, target, changes);
                }
 
                public virtual void BeforeAdding (IEnumerable<XElement> list)
                {
                        first = true;
-                       Output.WriteLine ("<p>Added {0}:</p><pre>", list.Count () > 1 ? GroupName : ElementName);
+                       Output.WriteLine ("<div>");
+                       Output.WriteLine ("<p>Added {0}:</p>", list.Count () > 1 ? GroupName : ElementName);
+                       Output.WriteLine ("<pre>");
                }
 
-               public override void Added (XElement target)
+               public override void Added (XElement target, bool wasParentAdded)
                {
                        var o = GetObsoleteMessage (target);
                        if (!first && (o.Length > 0))
                                Output.WriteLine ();
-                       Indent ().WriteLine ("\t{0}{1}", o, GetDescription (target));
+                       Indent ();
+                       bool isInterfaceBreakingChange = !wasParentAdded && IsInInterface (target);
+                       Output.Write ("\t<span class='added added-{0} {1}' {2}>", ElementName, isInterfaceBreakingChange ? "breaking" : string.Empty, isInterfaceBreakingChange ? "data-is-breaking" : "data-is-non-breaking");
+                       Output.Write ("{0}{1}", o, GetDescription (target));
+                       Output.WriteLine ("</span>");
                        first = false;
                }
 
                public virtual void AfterAdding ()
                {
                        Output.WriteLine ("</pre>");
+                       Output.WriteLine ("</div>");
                }
 
-               public virtual void BeforeObsoleting (IEnumerable<XElement> list)
+               public override void Modified (XElement source, XElement target, ApiChanges change)
                {
-                       Output.WriteLine ("<p>Obsoleted {0}:</p><pre>", list.Count () > 1 ? GroupName : ElementName);
                }
 
-               public void Obsoleted (XElement target)
+               public virtual void BeforeRemoving (IEnumerable<XElement> list)
                {
-                       Indent ().WriteLine ("\t{0}{1}{2}", GetObsoleteMessage (target), GetDescription (target), Environment.NewLine);
+                       first = true;
+                       Output.WriteLine ("<p>Removed {0}:</p>\n", list.Count () > 1 ? GroupName : ElementName);
+                       Output.WriteLine ("<pre>");
                }
 
-               public virtual void AfterObsoleting ()
+               public override void Removed (XElement source)
                {
-                       Output.WriteLine ("</pre>");
+                       var o = GetObsoleteMessage (source);
+                       if (!first && (o.Length > 0))
+                               Output.WriteLine ();
+
+                       bool is_breaking = IsBreakingRemoval (source);
+
+                       Indent ();
+                       Output.Write ("\t<span class='removed removed-{0} {2}' {1}>", ElementName, is_breaking ? "data-is-breaking" : "data-is-non-breaking", is_breaking ? "breaking" : string.Empty);
+                       Output.Write ("{0}{1}", o, GetDescription (source));
+                       Output.WriteLine ("</span>");
+                       first = false;
                }
 
-               public override void Modified (XElement source, XElement target)
+               public virtual void AfterRemoving ()
                {
+                       Output.WriteLine ("</pre>");;
                }
 
-               public virtual void BeforeRemoving (IEnumerable<XElement> list)
+               string RenderGenericParameter (XElement gp)
                {
-                       first = true;
-                       Output.WriteLine ("<p>Removed {0}:</p><pre>", list.Count () > 1 ? GroupName : ElementName);
+                       var sb = new StringBuilder ();
+                       sb.Append (gp.GetTypeName ("name"));
+
+                       var constraints = gp.DescendantList ("generic-parameter-constraints", "generic-parameter-constraint");
+                       if (constraints != null && constraints.Count > 0) {
+                               sb.Append (" : ");
+                               for (int i = 0; i < constraints.Count; i++) {
+                                       if (i > 0)
+                                               sb.Append (", ");
+                                       sb.Append (constraints [i].GetTypeName ("name"));
+                               }
+                       }
+                       return sb.ToString ();
                }
 
-               public override void Removed (XElement source)
+               protected void RenderGenericParameters (XElement source, XElement target, ApiChange change)
                {
-                       var o = GetObsoleteMessage (source);
-                       if (!first && (o.Length > 0))
-                               Output.WriteLine ();
-                       Indent ().WriteLine ("\t{0}{1}", o, GetDescription (source));
-                       first = false;
+                       var src = source.DescendantList ("generic-parameters", "generic-parameter");
+                       var tgt = target.DescendantList ("generic-parameters", "generic-parameter");
+                       var srcCount = src == null ? 0 : src.Count;
+                       var tgtCount = tgt == null ? 0 : tgt.Count;
+
+                       if (srcCount == 0 && tgtCount == 0)
+                               return;
+
+                       change.Append ("&lt;");
+                       for (int i = 0; i < Math.Max (srcCount, tgtCount); i++) {
+                               if (i > 0)
+                                       change.Append (", ");
+                               if (i >= srcCount) {
+                                       change.AppendAdded (RenderGenericParameter (tgt [i]), true);
+                               } else if (i >= tgtCount) {
+                                       change.AppendRemoved (RenderGenericParameter (src [i]), true);
+                               } else {
+                                       var srcName = RenderGenericParameter (src [i]);
+                                       var tgtName = RenderGenericParameter (tgt [i]);
+
+                                       if (srcName != tgtName) {
+                                               change.AppendModified (srcName, tgtName, true);
+                                       } else {
+                                               change.Append (srcName);
+                                       }
+                                       }
+                               }
+                       change.Append ("&gt;");
                }
 
-               public virtual void AfterRemoving ()
+               protected string FormatValue (string type, string value)
                {
-                       Output.WriteLine ("</pre>");
+                       if (value == null)
+                               return "null";
+
+                       if (type == "string")
+                               return "\"" + value + "\"";
+                       else if (type == "bool") {
+                               switch (value) {
+                               case "True":
+                                       return "true";
+                               case "False":
+                                       return "false";
+                               default:
+                                       return value;
+                               }
+                       }
+
+                       return value;
+               }
+
+               protected void RenderParameters (XElement source, XElement target, ApiChange change)
+               {
+                       var src = source.DescendantList ("parameters", "parameter");
+                       var tgt = target.DescendantList ("parameters", "parameter");
+                       var srcCount = src == null ? 0 : src.Count;
+                       var tgtCount = tgt == null ? 0 : tgt.Count;
+
+                       change.Append (" (");
+                       for (int i = 0; i < Math.Max (srcCount, tgtCount); i++) {
+                               if (i > 0)
+                                       change.Append (", ");
+
+                               if (i >= srcCount) {
+                                       change.AppendAdded (tgt [i].GetTypeName ("type") + " " + tgt [i].GetAttribute ("name"), true);
+                               } else if (i >= tgtCount) {
+                                       change.AppendRemoved (src [i].GetTypeName ("type") + " " + src [i].GetAttribute ("name"), true);
+                               } else {
+                                       var paramSourceType = src [i].GetTypeName ("type");
+                                       var paramTargetType = tgt [i].GetTypeName ("type");
+
+                                       var paramSourceName = src [i].GetAttribute ("name");
+                                       var paramTargetName = tgt [i].GetAttribute ("name");
+
+                                       if (paramSourceType != paramTargetType) {
+                                               change.AppendModified (paramSourceType, paramTargetType, true);
+                                       } else {
+                                               change.Append (paramSourceType);
+                                       }
+                                       change.Append (" ");
+                                       if (paramSourceName != paramTargetName) {
+                                               change.AppendModified (paramSourceName, paramTargetName, false);
+                                       } else {
+                                               change.Append (paramSourceName);
+                                       }
+
+                                       var optSource = src [i].Attribute ("optional");
+                                       var optTarget = tgt [i].Attribute ("optional");
+                                       var srcValue = FormatValue (paramSourceType, src [i].GetAttribute ("defaultValue"));
+                                       var tgtValue = FormatValue (paramTargetType, tgt [i].GetAttribute ("defaultValue"));
+
+                                       if (optSource != null) {
+                                               if (optTarget != null) {
+                                                       change.Append (" = ");
+                                                       if (srcValue != tgtValue) {
+                                                               change.AppendModified (srcValue, tgtValue, false);
+                                                       } else {
+                                                               change.Append (tgtValue);
+                                                       }
+                                               } else {
+                                                       change.AppendRemoved (" = " + srcValue);
+                                               }
+                                       } else {
+                                               if (optTarget != null)
+                                                       change.AppendAdded (" = " + tgtValue);
+                                       }
+                               }
+                       }
+
+                       change.Append (")");
+
+                       // Ignore any parameter name changes if requested.
+                       if (State.IgnoreParameterNameChanges && !change.Breaking) {
+                               change.AnyChange = false;
+                               change.HasIgnoredChanges = true;
+                       }
+               }
+
+               void RenderVTable (MethodAttributes source, MethodAttributes target, ApiChange change)
+               {
+                       var srcAbstract = (source & MethodAttributes.Abstract) == MethodAttributes.Abstract;
+                       var tgtAbstract = (target & MethodAttributes.Abstract) == MethodAttributes.Abstract;
+                       var srcFinal = (source & MethodAttributes.Final) == MethodAttributes.Final;
+                       var tgtFinal = (target & MethodAttributes.Final) == MethodAttributes.Final;
+                       var srcVirtual = (source & MethodAttributes.Virtual) == MethodAttributes.Virtual;
+                       var tgtVirtual = (target & MethodAttributes.Virtual) == MethodAttributes.Virtual;
+                       var srcOverride = (source & MethodAttributes.VtableLayoutMask) != MethodAttributes.NewSlot;
+                       var tgtOverride = (target & MethodAttributes.VtableLayoutMask) != MethodAttributes.NewSlot;
+
+                       var srcWord = srcVirtual ? (srcOverride ? "override" : "virtual") : string.Empty;
+                       var tgtWord = tgtVirtual ? (tgtOverride ? "override" : "virtual") : string.Empty;
+                       var breaking = srcWord.Length > 0 && tgtWord.Length == 0;
+
+                       if (srcAbstract) {
+                               if (tgtAbstract) {
+                                       change.Append ("abstract ");
+                               } else if (tgtVirtual) {
+                                       change.AppendModified ("abstract", tgtWord, false).Append (" ");
+                               } else {
+                                       change.AppendRemoved ("abstract").Append (" ");
+                               }
+                       } else {
+                               if (tgtAbstract) {
+                                       change.AppendAdded ("abstract", true).Append (" ");
+                               } else if (srcWord != tgtWord) {
+                                       if (!tgtFinal)
+                                               change.AppendModified (srcWord, tgtWord, breaking).Append (" ");
+                               } else if (tgtWord.Length > 0) {
+                                       change.Append (tgtWord).Append (" ");
+                               } else if (srcWord.Length > 0) {
+                                       change.AppendRemoved (srcWord, breaking).Append (" ");
+                               }
+                       }
+
+                       if (srcFinal) {
+                               if (tgtFinal) {
+                                       change.Append ("final ");
+                               } else {
+                                       change.AppendRemoved ("final", false).Append (" "); // removing 'final' is not a breaking change.
+                               }
+                       } else {
+                               if (tgtFinal && srcVirtual) {
+                                       change.AppendModified ("virtual", "final", true).Append (" "); // adding 'final' is a breaking change if the member was virtual
+                               }
+                       }
+
+                       if (!srcVirtual && !srcFinal && tgtVirtual && tgtFinal) {
+                               // existing member implements a member from a new interface
+                               // this would show up as 'virtual final', which is redundant, so show nothing at all.
+                               change.HasIgnoredChanges = true;
+                       }
+
+                       // Ignore non-breaking virtual changes.
+                       if (State.IgnoreVirtualChanges && !change.Breaking) {
+                               change.AnyChange = false;
+                               change.HasIgnoredChanges = true;
+                       }
+
+                       var tgtSecurity = (source & MethodAttributes.HasSecurity) == MethodAttributes.HasSecurity;
+                       var srcSecurity = (target & MethodAttributes.HasSecurity) == MethodAttributes.HasSecurity;
+
+                       if (tgtSecurity != srcSecurity)
+                               change.HasIgnoredChanges = true;
+
+                       var srcPInvoke = (source & MethodAttributes.PinvokeImpl) == MethodAttributes.PinvokeImpl;
+                       var tgtPInvoke = (target & MethodAttributes.PinvokeImpl) == MethodAttributes.PinvokeImpl;
+                       if (srcPInvoke != tgtPInvoke)
+                               change.HasIgnoredChanges = true;
+               }
+
+               protected string GetVisibility (MethodAttributes attr)
+               {
+                       switch (attr) {
+                       case MethodAttributes.Private:
+                       case MethodAttributes.PrivateScope:
+                               return "private";
+                       case MethodAttributes.Assembly:
+                               return "internal";
+                       case MethodAttributes.FamANDAssem:
+                               return "private internal";
+                       case MethodAttributes.FamORAssem:
+                               return "protected"; // customers don't care about 'internal';
+                       case MethodAttributes.Family:
+                               return "protected";
+                       case MethodAttributes.Public:
+                               return "public";
+                       default:
+                               throw new NotImplementedException ();
+                       }
+               }
+
+               protected void RenderVisibility (MethodAttributes source, MethodAttributes target, ApiChange diff)
+               {
+                       source = source & MethodAttributes.MemberAccessMask;
+                       target = target & MethodAttributes.MemberAccessMask;
+
+                       if (source == target) {
+                               diff.Append (GetVisibility (target));
+                       } else {
+                               var breaking = false;
+                               switch (source) {
+                               case MethodAttributes.Private:
+                               case MethodAttributes.Assembly:
+                               case MethodAttributes.FamANDAssem:
+                                       break; // these are not publicly visible, thus not breaking
+                               case MethodAttributes.FamORAssem:
+                               case MethodAttributes.Family:
+                                       switch (target) {
+                                       case MethodAttributes.Public:
+                                               // to public is not a breaking change
+                                               break;
+                                       case MethodAttributes.Family:
+                                       case MethodAttributes.FamORAssem:
+                                               // not a breaking change, but should still show up in diff
+                                               break;
+                                       default:
+                                               // anything else is a breaking change
+                                               breaking = true;
+                                               break;
+                                       }
+                                       break;
+                               case MethodAttributes.Public:
+                               default:
+                                       // any change from public is breaking.
+                                       breaking = true;
+                                       break;
+                               }
+
+                               diff.AppendModified (GetVisibility (source), GetVisibility (target), breaking);
+                       }
+                       diff.Append (" ");
+               }
+
+               protected void RenderStatic (MethodAttributes src, MethodAttributes tgt, ApiChange diff)
+               {
+                       var srcStatic = (src & MethodAttributes.Static) == MethodAttributes.Static;
+                       var tgtStatic = (tgt & MethodAttributes.Static) == MethodAttributes.Static;
+
+                       if (srcStatic != tgtStatic) {
+                               if (srcStatic) {
+                                       diff.AppendRemoved ("static", true).Append (" ");
+                               } else {
+                                       diff.AppendAdded ("static", true).Append (" ");
+                               }
+                       }
                }
+
+               protected void RenderMethodAttributes (MethodAttributes src, MethodAttributes tgt, ApiChange diff)
+               {
+                       RenderStatic (src, tgt, diff);
+                       RenderVisibility (src & MethodAttributes.MemberAccessMask, tgt & MethodAttributes.MemberAccessMask, diff);
+                       RenderVTable (src, tgt, diff);
+               }
+
+               protected void RenderMethodAttributes (XElement source, XElement target, ApiChange diff)
+               {
+                       RenderMethodAttributes (source.GetMethodAttributes (), target.GetMethodAttributes (), diff);
+               }
+
+               protected void RemoveAttributes (XElement element)
+               {
+                       var srcAttributes = element.Element ("attributes");
+                       if (srcAttributes != null)
+                               srcAttributes.Remove ();
+
+                       foreach (var el in element.Elements ())
+                               RemoveAttributes (el);
+               }
+
+               protected void RenderAttributes (XElement source, XElement target, ApiChanges changes)
+               {
+                       var srcObsolete = source.GetObsoleteMessage ();
+                       var tgtObsolete = target.GetObsoleteMessage ();
+
+                       if (srcObsolete == tgtObsolete)
+                               return; // nothing changed
+
+                       if (srcObsolete == null) {
+                               if (tgtObsolete == null)
+                                       return; // neither is obsolete
+                               var change = new ApiChange ();
+                               change.Header = "Obsoleted " + GroupName;
+                               change.Append (string.Format ("<span class='obsolete obsolete-{0}' data-is-non-breaking>", ElementName));
+                               change.Append ("[Obsolete (");
+                               if (tgtObsolete != string.Empty)
+                                       change.Append ("\"").Append (tgtObsolete).Append ("\"");
+                               change.Append (")]\n");
+                               change.Append (GetDescription (target));
+                               change.Append ("</span>");
+                               change.AnyChange = true;
+                               changes.Add (source, target, change);
+                       } else if (tgtObsolete == null) {
+                               // Made non-obsolete. Do we care to report this?
+                       } else {
+                               // Obsolete message changed. Do we care to report this?
+                       }
+               }
+
+               protected void RenderName (XElement source, XElement target, ApiChange change)
+               {
+                       var name = target.GetAttribute ("name");
+                       // show the constructor as it would be defined in C#
+                       name = name.Replace (".ctor", State.Type);
+
+                       var p = name.IndexOf ('(');
+                       if (p >= 0)
+                               name = name.Substring (0, p);
+
+                       change.Append (name);
+               }
+
        }
 }