1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package net.sf.clirr.core.internal.checks;
21
22 import net.sf.clirr.core.ApiDifference;
23 import net.sf.clirr.core.Message;
24 import net.sf.clirr.core.Severity;
25 import net.sf.clirr.core.ScopeSelector;
26 import net.sf.clirr.core.internal.AbstractDiffReporter;
27 import net.sf.clirr.core.internal.ApiDiffDispatcher;
28 import net.sf.clirr.core.internal.ClassChangeCheck;
29 import net.sf.clirr.core.internal.CoIterator;
30 import net.sf.clirr.core.spi.JavaType;
31 import net.sf.clirr.core.spi.Method;
32 import net.sf.clirr.core.spi.Scope;
33
34 import java.util.ArrayList;
35 import java.util.HashMap;
36 import java.util.Iterator;
37 import java.util.List;
38 import java.util.Map;
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 private static final Message MSG_METHOD_NOW_IN_SUPERCLASS = new Message(7000);
50 private static final Message MSG_METHOD_NOW_IN_INTERFACE = new Message(7001);
51 private static final Message MSG_METHOD_REMOVED = new Message(7002);
52 private static final Message MSG_METHOD_OVERRIDE_REMOVED = new Message(7003);
53 private static final Message MSG_METHOD_ARGCOUNT_CHANGED = new Message(7004);
54 private static final Message MSG_METHOD_PARAMTYPE_CHANGED = new Message(7005);
55 private static final Message MSG_METHOD_RETURNTYPE_CHANGED = new Message(7006);
56 private static final Message MSG_METHOD_DEPRECATED = new Message(7007);
57 private static final Message MSG_METHOD_UNDEPRECATED = new Message(7008);
58 private static final Message MSG_METHOD_LESS_ACCESSIBLE = new Message(7009);
59 private static final Message MSG_METHOD_MORE_ACCESSIBLE = new Message(7010);
60 private static final Message MSG_METHOD_ADDED = new Message(7011);
61 private static final Message MSG_METHOD_ADDED_TO_INTERFACE = new Message(7012);
62 private static final Message MSG_ABSTRACT_METHOD_ADDED = new Message(7013);
63 private static final Message MSG_METHOD_NOW_FINAL = new Message(7014);
64 private static final Message MSG_METHOD_NOW_NONFINAL = new Message(7015);
65
66 private ScopeSelector scopeSelector;
67
68 /***
69 * Instantiates the check.
70 *
71 * @param dispatcher the dispatcher where detected differences shoudl be reported.
72 * @param scopeSelector defines the scopes to look at when searching for differences.
73 */
74 public MethodSetCheck(ApiDiffDispatcher dispatcher, ScopeSelector scopeSelector)
75 {
76 super(dispatcher);
77 this.scopeSelector = scopeSelector;
78 }
79
80 public final boolean check(JavaType compatBaseline, JavaType currentVersion)
81 {
82
83
84
85 if (compatBaseline.isInterface() ^ currentVersion.isInterface())
86 {
87 return true;
88 }
89
90 Map bNameToMethod = buildNameToMethodMap(compatBaseline);
91 Map cNameToMethod = buildNameToMethodMap(currentVersion);
92
93 CoIterator iter = new CoIterator(null, bNameToMethod.keySet(), cNameToMethod.keySet());
94
95 while (iter.hasNext())
96 {
97 iter.next();
98
99 String baselineMethodName = (String) iter.getLeft();
100 String currentMethodName = (String) iter.getRight();
101
102 if (baselineMethodName == null)
103 {
104
105 List currentMethods = (List) cNameToMethod.get(currentMethodName);
106 reportMethodsAdded(currentVersion, currentMethods);
107 }
108 else if (currentMethodName == null)
109 {
110
111 List baselineMethods = (List) bNameToMethod.get(baselineMethodName);
112 reportMethodsRemoved(compatBaseline, baselineMethods, currentVersion);
113 }
114 else
115 {
116
117
118 List baselineMethods = (List) bNameToMethod.get(baselineMethodName);
119 List currentMethods = (List) cNameToMethod.get(currentMethodName);
120
121 filterSoftMatchedMethods(
122 compatBaseline, baselineMethods,
123 currentVersion, currentMethods);
124
125 filterChangedMethods(
126 baselineMethodName,
127 compatBaseline, baselineMethods,
128 currentVersion, currentMethods);
129
130
131
132
133 if (!baselineMethods.isEmpty())
134 {
135 reportMethodsRemoved(compatBaseline, baselineMethods, currentVersion);
136 }
137
138 if (!currentMethods.isEmpty())
139 {
140 reportMethodsAdded(currentVersion, currentMethods);
141 }
142 }
143 }
144
145 return true;
146 }
147
148 /***
149 * Given a list of old and new methods for a particular method name,
150 * find the (old, new) method pairs which have identical argument lists.
151 * <p>
152 * For these:
153 * <ul>
154 * <li>report on changes in accessibility, return type, etc
155 * <li>remove from the list
156 * </ul>
157 *
158 * On return from this method, the old and new method lists contain only
159 * methods whose argument lists have changed between versions [or possibly,
160 * methods which have been deleted while one or more new methods of the
161 * same name have been added, depending on how you view it]. All other
162 * situations have been dealt with.
163 * <p>
164 * Note that one or both method lists may be empty on return from
165 * this method.
166 */
167 private void filterSoftMatchedMethods(
168 JavaType compatBaseline,
169 List baselineMethods,
170 JavaType currentVersion,
171 List currentMethods)
172 {
173 for (Iterator bIter = baselineMethods.iterator(); bIter.hasNext();)
174 {
175 Method bMethod = (Method) bIter.next();
176
177 for (Iterator cIter = currentMethods.iterator(); cIter.hasNext();)
178 {
179 Method cMethod = (Method) cIter.next();
180
181 if (isSoftMatch(bMethod, cMethod))
182 {
183 check(compatBaseline, bMethod, cMethod);
184 bIter.remove();
185 cIter.remove();
186 break;
187 }
188 }
189 }
190 }
191
192 /***
193 * Two methods are a "soft" match if they have the same name and argument
194 * list. No two methods on the same class are ever a "soft match" for
195 * each other, because the compiler requires distinct parameter lists for
196 * overloaded methods. This also implies that for a given method on an "old"
197 * class version, there are either zero or one "soft matches" on the new
198 * version.
199 * <p>
200 * However a "soft match" is not sufficient to ensure binary compatibility.
201 * A change in the method return type will result in a link error when used
202 * with code compiled against the previous version of the class.
203 * <p>
204 * There may also be other differences between methods that are regarded
205 * as "soft matches": the exceptions thrown, the deprecation status of the
206 * methods, their accessibility, etc.
207 */
208 private boolean isSoftMatch(Method oldMethod, Method newMethod)
209 {
210 String oldName = oldMethod.getName();
211 String newName = newMethod.getName();
212
213 if (!oldName.equals(newName))
214 {
215 return false;
216 }
217
218 StringBuffer buf = new StringBuffer();
219 appendHumanReadableArgTypeList(oldMethod, buf);
220 String oldArgs = buf.toString();
221
222 buf.setLength(0);
223 appendHumanReadableArgTypeList(newMethod, buf);
224 String newArgs = buf.toString();
225
226 return (oldArgs.equals(newArgs));
227 }
228
229 /***
230 * For each method in the baselineMethods list, find the "best match"
231 * in the currentMethods list, report the changes between this method
232 * pair, then remove both methods from the lists.
233 * <p>
234 * On return, at least one of the method lists will be empty.
235 */
236 private void filterChangedMethods(
237 String methodName,
238 JavaType compatBaseline,
239 List baselineMethods,
240 JavaType currentVersion,
241 List currentMethods)
242 {
243
244
245
246
247
248
249
250
251 while (!baselineMethods.isEmpty() && !currentMethods.isEmpty())
252 {
253 int[][] similarityTable = buildSimilarityTable(baselineMethods, currentMethods);
254
255 int min = Integer.MAX_VALUE;
256 int iMin = baselineMethods.size();
257 int jMin = currentMethods.size();
258 for (int i = 0; i < baselineMethods.size(); i++)
259 {
260 for (int j = 0; j < currentMethods.size(); j++)
261 {
262 final int tableEntry = similarityTable[i][j];
263 if (tableEntry < min)
264 {
265 min = tableEntry;
266 iMin = i;
267 jMin = j;
268 }
269 }
270 }
271 Method iMethod = (Method) baselineMethods.remove(iMin);
272 Method jMethod = (Method) currentMethods.remove(jMin);
273 check(compatBaseline, iMethod, jMethod);
274 }
275 }
276
277 private int[][] buildSimilarityTable(List baselineMethods, List currentMethods)
278 {
279 int[][] similarityTable = new int[baselineMethods.size()][currentMethods.size()];
280 for (int i = 0; i < baselineMethods.size(); i++)
281 {
282 for (int j = 0; j < currentMethods.size(); j++)
283 {
284 final Method iMethod = (Method) baselineMethods.get(i);
285 final Method jMethod = (Method) currentMethods.get(j);
286 similarityTable[i][j] = distance(iMethod, jMethod);
287 }
288 }
289 return similarityTable;
290 }
291
292 private int distance(Method m1, Method m2)
293 {
294 final JavaType[] m1Args = m1.getArgumentTypes();
295 final JavaType[] m2Args = m2.getArgumentTypes();
296
297 if (m1Args.length != m2Args.length)
298 {
299 return 1000 * Math.abs(m1Args.length - m2Args.length);
300 }
301
302 int retVal = 0;
303 for (int i = 0; i < m1Args.length; i++)
304 {
305 if (!m1Args[i].toString().equals(m2Args[i].toString()))
306 {
307 retVal += 1;
308 }
309 }
310 return retVal;
311 }
312
313 /***
314 * Searches the class hierarchy for a method that has a certain signature.
315 * @param methodSignature the sig we're looking for
316 * @param clazz class where search starts
317 * @return class name of a superclass of clazz, might be null
318 */
319 private String findSuperClassWithSignature(String methodSignature, JavaType clazz)
320 {
321 final JavaType[] superClasses = clazz.getSuperClasses();
322 for (int i = 0; i < superClasses.length; i++)
323 {
324 JavaType superClass = superClasses[i];
325 final Method[] superMethods = superClass.getMethods();
326 for (int j = 0; j < superMethods.length; j++)
327 {
328 Method superMethod = superMethods[j];
329 final String superMethodSignature = getMethodId(superClass, superMethod);
330 if (methodSignature.equals(superMethodSignature))
331 {
332 return superClass.getName();
333 }
334 }
335
336 }
337 return null;
338 }
339
340 /***
341 * Searches the class hierarchy for a method that has a certtain signature.
342 * @param methodSignature the sig we're looking for
343 * @param clazz class where search starts
344 * @return class name of a superinterface of clazz, might be null
345 */
346 private String findSuperInterfaceWithSignature(String methodSignature, JavaType clazz)
347 {
348 final JavaType[] superClasses = clazz.getAllInterfaces();
349 for (int i = 0; i < superClasses.length; i++)
350 {
351 JavaType superClass = superClasses[i];
352 final Method[] superMethods = superClass.getMethods();
353 for (int j = 0; j < superMethods.length; j++)
354 {
355 Method superMethod = superMethods[j];
356 final String superMethodSignature = getMethodId(superClass, superMethod);
357 if (methodSignature.equals(superMethodSignature))
358 {
359 return superClass.getName();
360 }
361 }
362
363 }
364 return null;
365 }
366
367 /***
368 * Given a list of methods, report each one as being removed.
369 */
370 private void reportMethodsRemoved(
371 JavaType baselineClass,
372 List baselineMethods,
373 JavaType currentClass)
374 {
375 for (Iterator i = baselineMethods.iterator(); i.hasNext();)
376 {
377 Method method = (Method) i.next();
378 reportMethodRemoved(baselineClass, method, currentClass);
379 }
380 }
381
382 /***
383 * Report that a method has been removed from a class.
384 * @param oldClass the class where the method was available
385 * @param oldMethod the method that has been removed
386 * @param currentClass the superclass where the method is now available, might be null
387 */
388 private void reportMethodRemoved(
389 JavaType oldClass,
390 Method oldMethod,
391 JavaType currentClass)
392 {
393 if (!scopeSelector.isSelected(oldMethod))
394 {
395 return;
396 }
397
398 String signature = getMethodId(oldClass, oldMethod);
399
400 String oldBaseClassForMethod = findSuperClassWithSignature(signature, oldClass);
401 String oldInterfaceForMethod = findSuperInterfaceWithSignature(signature, oldClass);
402
403 String newBaseClassForMethod = findSuperClassWithSignature(signature, currentClass);
404 String newInterfaceForMethod = findSuperInterfaceWithSignature(signature, currentClass);
405
406 boolean oldInheritedMethod = (oldBaseClassForMethod != null) || (oldInterfaceForMethod != null);
407 boolean newInheritedMethod = (newBaseClassForMethod != null) || (newInterfaceForMethod != null);
408
409 if (oldInheritedMethod && newInheritedMethod)
410 {
411
412
413
414
415 fireDiff(MSG_METHOD_OVERRIDE_REMOVED,
416 Severity.INFO,
417 oldClass, oldMethod, null);
418 }
419 else if (oldInheritedMethod)
420 {
421
422
423
424
425 fireDiff(MSG_METHOD_REMOVED,
426 getSeverity(oldClass, oldMethod, Severity.ERROR),
427 oldClass, oldMethod, null);
428 }
429 else if (newBaseClassForMethod != null)
430 {
431
432
433
434
435
436 fireDiff(MSG_METHOD_NOW_IN_SUPERCLASS,
437 Severity.INFO, oldClass, oldMethod,
438 new String[] {newBaseClassForMethod});
439 }
440 else if (newInterfaceForMethod != null)
441 {
442
443
444
445
446
447 fireDiff(MSG_METHOD_NOW_IN_INTERFACE,
448 Severity.INFO, oldClass, oldMethod,
449 new String[] {newInterfaceForMethod});
450 }
451 else
452 {
453
454
455
456 fireDiff(MSG_METHOD_REMOVED,
457 getSeverity(oldClass, oldMethod, Severity.ERROR),
458 oldClass, oldMethod, null);
459 }
460 }
461
462 /***
463 * Given a list of methods, report each one as being added.
464 */
465 private void reportMethodsAdded(
466 JavaType currentClass,
467 List currentMethods)
468 {
469 for (Iterator i = currentMethods.iterator(); i.hasNext();)
470 {
471 Method method = (Method) i.next();
472 reportMethodAdded(currentClass, method);
473 }
474 }
475
476 /***
477 * Report that a method has been added to a class.
478 */
479 private void reportMethodAdded(JavaType newClass, Method newMethod)
480 {
481 if (!scopeSelector.isSelected(newMethod))
482 {
483 return;
484 }
485
486 if (newClass.isInterface())
487 {
488
489
490
491
492
493 fireDiff(MSG_METHOD_ADDED_TO_INTERFACE,
494 getSeverity(newClass, newMethod, Severity.ERROR),
495 newClass, newMethod, null);
496 }
497 else if (newMethod.isAbstract())
498 {
499
500
501
502
503
504
505
506
507 fireDiff(MSG_ABSTRACT_METHOD_ADDED,
508 Severity.ERROR, newClass, newMethod, null);
509 }
510 else
511 {
512
513
514
515
516
517
518
519
520
521
522 fireDiff(MSG_METHOD_ADDED,
523 Severity.INFO, newClass, newMethod, null);
524 }
525 }
526
527 /***
528 * Builds a map from a method name to a List of methods.
529 */
530 private Map buildNameToMethodMap(JavaType clazz)
531 {
532 Method[] methods = clazz.getMethods();
533 Map retVal = new HashMap();
534 for (int i = 0; i < methods.length; i++)
535 {
536 Method method = methods[i];
537
538 final String name = method.getName();
539 List set = (List) retVal.get(name);
540 if (set == null)
541 {
542 set = new ArrayList();
543 retVal.put(name, set);
544 }
545 set.add(method);
546 }
547 return retVal;
548 }
549
550 private void check(JavaType compatBaseline, Method baselineMethod, Method currentMethod)
551 {
552 if (!scopeSelector.isSelected(baselineMethod) && !scopeSelector.isSelected(currentMethod))
553 {
554 return;
555 }
556
557 checkParameterTypes(compatBaseline, baselineMethod, currentMethod);
558 checkReturnType(compatBaseline, baselineMethod, currentMethod);
559 checkDeclaredExceptions(compatBaseline, baselineMethod, currentMethod);
560 checkDeprecated(compatBaseline, baselineMethod, currentMethod);
561 checkVisibility(compatBaseline, baselineMethod, currentMethod);
562 checkFinal(compatBaseline, baselineMethod, currentMethod);
563 }
564
565 private void checkParameterTypes(JavaType compatBaseline, Method baselineMethod, Method currentMethod)
566 {
567 JavaType[] bArgs = baselineMethod.getArgumentTypes();
568 JavaType[] cArgs = currentMethod.getArgumentTypes();
569
570 if (bArgs.length != cArgs.length)
571 {
572 fireDiff(MSG_METHOD_ARGCOUNT_CHANGED,
573 getSeverity(compatBaseline, baselineMethod, Severity.ERROR),
574 compatBaseline, baselineMethod, null);
575 return;
576 }
577
578
579 for (int i = 0; i < bArgs.length; i++)
580 {
581 JavaType bArg = bArgs[i];
582 JavaType cArg = cArgs[i];
583
584 if (bArg.getName().equals(cArg.getName()))
585 {
586 continue;
587 }
588
589
590 String[] args =
591 {
592 "" + (i + 1),
593 cArg.toString()
594 };
595 fireDiff(MSG_METHOD_PARAMTYPE_CHANGED,
596 getSeverity(compatBaseline, baselineMethod, Severity.ERROR),
597 compatBaseline, baselineMethod, args);
598 }
599 }
600
601 private void checkReturnType(JavaType compatBaseline, Method baselineMethod, Method currentMethod)
602 {
603 JavaType bReturnType = baselineMethod.getReturnType();
604 JavaType cReturnType = currentMethod.getReturnType();
605
606
607
608
609 if (!bReturnType.toString().equals(cReturnType.toString()))
610 {
611 fireDiff(MSG_METHOD_RETURNTYPE_CHANGED,
612 getSeverity(compatBaseline, baselineMethod, Severity.ERROR),
613 compatBaseline, baselineMethod,
614 new String[] {cReturnType.toString()});
615 }
616 }
617
618 private void checkDeclaredExceptions(
619 JavaType compatBaseline,
620 Method baselineMethod, Method currentMethod)
621 {
622
623 }
624
625 private void checkDeprecated(
626 JavaType compatBaseline,
627 Method baselineMethod, Method currentMethod)
628 {
629 boolean bIsDeprecated = baselineMethod.isDeprecated();
630 boolean cIsDeprecated = currentMethod.isDeprecated();
631
632 if (bIsDeprecated && !cIsDeprecated)
633 {
634 fireDiff(MSG_METHOD_UNDEPRECATED,
635 Severity.INFO, compatBaseline, baselineMethod, null);
636 }
637 else if (!bIsDeprecated && cIsDeprecated)
638 {
639 fireDiff(MSG_METHOD_DEPRECATED,
640 Severity.INFO, compatBaseline, baselineMethod, null);
641 }
642 }
643
644 /***
645 * Report changes in the declared accessibility of a method
646 * (public/protected/etc).
647 */
648 private void checkVisibility(JavaType compatBaseline, Method baselineMethod, Method currentMethod)
649 {
650 Scope bScope = baselineMethod.getEffectiveScope();
651 Scope cScope = currentMethod.getEffectiveScope();
652
653 if (cScope.isLessVisibleThan(bScope))
654 {
655 String[] args = {bScope.getDesc(), cScope.getDesc()};
656 fireDiff(MSG_METHOD_LESS_ACCESSIBLE,
657 getSeverity(compatBaseline, baselineMethod, Severity.ERROR),
658 compatBaseline, baselineMethod, args);
659 }
660 else if (cScope.isMoreVisibleThan(bScope))
661 {
662 String[] args = {bScope.getDesc(), cScope.getDesc()};
663 fireDiff(MSG_METHOD_MORE_ACCESSIBLE,
664 Severity.INFO, compatBaseline, baselineMethod, args);
665 }
666 }
667
668 private void checkFinal(
669 JavaType compatBaseline,
670 Method baselineMethod, Method currentMethod)
671 {
672 boolean bIsFinal = baselineMethod.isFinal();
673 boolean cIsFinal = currentMethod.isFinal();
674
675 if (bIsFinal && !cIsFinal)
676 {
677 fireDiff(MSG_METHOD_NOW_NONFINAL,
678 Severity.INFO, compatBaseline, baselineMethod, null);
679 }
680 else if (!bIsFinal && cIsFinal)
681 {
682 fireDiff(MSG_METHOD_NOW_FINAL,
683 Severity.ERROR, compatBaseline, baselineMethod, null);
684 }
685 }
686
687 /***
688 * Creates a human readable String that is similar to the method signature
689 * and identifies the method within a class.
690 * @param clazz the container of the method
691 * @param method the method to identify.
692 * @return a human readable id, for example "public void print(java.lang.String)"
693 */
694 private String getMethodId(JavaType clazz, Method method)
695 {
696 StringBuffer buf = new StringBuffer();
697
698 final String scopeDecl = method.getDeclaredScope().getDecl();
699 if (scopeDecl.length() > 0)
700 {
701 buf.append(scopeDecl);
702 buf.append(" ");
703 }
704
705 String name = method.getName();
706 if ("<init>".equals(name))
707 {
708 final String className = clazz.getName();
709 int idx = className.lastIndexOf('.');
710 name = className.substring(idx + 1);
711 }
712 else
713 {
714 buf.append(method.getReturnType());
715 buf.append(' ');
716 }
717 buf.append(name);
718 buf.append('(');
719 appendHumanReadableArgTypeList(method, buf);
720 buf.append(')');
721 return buf.toString();
722 }
723
724 private void appendHumanReadableArgTypeList(Method method, StringBuffer buf)
725 {
726 JavaType[] argTypes = method.getArgumentTypes();
727 String argSeparator = "";
728 for (int i = 0; i < argTypes.length; i++)
729 {
730 buf.append(argSeparator);
731 buf.append(argTypes[i].getName());
732 argSeparator = ", ";
733 }
734 }
735
736 private void fireDiff(Message msg, Severity severity, JavaType clazz, Method method, String[] args)
737 {
738 final String className = clazz.getName();
739 final ApiDifference diff =
740 new ApiDifference(
741 msg, severity, className, getMethodId(clazz, method), null, args);
742 getApiDiffDispatcher().fireDiff(diff);
743 }
744
745 }