/*
* Copyright 2015 Lukas Krejci
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package org.revapi.java.checks.methods;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.SimpleElementVisitor7;
import javax.lang.model.util.SimpleTypeVisitor7;
import org.revapi.CoIterator;
import org.revapi.Difference;
import org.revapi.java.spi.CheckBase;
import org.revapi.java.spi.Code;
import org.revapi.java.spi.JavaMethodElement;
import org.revapi.java.spi.Util;
/**
* @author Lukas Krejci
* @since 0.3.0
*/
public class ExceptionsThrownChanged extends CheckBase {
private static final SimpleTypeVisitor7<TypeElement, Void> CONVERT_TO_ELEMENT = new SimpleTypeVisitor7<TypeElement, Void>() {
@Override
public TypeElement visitDeclared(DeclaredType t, Void ignored) {
return t.asElement().accept(new SimpleElementVisitor7<TypeElement, Void>() {
@Override
public TypeElement visitType(TypeElement e, Void aVoid) {
return e;
}
}, null);
}
};
@Override
public EnumSet<Type> getInterest() {
return EnumSet.of(Type.METHOD);
}
@Override
protected void doVisitMethod(@Nullable JavaMethodElement oldMethod, @Nullable JavaMethodElement newMethod) {
if (!isBothAccessible(oldMethod, newMethod)) {
return;
}
List<? extends TypeMirror> oldExceptions = oldMethod.getModelRepresentation().getThrownTypes();
List<? extends TypeMirror> newExceptions = newMethod.getModelRepresentation().getThrownTypes();
Set<String> oldExceptionClassNames = oldExceptions.isEmpty() ? Collections.emptySet() : oldExceptions.stream()
.map(Util::toUniqueString).collect(Collectors.toSet());
Set<String> newExceptionClassNames = newExceptions.isEmpty() ? Collections.emptySet() : newExceptions.stream()
.map(Util::toUniqueString).collect(Collectors.toSet());
if (!(oldExceptions.isEmpty() && newExceptions.isEmpty()) && !oldExceptionClassNames.equals(newExceptionClassNames)) {
pushActive(oldMethod, newMethod);
}
}
@Nullable
@Override
protected List<Difference> doEnd() {
ActiveElements<JavaMethodElement> methods = popIfActive();
if (methods == null) {
return null;
}
List<? extends TypeMirror> oldExceptions = new ArrayList<>(methods.oldElement.getModelRepresentation().getThrownTypes());
List<? extends TypeMirror> newExceptions = new ArrayList<>(methods.newElement.getModelRepresentation().getThrownTypes());
Comparator<TypeMirror> byClassName = Comparator.comparing(Util::toUniqueString);
Collections.sort(oldExceptions, byClassName);
Collections.sort(newExceptions, byClassName);
CoIterator<TypeMirror> it = new CoIterator<>(oldExceptions.iterator(), newExceptions.iterator(),
byClassName);
List<String> removedRuntimeExceptions = new ArrayList<>();
List<String> addedRuntimeExceptions = new ArrayList<>();
List<String> removedCheckedExceptions = new ArrayList<>();
List<String> addedCheckedExceptions = new ArrayList<>();
boolean reportSomething = false;
while (it.hasNext()) {
it.next();
TypeMirror oldType = it.getLeft();
TypeMirror newType = it.getRight();
if (oldType != null && newType != null) {
//they match, so move on, nothing to report here
continue;
}
reportSomething = true;
TypeElement oldException = oldType == null ? null : oldType.accept(CONVERT_TO_ELEMENT, null);
TypeElement newException = newType == null ? null : newType.accept(CONVERT_TO_ELEMENT, null);
if (oldException != null) {
if (isRuntimeException(oldException)) {
removedRuntimeExceptions.add(oldException.getQualifiedName().toString());
} else {
removedCheckedExceptions.add(oldException.getQualifiedName().toString());
}
} else if (newException != null) {
if (isRuntimeException(newException)) {
addedRuntimeExceptions.add(newException.getQualifiedName().toString());
} else {
addedCheckedExceptions.add(newException.getQualifiedName().toString());
}
}
}
if (!reportSomething) {
return null;
}
List<Difference> ret = new ArrayList<>();
if (!removedRuntimeExceptions.isEmpty()) {
removedRuntimeExceptions.forEach(ex ->
ret.add(createDifference(Code.METHOD_RUNTIME_EXCEPTION_REMOVED,
Code.attachmentsFor(methods.oldElement, methods.newElement,
"exception", ex)))
);
}
if (!addedRuntimeExceptions.isEmpty()) {
addedRuntimeExceptions.forEach(ex ->
ret.add(createDifference(Code.METHOD_RUNTIME_EXCEPTION_ADDED,
Code.attachmentsFor(methods.oldElement, methods.newElement,
"exception", ex)))
);
}
if (!addedCheckedExceptions.isEmpty()) {
addedCheckedExceptions.forEach(ex ->
ret.add(createDifference(Code.METHOD_CHECKED_EXCEPTION_ADDED,
Code.attachmentsFor(methods.oldElement, methods.newElement,
"exception", ex)))
);
}
if (!removedCheckedExceptions.isEmpty()) {
removedCheckedExceptions.forEach(ex ->
ret.add(createDifference(Code.METHOD_CHECKED_EXCEPTION_REMOVED,
Code.attachmentsFor(methods.oldElement, methods.newElement,
"exception", ex)))
);
}
return ret;
}
private boolean isRuntimeException(TypeElement exception) {
//noinspection LoopStatementThatDoesntLoop
while (exception != null) {
Name fqn = exception.getQualifiedName();
if (fqn.contentEquals("java.lang.RuntimeException")) {
return true;
}
if (fqn.contentEquals("java.lang.Error")) {
return true;
}
if (fqn.contentEquals("java.lang.Exception")) {
return false;
}
if (fqn.contentEquals("java.lang.Throwable")) {
return false;
}
exception = exception.getSuperclass().accept(CONVERT_TO_ELEMENT, null);
}
return false;
}
}