1 //////////////////////////////////////////////////////////////////////////////
2 // Clirr: compares two versions of a java library for binary compatibility
3 // Copyright (C) 2003 - 2004 Lars Kühne
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 //////////////////////////////////////////////////////////////////////////////
19
20 package net.sf.clirr.checks;
21
22 import net.sf.clirr.event.ApiDifference;
23 import net.sf.clirr.event.Severity;
24 import net.sf.clirr.framework.AbstractDiffReporter;
25 import net.sf.clirr.framework.ApiDiffDispatcher;
26 import net.sf.clirr.framework.ClassChangeCheck;
27 import org.apache.bcel.classfile.JavaClass;
28 import org.apache.bcel.classfile.Method;
29 import org.apache.bcel.generic.Type;
30
31 import java.util.ArrayList;
32 import java.util.Collection;
33 import java.util.HashMap;
34 import java.util.Iterator;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.Set;
38 import java.util.TreeSet;
39
40 /***
41 * Checks the methods of a class.
42 *
43 * @author lkuehne
44 */
45 public class MethodSetCheck
46 extends AbstractDiffReporter
47 implements ClassChangeCheck
48 {
49 /*** {@inheritDoc} */
50 public MethodSetCheck(ApiDiffDispatcher dispatcher)
51 {
52 super(dispatcher);
53 }
54
55 public final void check(JavaClass compatBaseline, JavaClass currentVersion)
56 {
57 // Dont't report method problems when gender has changed, as
58 // really the whole API is a pile of crap then - let GenderChange check
59 // do it's job, and that's it
60 if (compatBaseline.isInterface() ^ currentVersion.isInterface())
61 {
62 return;
63 }
64
65 // The main problem here is to figure out which old method corresponds to which new method.
66
67 // Methods that are named differently are trated as unrelated
68 //
69 // For Methods that differ only in their parameter list we build a similarity table, i.e.
70 // for new method i and old method j we have number that charaterizes how similar
71 // the method signatures are (0 means equal, higher number means more different)
72
73 Map bNameToMethod = buildNameToMethodMap(compatBaseline);
74 Map cNameToMethod = buildNameToMethodMap(currentVersion);
75
76 checkAddedOrRemoved(bNameToMethod, cNameToMethod, compatBaseline, currentVersion);
77
78 // now the key sets of the two maps are equal,
79 // we only have collections of methods that have the same name
80
81 // for each name analyse the differences
82 for (Iterator it = bNameToMethod.keySet().iterator(); it.hasNext();)
83 {
84 String name = (String) it.next();
85
86 List baselineMethods = (List) bNameToMethod.get(name);
87 List currentMethods = (List) cNameToMethod.get(name);
88
89 while (baselineMethods.size() * currentMethods.size() > 0)
90 {
91 int[][] similarityTable = buildSimilarityTable(baselineMethods, currentMethods);
92
93 int min = Integer.MAX_VALUE;
94 int iMin = baselineMethods.size();
95 int jMin = currentMethods.size();
96 for (int i = 0; i < baselineMethods.size(); i++)
97 {
98 for (int j = 0; j < currentMethods.size(); j++)
99 {
100 final int tableEntry = similarityTable[i][j];
101 if (tableEntry < min)
102 {
103 min = tableEntry;
104 iMin = i;
105 jMin = j;
106 }
107 }
108 }
109 Method iMethod = (Method) baselineMethods.remove(iMin);
110 Method jMethod = (Method) currentMethods.remove(jMin);
111 check(compatBaseline, iMethod, jMethod);
112 }
113 }
114 }
115
116 private int[][] buildSimilarityTable(List baselineMethods, List currentMethods)
117 {
118 int[][] similarityTable = new int[baselineMethods.size()][currentMethods.size()];
119 for (int i = 0; i < baselineMethods.size(); i++)
120 {
121 for (int j = 0; j < currentMethods.size(); j++)
122 {
123 final Method iMethod = (Method) baselineMethods.get(i);
124 final Method jMethod = (Method) currentMethods.get(j);
125 similarityTable[i][j] = distance(iMethod, jMethod);
126 }
127 }
128 return similarityTable;
129 }
130
131 private int distance(Method m1, Method m2)
132 {
133 final Type[] m1Args = m1.getArgumentTypes();
134 final Type[] m2Args = m2.getArgumentTypes();
135
136 if (m1Args.length != m2Args.length)
137 {
138 return 1000 * Math.abs(m1Args.length - m2Args.length);
139 }
140
141 int retVal = 0;
142 for (int i = 0; i < m1Args.length; i++)
143 {
144 if (!m1Args[i].toString().equals(m2Args[i].toString()))
145 {
146 retVal += 1;
147 }
148 }
149 return retVal;
150 }
151
152 /***
153 * Checks for added or removed methods, modifies the argument maps so their key sets are equal.
154 */
155 private void checkAddedOrRemoved(
156 Map bNameToMethod,
157 Map cNameToMethod,
158 JavaClass compatBaseline,
159 JavaClass currentVersion)
160 {
161 // create copies to avoid concurrent modification exception
162 Set baselineNames = new TreeSet(bNameToMethod.keySet());
163 Set currentNames = new TreeSet(cNameToMethod.keySet());
164
165 for (Iterator it = baselineNames.iterator(); it.hasNext();)
166 {
167 String name = (String) it.next();
168 if (!currentNames.contains(name))
169 {
170 Collection removedMethods = (Collection) bNameToMethod.get(name);
171 for (Iterator rmIterator = removedMethods.iterator(); rmIterator.hasNext();)
172 {
173 Method method = (Method) rmIterator.next();
174 reportMethodRemoved(compatBaseline, method);
175 }
176 bNameToMethod.remove(name);
177 }
178 }
179
180 for (Iterator it = currentNames.iterator(); it.hasNext();)
181 {
182 String name = (String) it.next();
183 if (!baselineNames.contains(name))
184 {
185 Collection addedMethods = (Collection) cNameToMethod.get(name);
186 for (Iterator addIterator = addedMethods.iterator(); addIterator.hasNext();)
187 {
188 Method method = (Method) addIterator.next();
189 reportMethodAdded(currentVersion, method);
190 }
191 cNameToMethod.remove(name);
192 }
193 }
194 }
195
196 private void reportMethodRemoved(JavaClass oldClass, Method oldMethod)
197 {
198 fireDiff("Method '"
199 + getMethodId(oldClass, oldMethod)
200 + "' has been removed",
201 Severity.ERROR, oldClass, oldMethod);
202 }
203
204 private void reportMethodAdded(JavaClass newClass, Method newMethod)
205 {
206
207 final Severity severity = !newClass.isInterface() && (newClass.isFinal() || !newMethod.isAbstract())
208 ? Severity.INFO
209 : Severity.ERROR;
210
211 fireDiff("Method '"
212 + getMethodId(newClass, newMethod)
213 + "' has been added",
214 severity, newClass, newMethod);
215 }
216
217 /***
218 * Builds a map from a method name to a List of methods.
219 */
220 private Map buildNameToMethodMap(JavaClass clazz)
221 {
222 Method[] methods = clazz.getMethods();
223 Map retVal = new HashMap();
224 for (int i = 0; i < methods.length; i++)
225 {
226 Method method = methods[i];
227
228 if (!(method.isPublic() || method.isProtected()))
229 {
230 continue;
231 }
232
233 final String name = method.getName();
234 List set = (List) retVal.get(name);
235 if (set == null)
236 {
237 set = new ArrayList();
238 retVal.put(name, set);
239 }
240 set.add(method);
241 }
242 return retVal;
243 }
244
245 private void check(JavaClass compatBaseline, Method baselineMethod, Method currentMethod)
246 {
247 checkParameterTypes(compatBaseline, baselineMethod, currentMethod);
248 checkReturnType(compatBaseline, baselineMethod, currentMethod);
249 checkDeclaredExceptions(compatBaseline, baselineMethod, currentMethod);
250 }
251
252 private void checkParameterTypes(JavaClass compatBaseline, Method baselineMethod, Method currentMethod)
253 {
254 Type[] bArgs = baselineMethod.getArgumentTypes();
255 Type[] cArgs = currentMethod.getArgumentTypes();
256
257 if (bArgs.length != cArgs.length)
258 {
259 fireDiff("In Method '" + getMethodId(compatBaseline, baselineMethod)
260 + "' the number of arguments has changed",
261 Severity.ERROR, compatBaseline, baselineMethod);
262 return;
263 }
264
265 //System.out.println("baselineMethod = " + getMethodId(compatBaseline, baselineMethod));
266 for (int i = 0; i < bArgs.length; i++)
267 {
268 Type bArg = bArgs[i];
269 Type cArg = cArgs[i];
270
271 if (bArg.toString().equals(cArg.toString()))
272 {
273 continue;
274 }
275
276 // TODO: Check assignability...
277 fireDiff("Parameter " + (i + 1) + " of '" + getMethodId(compatBaseline, baselineMethod)
278 + "' has changed it's type to " + cArg,
279 Severity.ERROR, compatBaseline, baselineMethod);
280 }
281 }
282
283 private void checkReturnType(JavaClass compatBaseline, Method baselineMethod, Method currentMethod)
284 {
285 Type bReturnType = baselineMethod.getReturnType();
286 Type cReturnType = currentMethod.getReturnType();
287
288 // TODO: Check assignability...
289 if (!bReturnType.toString().equals(cReturnType.toString()))
290 {
291 fireDiff("Return type of Method '" + getMethodId(compatBaseline, baselineMethod)
292 + "' has been changed to " + cReturnType,
293 Severity.ERROR, compatBaseline, baselineMethod);
294 }
295
296
297 }
298
299 private void checkDeclaredExceptions(
300 JavaClass compatBaseline, Method baselineMethod, Method currentMethod)
301 {
302 // TODO
303 }
304
305 /***
306 * Creates a human readable String that is similar to the method signature
307 * and identifies the method within a class.
308 * @param clazz the container of the method
309 * @param method the method to identify.
310 * @return a human readable id, for example "public void print(java.lang.String)"
311 */
312 private String getMethodId(JavaClass clazz, Method method)
313 {
314 if (!method.isPublic() && !method.isProtected())
315 {
316 throw new IllegalArgumentException();
317 }
318
319 StringBuffer buf = new StringBuffer();
320
321 buf.append(method.isPublic() ? "public" : "protected");
322 buf.append(" ");
323
324 String name = method.getName();
325 if ("<init>".equals(name))
326 {
327 final String className = clazz.getClassName();
328 int idx = className.lastIndexOf('.');
329 name = className.substring(idx + 1);
330 }
331 else
332 {
333 buf.append(method.getReturnType());
334 buf.append(' ');
335 }
336 buf.append(name);
337 buf.append('(');
338 appendHumanReadableArgTypeList(method, buf);
339 buf.append(')');
340 return buf.toString();
341 }
342
343 private void appendHumanReadableArgTypeList(Method method, StringBuffer buf)
344 {
345 Type[] argTypes = method.getArgumentTypes();
346 String argSeparator = "";
347 for (int i = 0; i < argTypes.length; i++)
348 {
349 buf.append(argSeparator);
350 buf.append(argTypes[i].toString());
351 argSeparator = ", ";
352 }
353 }
354
355 private void fireDiff(String report, Severity severity, JavaClass clazz, Method method)
356 {
357 final String className = clazz.getClassName();
358 final ApiDifference diff =
359 new ApiDifference(report + " in " + className,
360 severity, className, getMethodId(clazz, method), null);
361 getApiDiffDispatcher().fireDiff(diff);
362
363 }
364
365 }
This page was automatically generated by Maven