Skip to content

Commit

Permalink
Separate analyzer code into its own Apilyzer class
Browse files Browse the repository at this point in the history
Create an Apilyzer API that takes a provided PublicApi and analyzes it,
independently of any Maven functionality. Separating the reponsibilities
of different code components makes the code a bit more organized, and
testable.
  • Loading branch information
ctubbsii committed Jul 23, 2024
1 parent a58f0fa commit 4529986
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 186 deletions.
171 changes: 167 additions & 4 deletions src/main/java/net/revelc/code/apilyzer/Apilyzer.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,183 @@

package net.revelc.code.apilyzer;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import net.revelc.code.apilyzer.problems.Problem;
import net.revelc.code.apilyzer.problems.ProblemReporter;
import net.revelc.code.apilyzer.util.ClassUtils;

public class Apilyzer {

private final ProblemReporter problemReporter;
private final PatternSet allowsPs;
private final boolean ignoreDeprecated;
private final PublicApi publicApi;

public Apilyzer(Consumer<Problem> problemConsumer) {
problemReporter = new ProblemReporter(problemConsumer);
/**
* Analyze a given public API definition to ensure it exposes only types available in itself and
* in an allowed set of external APIs.
*/
public Apilyzer(PublicApi publicApi, List<String> allows, boolean ignoreDeprecated,
Consumer<Problem> problemConsumer) {
this.problemReporter = new ProblemReporter(problemConsumer);
this.allowsPs = new PatternSet(allows);
this.ignoreDeprecated = ignoreDeprecated;
this.publicApi = publicApi;
}

public ProblemReporter problemReporter() {
return problemReporter;
private boolean allowedExternalApi(String fqName) {
// TODO make default allows configurable?
if (fqName.startsWith("java.")) {
return true;
}
return allowsPs.anyMatch(fqName);
}

private boolean deprecatedToIgnore(AnnotatedElement element) {
return ignoreDeprecated && element.isAnnotationPresent(Deprecated.class);
}

private boolean isOk(Class<?> clazz) {

while (clazz.isArray()) {
clazz = clazz.getComponentType();
}

if (clazz.isPrimitive()) {
return true;
}

String fqName = clazz.getName();
return publicApi.contains(fqName) || allowedExternalApi(fqName);
}

private boolean checkClass(Class<?> clazz, Set<Class<?>> innerChecked) {

boolean ok = true;

if (deprecatedToIgnore(clazz)) {
return true;
}

// TODO check generic type parameters

for (Field field : ClassUtils.getFields(clazz)) {

if (deprecatedToIgnore(field)) {
continue;
}

if (!field.getDeclaringClass().getName().equals(clazz.getName())
&& isOk(field.getDeclaringClass())) {
continue;
}

if (!isOk(field.getType())) {
problemReporter.field(clazz, field);
ok = false;
}
}

Constructor<?>[] constructors = clazz.getConstructors();
for (Constructor<?> constructor : constructors) {

if (constructor.isSynthetic()) {
continue;
}

if (deprecatedToIgnore(constructor)) {
continue;
}

Class<?>[] params = constructor.getParameterTypes();
for (Class<?> param : params) {
if (!isOk(param)) {
problemReporter.constructorParameter(clazz, param);
ok = false;
}
}

Class<?>[] exceptions = constructor.getExceptionTypes();
for (Class<?> exception : exceptions) {
if (!isOk(exception)) {
problemReporter.constructorException(clazz, exception);
ok = false;
}
}
}

for (Method method : ClassUtils.getMethods(clazz)) {

if (method.isSynthetic() || method.isBridge()) {
continue;
}

if (deprecatedToIgnore(method)) {
continue;
}

if (!method.getDeclaringClass().getName().equals(clazz.getName())
&& isOk(method.getDeclaringClass())) {
continue;
}

if (!isOk(method.getReturnType())) {
problemReporter.methodReturn(clazz, method);
ok = false;
}

Class<?>[] params = method.getParameterTypes();
for (Class<?> param : params) {
if (!isOk(param)) {
problemReporter.methodParameter(clazz, method, param);
ok = false;
}
}

Class<?>[] exceptions = method.getExceptionTypes();
for (Class<?> exception : exceptions) {
if (!isOk(exception)) {
problemReporter.methodException(clazz, method, exception);
ok = false;
}
}
}

for (Class<?> class1 : ClassUtils.getInnerClasses(clazz)) {

if (innerChecked.contains(class1)) {
continue;
}

innerChecked.add(class1);

if (deprecatedToIgnore(class1)) {
continue;
}

if (publicApi.excludes(class1)) {
// this inner class is explicitly excluded from API so do not check it
continue;
}

if (!isOk(class1) && !checkClass(class1, innerChecked)) {
problemReporter.innerClass(clazz, class1);
ok = false;
}
}

return ok;
}

public void check() {
publicApi.classStream().forEach(c -> checkClass(c, new HashSet<Class<?>>()));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,26 @@
* limitations under the License.
*/

package net.revelc.code.apilyzer.maven.plugin;
package net.revelc.code.apilyzer;

import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public class PatternSet {
class PatternSet {
private final List<Pattern> patterns;

public PatternSet(List<String> regexs) {
PatternSet(List<String> regexs) {
patterns = regexs.isEmpty() ? Collections.emptyList()
: regexs.stream().map(Pattern::compile).collect(Collectors.toList());
}

public boolean anyMatch(String input) {
boolean anyMatch(String input) {
return patterns.stream().anyMatch(p -> p.matcher(input).matches());
}

public boolean isEmpty() {
boolean isEmpty() {
return patterns.isEmpty();
}
}
7 changes: 3 additions & 4 deletions src/main/java/net/revelc/code/apilyzer/PublicApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import java.util.List;
import java.util.TreeSet;
import java.util.stream.Stream;
import net.revelc.code.apilyzer.maven.plugin.PatternSet;
import net.revelc.code.apilyzer.util.ClassUtils;

public class PublicApi {
Expand Down Expand Up @@ -107,15 +106,15 @@ private boolean annotationExcludes(Annotation[] annotations) {
.anyMatch(annotation -> excludeAnnotationsPs.anyMatch(annotation.toString()));
}

public Stream<Class<?>> classStream() {
Stream<Class<?>> classStream() {
return publicApiClasses.stream();
}

public boolean contains(String fqName) {
boolean contains(String fqName) {
return publicSet.contains(fqName);
}

public boolean excludes(Class<?> classToCheck) {
boolean excludes(Class<?> classToCheck) {
return excludesPs.anyMatch(classToCheck.getName())
|| annotationExcludes(classToCheck.getDeclaredAnnotations());
}
Expand Down
Loading

0 comments on commit 4529986

Please sign in to comment.