3 // Sebastien Pouliot <sebastien@xamarin.com>
5 // Copyright 2013-2014 Xamarin Inc. http://www.xamarin.com
7 // Permission is hereby granted, free of charge, to any person obtaining
8 // a copy of this software and associated documentation files (the
9 // "Software"), to deal in the Software without restriction, including
10 // without limitation the rights to use, copy, modify, merge, publish,
11 // distribute, sublicense, and/or sell copies of the Software, and to
12 // permit persons to whom the Software is furnished to do so, subject to
13 // the following conditions:
15 // The above copyright notice and this permission notice shall be
16 // included in all copies or substantial portions of the Software.
18 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28 using System.Collections.Generic;
30 using System.Reflection;
32 using System.Xml.Linq;
34 namespace Xamarin.ApiDiff {
36 public abstract class MemberComparer : Comparer {
38 // true if this is the first element being added or removed in the group being rendered
41 public abstract string GroupName { get; }
42 public abstract string ElementName { get; }
44 protected virtual bool IsBreakingRemoval (XElement e)
49 public void Compare (XElement source, XElement target)
51 var s = source.Element (GroupName);
52 var t = target.Element (GroupName);
53 if (XNode.DeepEquals (s, t))
57 Add (t.Elements (ElementName));
58 } else if (t == null) {
59 Remove (s.Elements (ElementName));
61 Compare (s.Elements (ElementName), t.Elements (ElementName));
65 public override void SetContext (XElement current)
69 string GetContainingType (XElement el)
71 return el.Ancestors ("class").First ().Attribute ("type").Value;
74 bool IsInInterface (XElement el)
76 return GetContainingType (el) == "interface";
79 public XElement Source { get; set; }
81 public virtual bool Find (XElement e)
83 return e.GetAttribute ("name") == Source.GetAttribute ("name");
86 XElement Find (IEnumerable<XElement> target)
88 return State.Lax ? target.FirstOrDefault (Find) : target.SingleOrDefault (Find);
91 public override void Compare (IEnumerable<XElement> source, IEnumerable<XElement> target)
96 foreach (var s in source) {
99 var t = Find (target);
101 // not in target, it was removed
106 if (Equals (s, t, modified))
109 Modified (s, t, modified);
112 // delayed, that way we show "Modified", "Added" and then "Removed"
117 // remaining == newly added in target
121 void Add (IEnumerable<XElement> elements)
124 foreach (var item in elements) {
126 if (State.IgnoreAdded.Any (re => re.IsMatch (GetDescription (item))))
129 BeforeAdding (elements);
138 void Modify (ApiChanges modified)
140 foreach (var changes in modified) {
141 Output.WriteLine ("<p>{0}:</p>", changes.Key);
142 Output.WriteLine ("<pre>");
143 foreach (var element in changes.Value) {
144 Output.Write ("<div {0}>", element.Breaking ? "data-is-breaking" : "data-is-non-breaking");
145 foreach (var line in element.Member.ToString ().Split ('\n'))
146 Output.WriteLine ("\t{0}", line);
147 Output.Write ("</div>");
150 Output.WriteLine ("</pre>");
154 void Remove (IEnumerable<XElement> elements)
157 foreach (var item in elements) {
158 if (State.IgnoreRemoved.Any (re => re.IsMatch (GetDescription (item))))
162 BeforeRemoving (elements);
171 public abstract string GetDescription (XElement e);
173 protected StringBuilder GetObsoleteMessage (XElement e)
175 var sb = new StringBuilder ();
176 string o = e.GetObsoleteMessage ();
178 sb.Append ("[Obsolete");
180 sb.Append (" (\"").Append (o).Append ("\")");
182 for (int i = 0; i < State.Indent + 1; i++)
188 public override bool Equals (XElement source, XElement target, ApiChanges changes)
190 RenderAttributes (source, target, changes);
192 // We don't want to compare attributes.
193 RemoveAttributes (source);
194 RemoveAttributes (target);
196 return base.Equals (source, target, changes);
199 public virtual void BeforeAdding (IEnumerable<XElement> list)
202 Output.WriteLine ("<div>");
203 Output.WriteLine ("<p>Added {0}:</p>", list.Count () > 1 ? GroupName : ElementName);
204 Output.WriteLine ("<pre>");
207 public override void Added (XElement target, bool wasParentAdded)
209 var o = GetObsoleteMessage (target);
210 if (!first && (o.Length > 0))
213 bool isInterfaceBreakingChange = !wasParentAdded && IsInInterface (target);
214 Output.Write ("\t<span class='added added-{0} {1}' {2}>", ElementName, isInterfaceBreakingChange ? "breaking" : string.Empty, isInterfaceBreakingChange ? "data-is-breaking" : "data-is-non-breaking");
215 Output.Write ("{0}{1}", o, GetDescription (target));
216 Output.WriteLine ("</span>");
220 public virtual void AfterAdding ()
222 Output.WriteLine ("</pre>");
223 Output.WriteLine ("</div>");
226 public override void Modified (XElement source, XElement target, ApiChanges change)
230 public virtual void BeforeRemoving (IEnumerable<XElement> list)
233 Output.WriteLine ("<p>Removed {0}:</p>\n", list.Count () > 1 ? GroupName : ElementName);
234 Output.WriteLine ("<pre>");
237 public override void Removed (XElement source)
239 var o = GetObsoleteMessage (source);
240 if (!first && (o.Length > 0))
243 bool is_breaking = IsBreakingRemoval (source);
246 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);
247 Output.Write ("{0}{1}", o, GetDescription (source));
248 Output.WriteLine ("</span>");
252 public virtual void AfterRemoving ()
254 Output.WriteLine ("</pre>");;
257 string RenderGenericParameter (XElement gp)
259 var sb = new StringBuilder ();
260 sb.Append (gp.GetTypeName ("name"));
262 var constraints = gp.DescendantList ("generic-parameter-constraints", "generic-parameter-constraint");
263 if (constraints != null && constraints.Count > 0) {
265 for (int i = 0; i < constraints.Count; i++) {
268 sb.Append (constraints [i].GetTypeName ("name"));
271 return sb.ToString ();
274 protected void RenderGenericParameters (XElement source, XElement target, ApiChange change)
276 var src = source.DescendantList ("generic-parameters", "generic-parameter");
277 var tgt = target.DescendantList ("generic-parameters", "generic-parameter");
278 var srcCount = src == null ? 0 : src.Count;
279 var tgtCount = tgt == null ? 0 : tgt.Count;
281 if (srcCount == 0 && tgtCount == 0)
284 change.Append ("<");
285 for (int i = 0; i < Math.Max (srcCount, tgtCount); i++) {
287 change.Append (", ");
289 change.AppendAdded (RenderGenericParameter (tgt [i]), true);
290 } else if (i >= tgtCount) {
291 change.AppendRemoved (RenderGenericParameter (src [i]), true);
293 var srcName = RenderGenericParameter (src [i]);
294 var tgtName = RenderGenericParameter (tgt [i]);
296 if (srcName != tgtName) {
297 change.AppendModified (srcName, tgtName, true);
299 change.Append (srcName);
303 change.Append (">");
306 protected string FormatValue (string type, string value)
311 if (type == "string")
312 return "\"" + value + "\"";
313 else if (type == "bool") {
327 protected void RenderParameters (XElement source, XElement target, ApiChange change)
329 var src = source.DescendantList ("parameters", "parameter");
330 var tgt = target.DescendantList ("parameters", "parameter");
331 var srcCount = src == null ? 0 : src.Count;
332 var tgtCount = tgt == null ? 0 : tgt.Count;
334 change.Append (" (");
335 for (int i = 0; i < Math.Max (srcCount, tgtCount); i++) {
337 change.Append (", ");
340 change.AppendAdded (tgt [i].GetTypeName ("type") + " " + tgt [i].GetAttribute ("name"), true);
341 } else if (i >= tgtCount) {
342 change.AppendRemoved (src [i].GetTypeName ("type") + " " + src [i].GetAttribute ("name"), true);
344 var paramSourceType = src [i].GetTypeName ("type");
345 var paramTargetType = tgt [i].GetTypeName ("type");
347 var paramSourceName = src [i].GetAttribute ("name");
348 var paramTargetName = tgt [i].GetAttribute ("name");
350 if (paramSourceType != paramTargetType) {
351 change.AppendModified (paramSourceType, paramTargetType, true);
353 change.Append (paramSourceType);
356 if (paramSourceName != paramTargetName) {
357 change.AppendModified (paramSourceName, paramTargetName, false);
359 change.Append (paramSourceName);
362 var optSource = src [i].Attribute ("optional");
363 var optTarget = tgt [i].Attribute ("optional");
364 var srcValue = FormatValue (paramSourceType, src [i].GetAttribute ("defaultValue"));
365 var tgtValue = FormatValue (paramTargetType, tgt [i].GetAttribute ("defaultValue"));
367 if (optSource != null) {
368 if (optTarget != null) {
369 change.Append (" = ");
370 if (srcValue != tgtValue) {
371 change.AppendModified (srcValue, tgtValue, false);
373 change.Append (tgtValue);
376 change.AppendRemoved (" = " + srcValue);
379 if (optTarget != null)
380 change.AppendAdded (" = " + tgtValue);
387 // Ignore any parameter name changes if requested.
388 if (State.IgnoreParameterNameChanges && !change.Breaking) {
389 change.AnyChange = false;
390 change.HasIgnoredChanges = true;
394 void RenderVTable (MethodAttributes source, MethodAttributes target, ApiChange change)
396 var srcAbstract = (source & MethodAttributes.Abstract) == MethodAttributes.Abstract;
397 var tgtAbstract = (target & MethodAttributes.Abstract) == MethodAttributes.Abstract;
398 var srcFinal = (source & MethodAttributes.Final) == MethodAttributes.Final;
399 var tgtFinal = (target & MethodAttributes.Final) == MethodAttributes.Final;
400 var srcVirtual = (source & MethodAttributes.Virtual) == MethodAttributes.Virtual;
401 var tgtVirtual = (target & MethodAttributes.Virtual) == MethodAttributes.Virtual;
402 var srcOverride = (source & MethodAttributes.VtableLayoutMask) != MethodAttributes.NewSlot;
403 var tgtOverride = (target & MethodAttributes.VtableLayoutMask) != MethodAttributes.NewSlot;
405 var srcWord = srcVirtual ? (srcOverride ? "override" : "virtual") : string.Empty;
406 var tgtWord = tgtVirtual ? (tgtOverride ? "override" : "virtual") : string.Empty;
407 var breaking = srcWord.Length > 0 && tgtWord.Length == 0;
411 change.Append ("abstract ");
412 } else if (tgtVirtual) {
413 change.AppendModified ("abstract", tgtWord, false).Append (" ");
415 change.AppendRemoved ("abstract").Append (" ");
419 change.AppendAdded ("abstract", true).Append (" ");
420 } else if (srcWord != tgtWord) {
422 change.AppendModified (srcWord, tgtWord, breaking).Append (" ");
423 } else if (tgtWord.Length > 0) {
424 change.Append (tgtWord).Append (" ");
425 } else if (srcWord.Length > 0) {
426 change.AppendRemoved (srcWord, breaking).Append (" ");
432 change.Append ("final ");
434 change.AppendRemoved ("final", false).Append (" "); // removing 'final' is not a breaking change.
437 if (tgtFinal && srcVirtual) {
438 change.AppendModified ("virtual", "final", true).Append (" "); // adding 'final' is a breaking change if the member was virtual
442 if (!srcVirtual && !srcFinal && tgtVirtual && tgtFinal) {
443 // existing member implements a member from a new interface
444 // this would show up as 'virtual final', which is redundant, so show nothing at all.
445 change.HasIgnoredChanges = true;
448 // Ignore non-breaking virtual changes.
449 if (State.IgnoreVirtualChanges && !change.Breaking) {
450 change.AnyChange = false;
451 change.HasIgnoredChanges = true;
454 var tgtSecurity = (source & MethodAttributes.HasSecurity) == MethodAttributes.HasSecurity;
455 var srcSecurity = (target & MethodAttributes.HasSecurity) == MethodAttributes.HasSecurity;
457 if (tgtSecurity != srcSecurity)
458 change.HasIgnoredChanges = true;
460 var srcPInvoke = (source & MethodAttributes.PinvokeImpl) == MethodAttributes.PinvokeImpl;
461 var tgtPInvoke = (target & MethodAttributes.PinvokeImpl) == MethodAttributes.PinvokeImpl;
462 if (srcPInvoke != tgtPInvoke)
463 change.HasIgnoredChanges = true;
466 protected string GetVisibility (MethodAttributes attr)
469 case MethodAttributes.Private:
470 case MethodAttributes.PrivateScope:
472 case MethodAttributes.Assembly:
474 case MethodAttributes.FamANDAssem:
475 return "private internal";
476 case MethodAttributes.FamORAssem:
477 return "protected"; // customers don't care about 'internal';
478 case MethodAttributes.Family:
480 case MethodAttributes.Public:
483 throw new NotImplementedException ();
487 protected void RenderVisibility (MethodAttributes source, MethodAttributes target, ApiChange diff)
489 source = source & MethodAttributes.MemberAccessMask;
490 target = target & MethodAttributes.MemberAccessMask;
492 if (source == target) {
493 diff.Append (GetVisibility (target));
495 var breaking = false;
497 case MethodAttributes.Private:
498 case MethodAttributes.Assembly:
499 case MethodAttributes.FamANDAssem:
500 break; // these are not publicly visible, thus not breaking
501 case MethodAttributes.FamORAssem:
502 case MethodAttributes.Family:
504 case MethodAttributes.Public:
505 // to public is not a breaking change
507 case MethodAttributes.Family:
508 case MethodAttributes.FamORAssem:
509 // not a breaking change, but should still show up in diff
512 // anything else is a breaking change
517 case MethodAttributes.Public:
519 // any change from public is breaking.
524 diff.AppendModified (GetVisibility (source), GetVisibility (target), breaking);
529 protected void RenderStatic (MethodAttributes src, MethodAttributes tgt, ApiChange diff)
531 var srcStatic = (src & MethodAttributes.Static) == MethodAttributes.Static;
532 var tgtStatic = (tgt & MethodAttributes.Static) == MethodAttributes.Static;
534 if (srcStatic != tgtStatic) {
536 diff.AppendRemoved ("static", true).Append (" ");
538 diff.AppendAdded ("static", true).Append (" ");
543 protected void RenderMethodAttributes (MethodAttributes src, MethodAttributes tgt, ApiChange diff)
545 RenderStatic (src, tgt, diff);
546 RenderVisibility (src & MethodAttributes.MemberAccessMask, tgt & MethodAttributes.MemberAccessMask, diff);
547 RenderVTable (src, tgt, diff);
550 protected void RenderMethodAttributes (XElement source, XElement target, ApiChange diff)
552 RenderMethodAttributes (source.GetMethodAttributes (), target.GetMethodAttributes (), diff);
555 protected void RemoveAttributes (XElement element)
557 var srcAttributes = element.Element ("attributes");
558 if (srcAttributes != null)
559 srcAttributes.Remove ();
561 foreach (var el in element.Elements ())
562 RemoveAttributes (el);
565 protected void RenderAttributes (XElement source, XElement target, ApiChanges changes)
567 var srcObsolete = source.GetObsoleteMessage ();
568 var tgtObsolete = target.GetObsoleteMessage ();
570 if (srcObsolete == tgtObsolete)
571 return; // nothing changed
573 if (srcObsolete == null) {
574 if (tgtObsolete == null)
575 return; // neither is obsolete
576 var change = new ApiChange ();
577 change.Header = "Obsoleted " + GroupName;
578 change.Append (string.Format ("<span class='obsolete obsolete-{0}' data-is-non-breaking>", ElementName));
579 change.Append ("[Obsolete (");
580 if (tgtObsolete != string.Empty)
581 change.Append ("\"").Append (tgtObsolete).Append ("\"");
582 change.Append (")]\n");
583 change.Append (GetDescription (target));
584 change.Append ("</span>");
585 change.AnyChange = true;
586 changes.Add (source, target, change);
587 } else if (tgtObsolete == null) {
588 // Made non-obsolete. Do we care to report this?
590 // Obsolete message changed. Do we care to report this?
594 protected void RenderName (XElement source, XElement target, ApiChange change)
596 var name = target.GetAttribute ("name");
597 // show the constructor as it would be defined in C#
598 name = name.Replace (".ctor", State.Type);
600 var p = name.IndexOf ('(');
602 name = name.Substring (0, p);
604 change.Append (name);