Merge PR #3253 by @iaronaraujo - API scanning

develop
Cervator 2018-04-14 21:30:47 -04:00
commit dba360b11b
5 changed files with 630 additions and 0 deletions

3
.gitignore vendored
View File

@ -94,5 +94,8 @@ nuiEditorAutosave.json
/natives/
config/metrics/
# Ignore API files
API_file.txt
New_API_file.txt
# Ignore weird stuff that might be obsolete

View File

@ -0,0 +1,298 @@
/*
* Copyright 2018 MovingBlocks
*
* 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.terasology.documentation.apiScraper;
import org.terasology.documentation.apiScraper.util.ApiMethod;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.List;
/**
* Detects API changes between two versions.
*/
public final class ApiComparator {
private static final String ORIGINAL_API_FILE = "API_file.txt";
private static final String NEW_API_FILE = "New_API_file.txt";
private ApiComparator() {
}
/*
* Generates a NEW_API_FILE and compares it with the ORIGINAL_API_FILE to detect major and minor version increases.
* Major increases: Deletion of class, new public abstract method, new interface public method,
* public method deletion, existing public method's change of parameters types, exception types or return type.
* Minor increases: Creation of a new class and new public method in an abstract class.
*/
public static void main(String[] args) throws Exception {
try (BufferedReader br = new BufferedReader(new FileReader(ORIGINAL_API_FILE))) {
//Creating a map with the original api's data
Map<String, Collection<ApiMethod>> originalApi = getApi(br);
br.close();
//Generating "New_API_file.txt"
BufferedWriter writer = new BufferedWriter(new FileWriter(new File(NEW_API_FILE)));
writer.write(CompleteApiScraper.getApi().toString());
writer.flush();
writer.close();
BufferedReader br2 = new BufferedReader(new FileReader(NEW_API_FILE));
//Creating a map with the new api's data
Map<String, Collection<ApiMethod>> newApi = getApi(br2);
br2.close();
//Begins comparison and increases report
System.out.println("=================================================================");
checkClassAdditionAndDeletion(originalApi, newApi);
checkMethodChanges(originalApi, newApi);
System.out.println("REPORT FINISHED");
}
}
/**
* Reads an api file and puts its information in a map to be used in the api comparison
* @param br BufferedReader containing an api file content
* @return A map with the api classes and interfaces as keys.Their methods and as a list of ApiMethods in the values
* @throws Exception if the readLine fails.
*/
private static Map<String, Collection<ApiMethod>> getApi(BufferedReader br) throws Exception {
String line = br.readLine();
Map<String, Collection<ApiMethod>> api = new HashMap<>();
while (line != null) {
if (line.startsWith("*")) {
if (line.endsWith("(PACKAGE)")) {
line = br.readLine();
continue;
}
String className = line;
String aux;
api.put(className, new ArrayList<>());
ApiMethod method;
aux = br.readLine();
while ((aux != null && (aux.endsWith("(METHOD)")
|| aux.endsWith("(CONSTRUCTOR)")
|| aux.endsWith("(ABSTRACT METHOD)")
|| aux.endsWith("(DEFAULT METHOD)")))) {
//Checks if its a method or constructor
if (aux.endsWith("(METHOD)") || aux.endsWith("(ABSTRACT METHOD)") || aux.endsWith("(DEFAULT METHOD)")) {
String returnType = br.readLine();
String parameters = br.readLine();
String exceptionType = br.readLine();
method = new ApiMethod(className, aux, returnType, exceptionType, parameters);
} else {
String returnType = "";
String parameters = br.readLine();
String exceptionType = "";
method = new ApiMethod(className, aux, returnType, exceptionType, parameters);
}
api.get(className).add(method);
aux = br.readLine();
}
line = aux;
} else {
line = br.readLine();
}
}
return api;
}
private static void checkClassAdditionAndDeletion(Map<String, Collection<ApiMethod>> originalApi, Map<String, Collection<ApiMethod>> newApi) {
System.out.println("Checking Class Addition and Deletion");
for (String className : originalApi.keySet()) {
if (!newApi.containsKey(className)) {
System.out.println("MAJOR INCREASE, DELETION OF " + className);
}
}
for (String className : newApi.keySet()) {
if (!originalApi.containsKey(className)) {
System.out.println("MINOR INCREASE, ADDITION OF " + className);
}
}
}
/**
* Checks creation and deletion of methods, as well as existing method changes
* @param originalApi the original api generated from ORIGINAL_API_FILE
* @param newApi the new apí generated from NEW_API_FILE
*/
private static void checkMethodChanges(Map<String, Collection<ApiMethod>> originalApi,
Map<String, Collection<ApiMethod>> newApi) {
System.out.println("Checking Method Changes");
Collection<ApiMethod> originalMethods;
Collection<ApiMethod> newMethods;
for (String className : originalApi.keySet()) {
originalMethods = originalApi.get(className);
newMethods = newApi.get(className);
if (newMethods == null) {
continue;
}
checkMethodDeletion(originalMethods, newMethods);
for (ApiMethod method2 : newMethods) {
boolean found = false; // if found, the method is an existing one or a new overloaded method
for (ApiMethod method1 : originalMethods) {
if (method1.getName().equals(method2.getName())) {
ApiMethod auxMethod = getMethodWithSameNameAndParameters(method2, originalMethods);
if (auxMethod.getName().equals("")) {
ApiMethod auxMethod2 = getMethodWithSameNameAndParameters(method1, newMethods);
if (auxMethod2.getName().equals("")) {
checkMethodIncrease(method1, method2);
} else if (isInterfaceOrAbstract(method2.getClassName())) {
System.out.println("MINOR INCREASE, NEW OVERLOADED METHOD " + method2.getName() +
" ON " + method2.getClassName() + "\nNEW PARAMETERS: " + method2.getParametersType());
System.out.println("=================================================================");
}
} else {
checkMethodIncrease(auxMethod, method2);
}
found = true;
}
}
if (!found) {
if (isInterfaceOrAbstract(method2.getClassName())) {
if (method2.getName().endsWith("(ABSTRACT METHOD)")) {
System.out.println("MAJOR INCREASE, NEW ABSTRACT METHOD " + method2.getName() + " ON " + method2.getClassName());
} else {
String minorOrMajor;
if (method2.getClassName().endsWith("(INTERFACE)")) {
if (method2.getName().endsWith("(DEFAULT METHOD)")) {
minorOrMajor = "MINOR";
} else {
minorOrMajor = "MAJOR";
}
} else {
minorOrMajor = "MINOR";
}
System.out.println(minorOrMajor + " INCREASE, NEW METHOD " + method2.getName() + " ON " + method2.getClassName());
}
} else {
System.out.println("MINOR INCREASE, NEW METHOD " + method2.getName() + " ON " + method2.getClassName());
}
System.out.println("=================================================================");
}
}
}
}
private static void checkMethodDeletion(Collection<ApiMethod> originalMethods, Collection<ApiMethod> newMethods) {
List<String> checkedMethods = new ArrayList<>();
for (ApiMethod method1 : originalMethods) {
boolean found = false;
List<ApiMethod> newMethodsWithSameName = new ArrayList<>();
List<ApiMethod> originalMethodsWithSameName = new ArrayList<>();
for (ApiMethod method2 : newMethods) {
if (method1.getName().equals(method2.getName())) {
found = true;
newMethodsWithSameName.add(method2);
}
}
//this checks the deletion of an overloaded method
if (found && !checkedMethods.contains(method1.getName())) {
for (ApiMethod oMethod : originalMethods) {
if (oMethod.getName().equals(method1.getName())) {
originalMethodsWithSameName.add(oMethod);
}
}
if ((originalMethodsWithSameName.size() - newMethodsWithSameName.size()) > 0) {
for (ApiMethod method : originalMethodsWithSameName) {
ApiMethod result = getMethodWithSameNameAndParameters(method, newMethodsWithSameName);
if (result.getName().equals("")) {
checkedMethods.add(method.getName());
System.out.println("MAJOR INCREASE, OVERLOADED METHOD DELETION: " + method.getName()
+ " ON " + method.getClassName() + "\nPARAMETERS: " + method.getParametersType());
}
}
}
}
if (!found) {
System.out.println("MAJOR INCREASE, METHOD DELETION: " + method1.getName() + " ON " + method1.getClassName());
}
}
}
private static boolean isInterfaceOrAbstract(String className) {
return (className.endsWith("(ABSTRACT CLASS)") || className.endsWith("(INTERFACE)"));
}
/**
* Compares a not overloaded method in the newApi and originalApi to notify parameter type, return type or
* exception type changes
* @param method1 a not overloaded method from the originalApi, with the same name as method2
* @param method2 a not overloaded method from the newApi, with the same name as method1
*/
private static void checkMethodIncrease(ApiMethod method1, ApiMethod method2) {
check(method1.getReturnType(), method2.getReturnType(), method1.getName(), method1.getClassName());
check(method1.getParametersType(), method2.getParametersType(), method1.getName(), method1.getClassName());
check(method1.getExceptionType(), method2.getExceptionType(), method1.getName(), method1.getClassName());
}
/**
* Compares a method's field in the newApi and originalApi. This field can be, return, parameter or exception type
* @param s1 field to be compared from a method in the originalApi
* @param s2 field to be compared from a method in the newApi
* @param methodName name of the method to have it's field being compared
* @param className the name of the class the have the method
*/
private static void check(String s1, String s2, String methodName, String className) {
if (!s1.equals(s2)) {
System.out.println("MAJOR INCREASE ON : " + methodName + " " + className);
System.out.println("ORIGINAL: " + s1);
System.out.println("NEW: " + s2);
System.out.println("=================================================================");
}
}
/**
* Tries to find a method with the same name and parameter type as 'method' in a collection of methods
* @param method the method used in the search
* @param methods the collection of methods
* @return the method with the same name and parameter type as 'method' if it exists. If not, returns a new
* ApiMethod with all the attributes being empty String.
*/
private static ApiMethod getMethodWithSameNameAndParameters(ApiMethod method, Collection<ApiMethod> methods) {
for (ApiMethod m : methods) {
if (m.getName().equals(method.getName()) && m.getParametersType().equals(method.getParametersType())) {
return m;
}
}
return new ApiMethod("", "", "", "", "");
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2018 MovingBlocks
*
* 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.terasology.documentation.apiScraper;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
/**
* Saves the API generated by CompleteApiScraper in a txt file
*/
public final class ApiSaver {
private ApiSaver() {
}
/**
* @param args (ignored)
* @throws Exception if the module environment cannot be loaded
*/
public static void main(String[] args) throws Exception {
StringBuffer api = CompleteApiScraper.getApi();
BufferedWriter writer = new BufferedWriter(new FileWriter(new File("API_file.txt")));
writer.write(api.toString());
writer.flush();
writer.close();
System.out.println("API file is ready!");
}
}

View File

@ -0,0 +1,195 @@
/*
* Copyright 2018 MovingBlocks
*
* 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.terasology.documentation.apiScraper;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.engine.module.ExternalApiWhitelist;
import org.terasology.engine.module.ModuleManager;
import org.terasology.module.ModuleEnvironment;
import org.terasology.module.sandbox.API;
import org.terasology.testUtil.ModuleManagerFactory;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.stream.Collectors;
/**
* Enumerates all classes, interfaces and packages that are annotated with {@link API} and their public methods and
* constructors.
*/
final class CompleteApiScraper {
private static final String TERASOLOGY_API_CLASS_CATEGORY = "terasology engine";
private static final String EXTERNAL = "external";
private static final Logger logger = LoggerFactory.getLogger(CompleteApiScraper.class);
private CompleteApiScraper() {
// Private constructor, utility class
}
/**
*
* @return Project's Packages, Interfaces, Classes and Methods
* @throws Exception if the module environment cannot be loaded
*/
static StringBuffer getApi() throws Exception {
ModuleManager moduleManager = ModuleManagerFactory.create();
ModuleEnvironment environment = moduleManager.getEnvironment();
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Multimap<String, String> api = Multimaps.newMultimap(new HashMap<String, Collection<String>>(), ArrayList::new);
for (Class<?> apiClass : environment.getTypesAnnotatedWith(API.class)) {
boolean isPackage = apiClass.isSynthetic();
URL location;
String category;
String apiPackage = "";
if (isPackage) {
apiPackage = apiClass.getPackage().getName();
location = classLoader.getResource(apiPackage.replace('.', '/'));
} else {
location = apiClass.getResource('/' + apiClass.getName().replace('.', '/') + ".class");
}
if (location == null) {
logger.error("Failed to get a class/package location, skipping " + apiClass);
continue;
}
switch (location.getProtocol()) {
case "jar" :
// Find out what jar it came from and consider that the category
String categoryFragment = location.getPath();
int bang = categoryFragment.lastIndexOf("!");
int hyphen = categoryFragment.lastIndexOf("-", bang);
int slash = categoryFragment.lastIndexOf("/", hyphen);
category = categoryFragment.substring(slash + 1, hyphen);
if (isPackage) {
api.put(category, apiPackage + " (PACKAGE)");
} else {
addToApi(category, apiClass, api);
}
break;
case "file" :
// If file based we know it is local so organize it like that
category = TERASOLOGY_API_CLASS_CATEGORY;
if (isPackage) {
api.put(category, apiPackage + " (PACKAGE)");
} else {
addToApi(category, apiClass, api);
}
break;
default :
logger.error("Unknown protocol for: " + apiClass + ", came from " + location);
}
}
api.putAll(EXTERNAL, ExternalApiWhitelist.CLASSES.stream()
.map(clazz->clazz.getName() + " (CLASS)").collect(Collectors.toSet()));
api.putAll(EXTERNAL, ExternalApiWhitelist.PACKAGES.stream()
.map(packagee->packagee + " (PACKAGE)").collect(Collectors.toSet()));
//Puts the information in the StringBuffer
StringBuffer stringApi = new StringBuffer();
stringApi.append("# Modding API:\n");
for (String key : api.keySet()) {
stringApi.append("## ");
stringApi.append(key);
stringApi.append("\n");
for (String value : api.get(key)) {
stringApi.append("* ");
stringApi.append(value);
stringApi.append("\n");
}
stringApi.append("\n");
}
return stringApi;
}
/**
* Adds interface or class and their methods and constructors to api
* are also added.
* @param category where the apiClass belongs
* @param apiClass the class or interface to be added
* @param api that maps category to classes/interface/methods
*/
private static void addToApi(String category, Class<?> apiClass, Multimap<String, String> api) {
String className = apiClass.getName();
String type;
if (apiClass.isInterface()) {
type = " (INTERFACE)";
} else {
int modifier = apiClass.getModifiers();
if (Modifier.isAbstract(modifier)) {
type = " (ABSTRACT CLASS)";
} else {
type = " (CLASS)";
}
}
api.put(category, className + type);
//Add current apiClass's constructors
Constructor[] constructors = apiClass.getDeclaredConstructors();
for (Constructor constructor : constructors) {
api.put(category, " - " + constructor.getName() + " (CONSTRUCTOR)");
api.put(category, " -- " + Arrays.toString(constructor.getParameterTypes()) + " (PARAMETERS)");
}
//Add current apiClass's methods
Method[] methods = apiClass.getDeclaredMethods();
for (Method method: methods) {
if (!method.isDefault() && !method.isBridge() && !method.isSynthetic()) {
//Check if it's an abstract method
int modifier = method.getModifiers();
if (Modifier.isAbstract(modifier)) {
type = " (ABSTRACT METHOD)";
} else {
type = " (METHOD)";
}
//Adds method's information
api.put(category, " - " + method.getName() + type);
api.put(category, " -- " + method.getReturnType() + " (RETURN)");
api.put(category, " -- " + Arrays.toString(method.getParameterTypes()) + " (PARAMETERS)");
api.put(category, " -- " + Arrays.toString(method.getExceptionTypes()) + " (EXCEPTIONS)");
} else if (method.isDefault() && apiClass.isInterface()) {
api.put(category, " - " + method.getName() + " (DEFAULT METHOD)");
api.put(category, " -- " + method.getReturnType() + " (RETURN)");
api.put(category, " -- " + Arrays.toString(method.getParameterTypes()) + " (PARAMETERS)");
api.put(category, " -- " + Arrays.toString(method.getExceptionTypes()) + " (EXCEPTIONS)");
}
}
}
}

View File

@ -0,0 +1,92 @@
/*
* Copyright 2018 MovingBlocks
*
* 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.terasology.documentation.apiScraper.util;
import java.util.Objects;
/**
* Saves informations about methods and constructors to be used at the ApiComparator class
*/
public class ApiMethod {
private String className;
private String name;
private String returnType;
private String exceptionType;
private String parametersType;
/**
* @param className Name of the class in which the method can be found
* @param name Name of the method
* @param returnType Return type of the method
* @param exceptionType List of exception types of the method
* @param parametersType List of the method's parameters' type
*/
public ApiMethod(String className, String name, String returnType, String exceptionType, String parametersType) {
this.className = className;
this.name = name;
this.returnType = returnType;
this.exceptionType = exceptionType;
this.parametersType = parametersType;
}
public String getClassName() {
return className;
}
public String getName() {
return name;
}
public String getReturnType() {
return returnType;
}
public String getExceptionType() {
return exceptionType;
}
public String getParametersType() {
return parametersType;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ApiMethod apiMethod = (ApiMethod) o;
return getClassName().equals(apiMethod.getClassName())
&& getName().equals(apiMethod.getName())
&& getReturnType().equals(apiMethod.getReturnType())
&& getExceptionType().equals(apiMethod.getExceptionType())
&& getParametersType().equals(apiMethod.getParametersType());
}
@Override
public int hashCode() {
return Objects.hash();
}
}