1 //
2 // The main differences with mono-api-diff are:
3 // * this tool directly produce HTML similar to gdiff.sh used for Xamarin.iOS
4 // * this tool reports changes in an "evolutionary" way, not in a breaking way,
5 //   i.e. it does not assume the source assembly is right (but simply older)
6 // * the diff .xml output was not easy to convert back into the HTML format
7 //   that gdiff.sh produced
8 //
9 // Authors
10 //    Sebastien Pouliot  <sebastien@xamarin.com>
11 //
12 // Copyright 2013-2014 Xamarin Inc. http://www.xamarin.com
13 //
14 // Permission is hereby granted, free of charge, to any person obtaining
15 // a copy of this software and associated documentation files (the
16 // "Software"), to deal in the Software without restriction, including
17 // without limitation the rights to use, copy, modify, merge, publish,
18 // distribute, sublicense, and/or sell copies of the Software, and to
19 // permit persons to whom the Software is furnished to do so, subject to
20 // the following conditions:
21 //
22 // The above copyright notice and this permission notice shall be
23 // included in all copies or substantial portions of the Software.
24 //
25 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
26 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
27 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
28 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
29 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
30 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
31 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32 //
33 
34 using System;
35 using System.IO;
36 using System.Collections.Generic;
37 using System.Text.RegularExpressions;
38 
39 using Mono.Options;
40 
41 namespace Xamarin.ApiDiff {
42 
43 	public static class State {
44 		static TextWriter output;
45 
46 		public static TextWriter Output {
47 			get {
48 				if (output == null)
49 					output = Console.Out;
50 				return output;
51 			}
52 			set { output = value; }
53 		}
54 
55 		public static string Assembly { get; set; }
56 		public static string Namespace { get; set; }
57 		public static string Type { get; set; }
58 		public static string BaseType { get; set; }
59 
60 		public static int Indent { get; set; }
61 
62 		static List<Regex> ignoreAdded = new List<Regex> ();
63 		public static List<Regex> IgnoreAdded {
64 			get { return ignoreAdded; }
65 		}
66 
67 		static List<Regex> ignoreNew = new List<Regex> ();
68 		public static List<Regex> IgnoreNew {
69 			get { return ignoreNew; }
70 		}
71 
72 		static List<Regex> ignoreRemoved = new List<Regex> ();
73 		public static List<Regex> IgnoreRemoved {
74 			get { return ignoreRemoved; }
75 		}
76 
77 		public  static  bool    IgnoreParameterNameChanges  { get; set; }
78 		public  static  bool    IgnoreVirtualChanges        { get; set; }
79 		public  static  bool    IgnoreAddedPropertySetters  { get; set; }
80 
81 		public static bool IgnoreNonbreaking { get; set; }
82 
83 		public static bool Lax;
84 		public static bool Colorize = true;
85 
86 		public static int Verbosity;
87 
LogDebugMessage(string value)88 		public static void LogDebugMessage (string value)
89 		{
90 			if (Verbosity == 0)
91 				return;
92 			Console.WriteLine (value);
93 		}
94 	}
95 	class Program {
96 
Main(string[] args)97 		public static int Main (string[] args)
98 		{
99 			var showHelp = false;
100 			string diff = null;
101 			List<string> extra = null;
102 
103 			var options = new OptionSet {
104 				{ "h|help", "Show this help", v => showHelp = true },
105 				{ "d|diff=", "HTML diff file out output (omit for stdout)", v => diff = v },
106 				{ "i|ignore=", "Ignore new, added, and removed members whose description matches a given C# regular expression (see below).",
107 					v => {
108 						var r = new Regex (v);
109 						State.IgnoreAdded.Add (r);
110 						State.IgnoreRemoved.Add (r);
111 						State.IgnoreNew.Add (r);
112 					}
113 				},
114 				{ "a|ignore-added=", "Ignore added members whose description matches a given C# regular expression (see below).",
115 					v => State.IgnoreAdded.Add (new Regex (v))
116 				},
117 				{ "r|ignore-removed=", "Ignore removed members whose description matches a given C# regular expression (see below).",
118 					v => State.IgnoreRemoved.Add (new Regex (v))
119 				},
120 				{ "n|ignore-new=", "Ignore new namespaces and types whose description matches a given C# regular expression (see below).",
121 					v => State.IgnoreNew.Add (new Regex (v))
122 				},
123 				{ "ignore-changes-parameter-names", "Ignore changes to parameter names for identically prototyped methods.",
124 					v => State.IgnoreParameterNameChanges   = v != null
125 				},
126 				{ "ignore-changes-property-setters", "Ignore adding setters to properties.",
127 					v => State.IgnoreAddedPropertySetters = v != null
128 				},
129 				{ "ignore-changes-virtual", "Ignore changing non-`virtual` to `virtual` or adding `override`.",
130 					v => State.IgnoreVirtualChanges = v != null
131 				},
132 				{ "c|colorize:", "Colorize HTML output", v => State.Colorize = string.IsNullOrEmpty (v) ? true : bool.Parse (v) },
133 				{ "x|lax", "Ignore duplicate XML entries", v => State.Lax = true },
134 				{ "ignore-nonbreaking", "Ignore all nonbreaking changes", v => State.IgnoreNonbreaking = true },
135 				{ "v|verbose:", "Verbosity level; when set, will print debug messages",
136 				  (int? v) => State.Verbosity = v ?? (State.Verbosity + 1)},
137 				new ResponseFileSource (),
138 			};
139 
140 			try {
141 				extra = options.Parse (args);
142 			} catch (OptionException e) {
143 				Console.WriteLine ("Option error: {0}", e.Message);
144 				showHelp = true;
145 			}
146 
147 			if (State.IgnoreNonbreaking) {
148 				State.IgnoreAddedPropertySetters = true;
149 				State.IgnoreVirtualChanges = true;
150 				State.IgnoreNew.Add (new Regex (".*"));
151 				State.IgnoreAdded.Add (new Regex (".*"));
152 			}
153 
154 			if (showHelp || extra == null || extra.Count < 2 || extra.Count > 3) {
155 				Console.WriteLine (@"Usage: mono-api-html [options] <reference.xml> <assembly.xml> [diff.html]");
156 				Console.WriteLine ();
157 				Console.WriteLine ("Available options:");
158 				options.WriteOptionDescriptions (Console.Out);
159 				Console.WriteLine ();
160 				Console.WriteLine ("Ignoring Members:");
161 				Console.WriteLine ();
162 				Console.WriteLine ("  Members that were added can be filtered out of the diff by using the");
163 				Console.WriteLine ("  -i, --ignore-added option. The option takes a C# regular expression");
164 				Console.WriteLine ("  to match against member descriptions. For example, to ignore the");
165 				Console.WriteLine ("  introduction of the interfaces 'INSCopying' and 'INSCoding' on types");
166 				Console.WriteLine ("  pass the following to mono-api-html:");
167 				Console.WriteLine ();
168 				Console.WriteLine ("    mono-api-html ... -i 'INSCopying$' -i 'INSCoding$'");
169 				Console.WriteLine ();
170 				Console.WriteLine ("  The regular expressions will match any member description ending with");
171 				Console.WriteLine ("  'INSCopying' or 'INSCoding'.");
172 				Console.WriteLine ();
173 				return 1;
174 			}
175 
176 			var input = extra [0];
177 			var output = extra [1];
178 			if (extra.Count == 3 && diff == null)
179 				diff = extra [2];
180 
181 			try {
182 				var ac = new AssemblyComparer (input, output);
183 				if (diff != null) {
184 					string diffHtml = String.Empty;
185 					using (var writer = new StringWriter ()) {
186 						State.Output = writer;
187 						ac.Compare ();
188 						diffHtml = State.Output.ToString ();
189 					}
190 					if (diffHtml.Length > 0) {
191 						using (var file = new StreamWriter (diff)) {
192 							file.WriteLine ("<div>");
193 							if (State.Colorize) {
194 								file.WriteLine ("<style scoped>");
195 								file.WriteLine ("\t.obsolete { color: gray; }");
196 								file.WriteLine ("\t.added { color: green; }");
197 								file.WriteLine ("\t.removed-inline { text-decoration: line-through; }");
198 								file.WriteLine ("\t.removed-breaking-inline { color: red;}");
199 								file.WriteLine ("\t.added-breaking-inline { text-decoration: underline; }");
200 								file.WriteLine ("\t.nonbreaking { color: black; }");
201 								file.WriteLine ("\t.breaking { color: red; }");
202 								file.WriteLine ("</style>");
203 							}
204 							file.WriteLine (
205 @"<script type=""text/javascript"">
206 	// Only some elements have 'data-is-[non-]breaking' attributes. Here we
207 	// iterate over all descendents elements, and set 'data-is-[non-]breaking'
208 	// depending on whether there are any descendents with that attribute.
209 	function propagateDataAttribute (element)
210 	{
211 		if (element.hasAttribute ('data-is-propagated'))
212 			return;
213 
214 		var i;
215 		var any_breaking = element.hasAttribute ('data-is-breaking');
216 		var any_non_breaking = element.hasAttribute ('data-is-non-breaking');
217 		for (i = 0; i < element.children.length; i++) {
218 			var el = element.children [i];
219 			propagateDataAttribute (el);
220 			any_breaking |= el.hasAttribute ('data-is-breaking');
221 			any_non_breaking |= el.hasAttribute ('data-is-non-breaking');
222 		}
223 
224 		if (any_breaking)
225 			element.setAttribute ('data-is-breaking', null);
226 		else if (any_non_breaking)
227 			element.setAttribute ('data-is-non-breaking', null);
228 		element.setAttribute ('data-is-propagated', null);
229 	}
230 
231 	function hideNonBreakingChanges ()
232 	{
233 		var topNodes = document.querySelectorAll ('[data-is-topmost]');
234 		var n;
235 		var i;
236 		for (n = 0; n < topNodes.length; n++) {
237 			propagateDataAttribute (topNodes [n]);
238 			var elements = topNodes [n].querySelectorAll ('[data-is-non-breaking]');
239 			for (i = 0; i < elements.length; i++) {
240 				var el = elements [i];
241 				if (!el.hasAttribute ('data-original-display'))
242 					el.setAttribute ('data-original-display', el.style.display);
243 				el.style.display = 'none';
244 			}
245 		}
246 
247 		var links = document.getElementsByClassName ('hide-nonbreaking');
248 		for (i = 0; i < links.length; i++)
249 			links [i].style.display = 'none';
250 		links = document.getElementsByClassName ('restore-nonbreaking');
251 		for (i = 0; i < links.length; i++)
252 			links [i].style.display = '';
253 	}
254 
255 	function showNonBreakingChanges ()
256 	{
257 		var elements = document.querySelectorAll ('[data-original-display]');
258 		var i;
259 		for (i = 0; i < elements.length; i++) {
260 			var el = elements [i];
261 			el.style.display = el.getAttribute ('data-original-display');
262 		}
263 
264 		var links = document.getElementsByClassName ('hide-nonbreaking');
265 		for (i = 0; i < links.length; i++)
266 			links [i].style.display = '';
267 		links = document.getElementsByClassName ('restore-nonbreaking');
268 		for (i = 0; i < links.length; i++)
269 			links [i].style.display = 'none';
270 	}
271 </script>");
272 							if (ac.SourceAssembly == ac.TargetAssembly) {
273 								file.WriteLine ("<h1>{0}.dll</h1>", ac.SourceAssembly);
274 							} else {
275 								file.WriteLine ("<h1>{0}.dll vs {1}.dll</h1>", ac.SourceAssembly, ac.TargetAssembly);
276 							}
277 							if (!State.IgnoreNonbreaking) {
278 								file.WriteLine ("<a href='javascript: hideNonBreakingChanges (); ' class='hide-nonbreaking'>Hide non-breaking changes</a>");
279 								file.WriteLine ("<a href='javascript: showNonBreakingChanges (); ' class='restore-nonbreaking' style='display: none;'>Show non-breaking changes</a>");
280 								file.WriteLine ("<br/>");
281 							}
282 							file.WriteLine ("<div data-is-topmost>");
283 							file.Write (diffHtml);
284 							file.WriteLine ("</div> <!-- end topmost div -->");
285 							file.WriteLine ("</div>");
286 						}
287 					}
288 				} else {
289 					State.Output = Console.Out;
290 					ac.Compare ();
291 				}
292 			}
293 			catch (Exception e) {
294 				Console.WriteLine (e);
295 				return 1;
296 			}
297 			return 0;
298 		}
299 	}
300 }
301