Skip to content

Commit

Permalink
Organize project into a reusable library
Browse files Browse the repository at this point in the history
* Use `{@code}` in javadocs for maintainability
* Replace some loops with functional code
* Inline some trivial methods
* Create flexible API for reporting type problems
* Create separate ClassUtils for getting class info
* Create separate PublicApi class for defining a public API to be
  analyzed
* Separate analyzer code into its own Apilyzer class 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 24, 2024
1 parent e936a01 commit 91d8de1
Show file tree
Hide file tree
Showing 9 changed files with 750 additions and 454 deletions.
199 changes: 199 additions & 0 deletions src/main/java/net/revelc/code/apilyzer/Apilyzer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/*
* 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
*
* https://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 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;

/**
* The entry point to this library.
*/
public class Apilyzer {

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

/**
* 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;
}

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,38 +12,29 @@
* limitations under the License.
*/

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

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

/**
* A set of patterns to match classes on the class path.
*/
class PatternSet {
private final List<Pattern> patterns;

PatternSet(List<String> regexs) {
if (regexs.size() == 0) {
patterns = Collections.emptyList();
} else {
patterns = new ArrayList<>();
for (String regex : regexs) {
patterns.add(Pattern.compile(regex));
}
}
patterns = regexs.isEmpty() ? Collections.emptyList()
: regexs.stream().map(Pattern::compile).collect(Collectors.toList());
}

boolean matchesAny(String input) {
for (Pattern pattern : patterns) {
if (pattern.matcher(input).matches()) {
return true;
}
}

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

public int size() {
return patterns.size();
boolean isEmpty() {
return patterns.isEmpty();
}
}
Loading

0 comments on commit 91d8de1

Please sign in to comment.