Xamarin Public Jenkins (auto-signing) 0abdbe5a7d Imported Upstream version 5.18.0.142
Former-commit-id: 7467d4b717762eeaf652d77f1486dd11ffb1ff1f
2018-10-09 08:20:59 +00:00

599 lines
18 KiB
C#

//
// Authors
// Sebastien Pouliot <sebastien@xamarin.com>
//
// Copyright 2013-2014 Xamarin Inc. http://www.xamarin.com
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml.Linq;
namespace Mono.ApiTools {
abstract class MemberComparer : Comparer {
// true if this is the first element being added or removed in the group being rendered
protected bool first;
public MemberComparer (State state)
: base (state)
{
}
public abstract string GroupName { get; }
public abstract string ElementName { get; }
protected virtual bool IsBreakingRemoval (XElement e)
{
return !e.Elements ("attributes").SelectMany (a => a.Elements ("attribute")).Any (c => c.Attribute ("name")?.Value == "System.ObsoleteAttribute");
}
public void Compare (XElement source, XElement target)
{
var s = source.Element (GroupName);
var t = target.Element (GroupName);
if (XNode.DeepEquals (s, t))
return;
if (s == null) {
Add (t.Elements (ElementName));
} else if (t == null) {
Remove (s.Elements (ElementName));
} else {
Compare (s.Elements (ElementName), t.Elements (ElementName));
}
}
public override void SetContext (XElement current)
{
}
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)
{
return e.GetAttribute ("name") == Source.GetAttribute ("name");
}
XElement Find (IEnumerable<XElement> target)
{
return State.Lax ? target.FirstOrDefault (Find) : target.SingleOrDefault (Find);
}
public override void Compare (IEnumerable<XElement> source, IEnumerable<XElement> target)
{
removed.Clear ();
modified.Clear ();
foreach (var s in source) {
SetContext (s);
Source = s;
var t = Find (target);
if (t == null) {
// not in target, it was removed
removed.Add (s);
} else {
t.Remove ();
// possibly modified
if (Equals (s, t, modified))
continue;
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);
}
void Add (IEnumerable<XElement> elements)
{
bool a = false;
foreach (var item in elements) {
var memberDescription = $"{State.Namespace}.{State.Type}: Added {GroupName}: {GetDescription (item)}";
State.LogDebugMessage ($"Possible -a value: {memberDescription}");
SetContext (item);
if (State.IgnoreAdded.Any (re => re.IsMatch (memberDescription)))
continue;
if (!a) {
Formatter.BeginMemberAddition (Output, elements, this);
a = true;
}
Added (item, false);
}
if (a)
Formatter.EndMemberAddition (Output);
}
void Modify (ApiChanges modified)
{
foreach (var changes in modified) {
if (State.IgnoreNonbreaking && changes.Value.All (c => !c.Breaking))
continue;
Formatter.BeginMemberModification (Output, changes.Key);
foreach (var element in changes.Value) {
if (State.IgnoreNonbreaking && !element.Breaking)
continue;
Formatter.Diff (Output, element);
}
Formatter.EndMemberModification (Output);
}
}
void Remove (IEnumerable<XElement> elements)
{
bool r = false;
foreach (var item in elements) {
var memberDescription = $"{State.Namespace}.{State.Type}: Removed {GroupName}: {GetDescription (item)}";
State.LogDebugMessage ($"Possible -r value: {memberDescription}");
if (State.IgnoreRemoved.Any (re => re.IsMatch (memberDescription)))
continue;
SetContext (item);
if (State.IgnoreNonbreaking && !IsBreakingRemoval (item))
continue;
if (!r) {
Formatter.BeginMemberRemoval (Output, elements, this);
first = true;
r = true;
}
Removed (item);
}
if (r)
Formatter.EndMemberRemoval (Output);
}
public abstract string GetDescription (XElement e);
protected StringBuilder GetObsoleteMessage (XElement e)
{
var sb = new StringBuilder ();
string o = e.GetObsoleteMessage ();
if (o != null) {
sb.Append ("[Obsolete");
if (o.Length > 0)
sb.Append (" (\"").Append (o).Append ("\")");
sb.AppendLine ("]");
}
return sb;
}
public override bool Equals (XElement source, XElement target, ApiChanges changes)
{
RenderAttributes (source, target, changes);
// We don't want to compare attributes.
RemoveAttributes (source);
RemoveAttributes (target);
return base.Equals (source, target, changes);
}
public override void Added (XElement target, bool wasParentAdded)
{
var o = GetObsoleteMessage (target);
if (!first && (o.Length > 0))
Output.WriteLine ();
Indent ();
bool isInterfaceBreakingChange = !wasParentAdded && IsInInterface (target);
Formatter.AddMember (Output, this, isInterfaceBreakingChange, o.ToString (), GetDescription (target));
first = false;
}
public override void Modified (XElement source, XElement target, ApiChanges change)
{
}
public override void Removed (XElement source)
{
var o = GetObsoleteMessage (source);
if (!first && (o.Length > 0))
Output.WriteLine ();
bool is_breaking = IsBreakingRemoval (source);
Formatter.RemoveMember (Output, this, is_breaking, o.ToString (), GetDescription (source));
first = false;
}
string RenderGenericParameter (XElement gp)
{
var sb = new StringBuilder ();
sb.Append (gp.GetTypeName ("name", State));
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", State));
}
}
return sb.ToString ();
}
protected void RenderGenericParameters (XElement source, XElement target, ApiChange change)
{
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 (Formatter.LesserThan);
for (int i = 0; i < System.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 (Formatter.GreaterThan);
}
protected string FormatValue (string type, string value)
{
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 < System.Math.Max (srcCount, tgtCount); i++) {
if (i > 0)
change.Append (", ");
string mods_tgt = tgt [i].GetAttribute ("direction") ?? "";
string mods_src = src [i].GetAttribute ("direction") ?? "";
if (mods_tgt.Length > 0)
mods_tgt = mods_tgt + " ";
if (mods_src.Length > 0)
mods_src = mods_src + " ";
if (i >= srcCount) {
change.AppendAdded (mods_tgt + tgt [i].GetTypeName ("type", State) + " " + tgt [i].GetAttribute ("name"), true);
} else if (i >= tgtCount) {
change.AppendRemoved (mods_src + src [i].GetTypeName ("type", State) + " " + src [i].GetAttribute ("name"), true);
} else {
var paramSourceType = src [i].GetTypeName ("type", State);
var paramTargetType = tgt [i].GetTypeName ("type", State);
var paramSourceName = src [i].GetAttribute ("name");
var paramTargetName = tgt [i].GetAttribute ("name");
if (mods_src != mods_tgt) {
change.AppendModified (mods_src, mods_tgt, true);
} else {
change.Append (mods_src);
}
if (paramSourceType != paramTargetType) {
change.AppendModified (paramSourceType, paramTargetType, true);
} else {
change.Append (paramSourceType);
}
change.Append (" ");
if (!State.IgnoreParameterNameChanges && paramSourceName != paramTargetName) {
change.AppendModified (paramSourceName, paramTargetName, true);
} 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 (")");
}
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) {
if (State.IgnoreVirtualChanges) {
change.HasIgnoredChanges = true;
} else {
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 {
if (srcVirtual && !tgtVirtual && State.IgnoreVirtualChanges) {
change.HasIgnoredChanges = true;
} 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 (GetDescription (source), State);
change.Header = "Obsoleted " + GroupName;
Formatter.RenderObsoleteMessage (change.Member, this, GetDescription (target), tgtObsolete);
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);
}
}
}