diff --git a/src/main/java/net/revelc/code/apilyzer/Apilyzer.java b/src/main/java/net/revelc/code/apilyzer/Apilyzer.java new file mode 100644 index 0000000..acb4e2c --- /dev/null +++ b/src/main/java/net/revelc/code/apilyzer/Apilyzer.java @@ -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 allows, boolean ignoreDeprecated, + Consumer 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> 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>())); + } + +} diff --git a/src/main/java/net/revelc/code/apilyzer/maven/plugin/PatternSet.java b/src/main/java/net/revelc/code/apilyzer/PatternSet.java similarity index 59% rename from src/main/java/net/revelc/code/apilyzer/maven/plugin/PatternSet.java rename to src/main/java/net/revelc/code/apilyzer/PatternSet.java index 6f8ef4c..6e82333 100644 --- a/src/main/java/net/revelc/code/apilyzer/maven/plugin/PatternSet.java +++ b/src/main/java/net/revelc/code/apilyzer/PatternSet.java @@ -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 patterns; PatternSet(List 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(); } } diff --git a/src/main/java/net/revelc/code/apilyzer/PublicApi.java b/src/main/java/net/revelc/code/apilyzer/PublicApi.java new file mode 100644 index 0000000..102aafd --- /dev/null +++ b/src/main/java/net/revelc/code/apilyzer/PublicApi.java @@ -0,0 +1,159 @@ +/* + * 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 com.google.common.reflect.ClassPath; +import com.google.common.reflect.ClassPath.ClassInfo; +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.TreeSet; +import java.util.stream.Stream; +import net.revelc.code.apilyzer.util.ClassUtils; + +/** + * An object representing the public API for the analysis target. + */ +public class PublicApi { + + /** + * Construct a public API definition object from a class path object and includes/excludes. + */ + public static PublicApi fromClassPath(ClassPath classPath, List includes, + List excludes, List includeAnnotations, List excludeAnnotations) { + PublicApi api = new PublicApi(includes, excludes, includeAnnotations, excludeAnnotations); + + classLoop: for (ClassInfo classInfo : classPath.getAllClasses()) { + + // Do this check before possibly attempting any annotation checks as these require class + // loading. If the class is excluded by a pattern, then no need to load class. + if (api.excludes(classInfo)) { + continue; + } + + Annotation[] annotations = api.getAnnotations(classInfo); + for (Annotation annotation : annotations) { + if (api.includes(annotation)) { + if (!api.annotationExcludes(annotations)) { + api.addPublicApiType(classInfo); + } + continue classLoop; + } + } + + if (api.includes(classInfo) && !api.annotationExcludes(annotations)) { + api.addPublicApiType(classInfo); + } + } + return api; + } + + private PatternSet includesPs; + private PatternSet excludesPs; + private PatternSet includeAnnotationsPs; + private PatternSet excludeAnnotationsPs; + private final List> publicApiClasses = new ArrayList<>(); + private final TreeSet publicSet = new TreeSet<>(); + + private PublicApi(List includes, List excludes, List includeAnnotations, + List excludeAnnotations) { + this.includesPs = new PatternSet(includes); + this.includeAnnotationsPs = new PatternSet(includeAnnotations); + this.excludesPs = new PatternSet(excludes); + this.excludeAnnotationsPs = new PatternSet(excludeAnnotations); + } + + private void addPublicApiType(ClassInfo classInfo) { + Class clazz = classInfo.load(); + if (ClassUtils.isPublicOrProtected(clazz) && !publicSet.contains(clazz.getName())) { + publicApiClasses.add(clazz); + publicSet.add(clazz.getName()); + + addPublicInnerClasses(publicApiClasses, publicSet, clazz); + } + } + + private void addPublicInnerClasses(List> publicApiClasses, TreeSet publicSet, + Class clazz) { + + Class[] innerClasses = clazz.getDeclaredClasses(); + for (Class ic : innerClasses) { + // If a class is in the Public API then all of its public inner class are also considered + // to be in the public API unless explicitly excluded. + if (ClassUtils.isPublicOrProtected(ic) && !publicSet.contains(ic.getName()) + && !annotationExcludes(ic.getDeclaredAnnotations()) + && !excludesPs.anyMatch(ic.getName())) { + publicApiClasses.add(ic); + publicSet.add(ic.getName()); + + addPublicInnerClasses(publicApiClasses, publicSet, ic); + } + } + } + + private static String formatAnnotation(Annotation annotation) { + return "@" + annotation.annotationType().getName(); + } + + private boolean annotationExcludes(Annotation[] annotations) { + return !excludeAnnotationsPs.isEmpty() && Arrays.stream(annotations) + .anyMatch(annotation -> excludeAnnotationsPs.anyMatch(formatAnnotation(annotation))); + } + + Stream> classStream() { + return publicApiClasses.stream(); + } + + boolean contains(String fqName) { + return publicSet.contains(fqName); + } + + boolean excludes(Class classToCheck) { + return excludesPs.anyMatch(classToCheck.getName()) + || annotationExcludes(classToCheck.getDeclaredAnnotations()); + } + + private boolean excludes(ClassInfo classInfo) { + return excludesPs.anyMatch(classInfo.getName()); + } + + private Annotation[] getAnnotations(ClassInfo classInfo) { + if (includeAnnotationsPs.isEmpty() && excludeAnnotationsPs.isEmpty()) { + return new Annotation[0]; + } + // ignore annotations from java itself, to avoid ClassNotFoundExceptions + String name = classInfo.getName(); + return (name.startsWith("com.sun") || name.startsWith("java.")) ? new Annotation[0] + : classInfo.load().getDeclaredAnnotations(); + } + + private boolean includes(Annotation annotation) { + return includeAnnotationsPs.anyMatch(formatAnnotation(annotation)); + } + + private boolean includes(ClassInfo classInfo) { + return includesPs.anyMatch(classInfo.getName()); + } + + public boolean isEmpty() { + return publicSet.isEmpty(); + } + + public Stream nameStream() { + return publicSet.stream(); + } + +} diff --git a/src/main/java/net/revelc/code/apilyzer/maven/plugin/AnalyzeMojo.java b/src/main/java/net/revelc/code/apilyzer/maven/plugin/AnalyzeMojo.java index 251392c..7f36707 100644 --- a/src/main/java/net/revelc/code/apilyzer/maven/plugin/AnalyzeMojo.java +++ b/src/main/java/net/revelc/code/apilyzer/maven/plugin/AnalyzeMojo.java @@ -15,29 +15,18 @@ package net.revelc.code.apilyzer.maven.plugin; import com.google.common.reflect.ClassPath; -import com.google.common.reflect.ClassPath.ClassInfo; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintStream; -import java.lang.annotation.Annotation; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; import java.util.List; -import java.util.Set; -import java.util.TreeSet; import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Function; +import java.util.function.Consumer; +import net.revelc.code.apilyzer.Apilyzer; +import net.revelc.code.apilyzer.PublicApi; +import net.revelc.code.apilyzer.problems.Problem; +import net.revelc.code.apilyzer.util.ClassUtils; import org.apache.maven.artifact.DependencyResolutionRequiredException; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; @@ -49,7 +38,7 @@ import org.apache.maven.project.MavenProject; /** - * Analyzes declared public API. + * Analyzes declared public API in a Maven build. */ @Mojo(name = "analyze", defaultPhase = LifecyclePhase.VERIFY, requiresDependencyResolution = ResolutionScope.COMPILE, threadSafe = true) @@ -73,26 +62,27 @@ public class AnalyzeMojo extends AbstractMojo { * protected inner classes are added to the public API definition. If you do not wish for a * particular inner class to be in the public API then you can add a more specific exclusion for * it. For example could include {@code com.foo.C} and exclude {@code com.foo.C$I1} if the inner - * class {@code C$I1} ends up in the API when its not wanted. + * class {@code C$I1} ends up in the API when it's not wanted. * *

Example: * *

-   * <configuration>
-   *   ...
-   *   <includes>
-   *     <include>org[.]apache[.].*</include>
-   *     <include>com[.]example[.]myproject[.].*</include>
-   *   </includes>
-   *   ...
-   * </configuration>
+   * {@code
+   * 
+   *   ...
+   *   
+   *     org[.]apache[.].*
+   *     com[.]example[.]myproject[.].*
+   *   
+   *   ...
+   * 
+   * }
    * 
* * @since 1.0.0 */ @Parameter(alias = "includes") private List includes = Collections.emptyList(); - private PatternSet includesPs; /** * The classes to exclude from your public API definition, which may have otherwise matched your @@ -101,20 +91,21 @@ public class AnalyzeMojo extends AbstractMojo { *

Example: * *

-   * <configuration>
-   *   ...
-   *   <excludes>
-   *     <exclude>.*[.]impl[.].*</exclude>
-   *   </excludes>
-   *   ...
-   * </configuration>
+   * {@code
+   * 
+   *   ...
+   *   
+   *     .*[.]impl[.].*
+   *   
+   *   ...
+   * 
+   * }
    * 
* * @since 1.0.0 */ @Parameter(alias = "excludes") private List excludes = Collections.emptyList(); - private PatternSet excludesPs; /** * The additional classes, which are allowed to be referenced in your public API, but are not, @@ -126,20 +117,21 @@ public class AnalyzeMojo extends AbstractMojo { *

Example: * *

-   * <configuration>
-   *   ...
-   *   <allows>
-   *     <allow>com[.]google[.]common[.].*</allow>
-   *   </allows>
-   *   ...
-   * </configuration>
+   * {@code
+   * 
+   *   ...
+   *   
+   *     com[.]google[.]common[.].*
+   *   
+   *   ...
+   * 
+   * }
    * 
* * @since 1.0.0 */ @Parameter(alias = "allows") private List allows = Collections.emptyList(); - private PatternSet allowsPs; /** * Allows skipping execution of this plugin. This may be useful for testing, or if you find that @@ -198,22 +190,21 @@ public class AnalyzeMojo extends AbstractMojo { *

Example: * *

-   * <configuration>
-   *   ....
-   *   <includeAnnotations>
-   *     <include>
-   *       [@]com[.]proj42[.]Public.*
-   *     </include>
-   *   </includeAnnotations>
-   *   .....
-   * </configuration>
+   * {@code
+   * 
+   *   ...
+   *   
+   *     [@]com[.]proj42[.]Public.*
+   *   
+   *   ...
+   * 
+   * }
    * 
* * @since 1.1.0 */ @Parameter(alias = "includeAnnotations") private List includeAnnotations = Collections.emptyList(); - private PatternSet includeAnnotationsPs; /** * Exclude classes from public API definition using annotation. @@ -221,15 +212,15 @@ public class AnalyzeMojo extends AbstractMojo { *

Example: * *

-   * <configuration>
-   *   ....
-   *   <excludeAnnotations>
-   *     <exclude>
-   *       [@]com[.]proj42[.]Alpha.*
-   *     </exclude>
-   *   </excludeAnnotations>
-   *   .....
-   * </configuration>
+   * {@code
+   * 
+   *   ...
+   *   
+   *     [@]com[.]proj42[.]Alpha.*
+   *   
+   *   ...
+   * 
+   * }
    * 
* * @see AnalyzeMojo#includeAnnotations @@ -237,21 +228,12 @@ public class AnalyzeMojo extends AbstractMojo { */ @Parameter(alias = "excludeAnnotations") private List excludeAnnotations = Collections.emptyList(); - private PatternSet excludeAnnotationsPs; - private static final String FORMAT = " %-20s %-60s %-35s %s\n"; + private static final String FORMAT = " %-20s %-60s %-35s %s%n"; @Override public void execute() throws MojoFailureException, MojoExecutionException { - includesPs = new PatternSet(includes); - excludesPs = new PatternSet(excludes); - includeAnnotationsPs = new PatternSet(includeAnnotations); - excludeAnnotationsPs = new PatternSet(excludeAnnotations); - allowsPs = new PatternSet(allows); - - AtomicLong counter = new AtomicLong(0); - if (skip) { getLog().info("APILyzer execution skipped"); return; @@ -259,7 +241,7 @@ public void execute() throws MojoFailureException, MojoExecutionException { ClassPath classPath; try { - classPath = getClassPath(); + classPath = ClassUtils.getClassPath(project.getCompileClasspathElements()); } catch (IOException | DependencyResolutionRequiredException | IllegalArgumentException e) { throw new MojoExecutionException("Error resolving project classpath", e); } @@ -272,43 +254,45 @@ public void execute() throws MojoFailureException, MojoExecutionException { out.println("Excludes: " + excludes); out.println("Allowed: " + allows); - List> publicApiClasses = new ArrayList<>(); - TreeSet publicSet = new TreeSet<>(); - buildPublicSet(classPath, publicApiClasses, publicSet); + PublicApi publicApi = PublicApi.fromClassPath(classPath, includes, excludes, + includeAnnotations, excludeAnnotations); - if (publicSet.size() == 0) { + if (publicApi.isEmpty()) { throw new MojoExecutionException("No public API types were matched"); } out.println(); out.println("Public API:"); - for (String item : publicSet) { - out.println(" " + item); - } - + publicApi.nameStream().map(item -> " " + item).forEach(out::println); out.println(); out.println("Problems : "); - out.println(); out.printf(FORMAT, "CONTEXT", "TYPE", "FIELD/METHOD", "NON-PUBLIC REFERENCE"); - out.println(); + + AtomicLong problemCounter = new AtomicLong(0); + // look for public API methods/fields/subclasses that use classes not in public API - for (Class clazz : publicApiClasses) { - checkClass(clazz, publicSet, out, counter); - } + Consumer problemConsumer = problem -> { + problemCounter.incrementAndGet(); + out.printf(FORMAT, problem.problemType, problem.contextClass.getName(), problem.memberName, + problem.nonPublicType.getName()); + }; + new Apilyzer(publicApi, allows, ignoreDeprecated, problemConsumer).check(); + + long problemCount = problemCounter.get(); out.println(); - out.println("Total : " + counter.get()); + out.println("Total : " + problemCount); String msg = - "APILyzer found " + counter.get() + " problem" + (counter.get() == 1 ? "" : "s") + "."; + "APILyzer found " + problemCount + " problem" + (problemCount == 1 ? "" : "s") + "."; msg += " See " + outputFile + " for details."; - if (counter.get() < 0) { + if (problemCount < 0) { throw new AssertionError("Inconceivable!"); - } else if (counter.get() == 0) { + } else if (problemCount == 0) { getLog().info(msg); - } else if (counter.get() > 0 && ignoreProblems) { + } else if (problemCount > 0 && ignoreProblems) { getLog().warn(msg); } else { getLog().error(msg); @@ -319,347 +303,4 @@ public void execute() throws MojoFailureException, MojoExecutionException { } } - private static enum ProblemType { - INNER_CLASS, METHOD_PARAM, METHOD_RETURN, FIELD, CTOR_PARAM, CTOR_EXCEPTION, METHOD_EXCEPTION - } - - private static final Function TO_URL = item -> { - URI uri = new File(item).toURI(); - try { - return uri.toURL(); - } catch (MalformedURLException e) { - throw new IllegalArgumentException("Unable to convert string (" + item + ") to URL", e); - } - }; - - private ClassPath getClassPath() throws DependencyResolutionRequiredException, IOException { - URL[] urls = project.getCompileClasspathElements().stream().map(TO_URL).toArray(URL[]::new); - return ClassPath.from(new URLClassLoader(urls, null)); - } - - private Annotation[] getAnnotations(ClassInfo classInfo) { - if (classInfo.getName().startsWith("com.sun") || classInfo.getName().startsWith("java.")) { - // was getting class not found exceptions when trying to get annotations for com.sun class... - return new Annotation[0]; - } - - return getAnnotations(classInfo.load()); - } - - private Annotation[] getAnnotations(Class clazz) { - return clazz.getDeclaredAnnotations(); - } - - private static String formatAnnotation(Annotation annotation) { - return "@" + annotation.annotationType().getName(); - } - - /** - * Builds the set of Types that are in the public API. - */ - private void buildPublicSet(ClassPath classPath, List> publicApiClasses, - TreeSet publicSet) { - classLoop: for (ClassInfo classInfo : classPath.getAllClasses()) { - - // Do this check before possibly attempting any annotation checks as these require class - // loading. If the class is excluded by a pattern, then no need to load class. - if (patternExcludes(classInfo)) { - continue; - } - - Annotation[] annotations; - if (includeAnnotationsPs.size() > 0 || excludeAnnotationsPs.size() > 0) { - annotations = getAnnotations(classInfo); - } else { - annotations = new Annotation[0]; - } - - for (Annotation annotation : annotations) { - if (includeAnnotationsPs.matchesAny(formatAnnotation(annotation))) { - if (!annotationExcludes(annotations)) { - addPublicApiType(publicApiClasses, publicSet, classInfo); - } - continue classLoop; - } - } - - if (includesPs.matchesAny(classInfo.getName()) && !annotationExcludes(annotations)) { - addPublicApiType(publicApiClasses, publicSet, classInfo); - } - } - } - - private void addPublicApiType(List> publicApiClasses, TreeSet publicSet, - ClassInfo classInfo) { - Class clazz = classInfo.load(); - if (isPublicOrProtected(clazz) && !publicSet.contains(clazz.getName())) { - publicApiClasses.add(clazz); - publicSet.add(clazz.getName()); - - addPublicInnerClasses(publicApiClasses, publicSet, clazz); - } - } - - private void addPublicInnerClasses(List> publicApiClasses, TreeSet publicSet, - Class clazz) { - - Class[] innerClasses = clazz.getDeclaredClasses(); - for (Class ic : innerClasses) { - // If a class is in the Public API then all of its public inner class are also considered - // to be in the public API unless explicitly excluded. - if (isPublicOrProtected(ic) && !publicSet.contains(ic.getName()) && !annotationExcludes(ic) - && !patternExcludes(ic)) { - publicApiClasses.add(ic); - publicSet.add(ic.getName()); - - addPublicInnerClasses(publicApiClasses, publicSet, ic); - } - } - } - - private boolean patternExcludes(ClassInfo classInfo) { - return excludesPs.matchesAny(classInfo.getName()); - } - - private boolean patternExcludes(Class clazz) { - return excludesPs.matchesAny(clazz.getName()); - } - - private boolean annotationExcludes(Annotation[] annotations) { - if (excludeAnnotationsPs.size() == 0) { - return false; - } - - for (Annotation annotation : annotations) { - if (excludeAnnotationsPs.matchesAny(formatAnnotation(annotation))) { - return true; - } - } - - return false; - } - - private boolean annotationExcludes(Class clazz) { - if (excludeAnnotationsPs.size() == 0) { - return false; - } - - Annotation[] annotations = getAnnotations(clazz); - - for (Annotation annotation : annotations) { - if (excludeAnnotationsPs.matchesAny(formatAnnotation(annotation))) { - return true; - } - } - - return false; - } - - private boolean isOk(Set publicSet, Class clazz) { - - while (clazz.isArray()) { - clazz = clazz.getComponentType(); - } - - if (clazz.isPrimitive()) { - return true; - } - - String fqName = clazz.getName(); - - if (publicSet.contains(fqName)) { - return true; - } - - // TODO make default allows configurable - if (fqName.startsWith("java.")) { - return true; - } - - if (allowsPs.matchesAny(fqName)) { - return true; - } - - return false; - } - - // get public and protected fields - private List getFields(Class clazz) { - ArrayList fields = new ArrayList<>(Arrays.asList(clazz.getFields())); - - // TODO need to get superclasses protected fields, deduping on name - for (Field f : clazz.getDeclaredFields()) { - if ((f.getModifiers() & Modifier.PROTECTED) != 0) { - fields.add(f); - } - } - - return fields; - } - - // get public and protected methods - private List getMethods(Class clazz) { - ArrayList methods = new ArrayList<>(Arrays.asList(clazz.getMethods())); - - // TODO need to get superlclasses protected methods, deduping on signature - for (Method m : clazz.getDeclaredMethods()) { - if ((m.getModifiers() & Modifier.PROTECTED) != 0) { - methods.add(m); - } - } - - return methods; - } - - private List> getInnerClasses(Class clazz) { - ArrayList> classes = new ArrayList<>(Arrays.asList(clazz.getClasses())); - - // TODO need to get superclasses' protected classes, deduping on name - for (Class c : clazz.getDeclaredClasses()) { - if ((c.getModifiers() & Modifier.PROTECTED) != 0) { - classes.add(c); - } - } - - return classes; - } - - private boolean checkClass(Class clazz, Set publicSet, PrintStream out, - AtomicLong counter) { - return checkClass(clazz, publicSet, out, counter, new HashSet>()); - } - - private boolean checkClass(Class clazz, Set publicSet, PrintStream out, - AtomicLong counter, Set> innerChecked) { - - boolean ok = true; - - // TODO make configurable - if (ignoreDeprecated && clazz.isAnnotationPresent(Deprecated.class)) { - return true; - } - - // TODO check generic type parameters - - for (Field field : getFields(clazz)) { - - if (ignoreDeprecated && field.isAnnotationPresent(Deprecated.class)) { - continue; - } - - if (!field.getDeclaringClass().getName().equals(clazz.getName()) - && isOk(publicSet, field.getDeclaringClass())) { - continue; - } - - if (!isOk(publicSet, field.getType())) { - problem(out, counter, ProblemType.FIELD, clazz, field.getName(), field.getType().getName()); - ok = false; - } - } - - Constructor[] constructors = clazz.getConstructors(); - for (Constructor constructor : constructors) { - - if (constructor.isSynthetic()) { - continue; - } - - if (ignoreDeprecated && constructor.isAnnotationPresent(Deprecated.class)) { - continue; - } - - Class[] params = constructor.getParameterTypes(); - for (Class param : params) { - if (!isOk(publicSet, param)) { - problem(out, counter, ProblemType.CTOR_PARAM, clazz, "(...)", param.getName()); - ok = false; - } - } - - Class[] exceptions = constructor.getExceptionTypes(); - for (Class exception : exceptions) { - if (!isOk(publicSet, exception)) { - problem(out, counter, ProblemType.CTOR_EXCEPTION, clazz, "(...) throws", - exception.getName()); - ok = false; - } - } - } - - for (Method method : getMethods(clazz)) { - - if (method.isSynthetic() || method.isBridge()) { - continue; - } - - if (ignoreDeprecated && method.isAnnotationPresent(Deprecated.class)) { - continue; - } - - if (!method.getDeclaringClass().getName().equals(clazz.getName()) - && isOk(publicSet, method.getDeclaringClass())) { - continue; - } - - if (!isOk(publicSet, method.getReturnType())) { - problem(out, counter, ProblemType.METHOD_RETURN, clazz, method.getName() + "(...)", - method.getReturnType().getName()); - ok = false; - } - - Class[] params = method.getParameterTypes(); - for (Class param : params) { - if (!isOk(publicSet, param)) { - problem(out, counter, ProblemType.METHOD_PARAM, clazz, method.getName() + "(...)", - param.getName()); - ok = false; - } - } - - Class[] exceptions = method.getExceptionTypes(); - for (Class exception : exceptions) { - if (!isOk(publicSet, exception)) { - problem(out, counter, ProblemType.METHOD_EXCEPTION, clazz, - method.getName() + "(...) throws", exception.getName()); - ok = false; - } - } - } - - for (Class class1 : getInnerClasses(clazz)) { - - if (innerChecked.contains(class1)) { - continue; - } - - innerChecked.add(class1); - - if (ignoreDeprecated && class1.isAnnotationPresent(Deprecated.class)) { - continue; - } - - if (patternExcludes(class1) || annotationExcludes(class1)) { - // this inner class is explicitly excluded from API so do not check it - continue; - } - - if (!isOk(publicSet, class1) && !checkClass(class1, publicSet, out, counter, innerChecked)) { - problem(out, counter, ProblemType.INNER_CLASS, clazz, "N/A", class1.getName()); - ok = false; - } - } - - return ok; - } - - private boolean isPublicOrProtected(Class clazz) { - return (clazz.getModifiers() & (Modifier.PUBLIC | Modifier.PROTECTED)) != 0; - } - - private void problem(PrintStream out, AtomicLong counter, ProblemType type, Class clazz, - String member, String problemRef) { - counter.incrementAndGet(); - out.printf(FORMAT, type, clazz.getName(), member, problemRef); - } } diff --git a/src/main/java/net/revelc/code/apilyzer/problems/Problem.java b/src/main/java/net/revelc/code/apilyzer/problems/Problem.java new file mode 100644 index 0000000..9bd95c1 --- /dev/null +++ b/src/main/java/net/revelc/code/apilyzer/problems/Problem.java @@ -0,0 +1,35 @@ +/* + * 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.problems; + +/** + * An object type that represents a problem to be reported. + */ +public class Problem { + + public final String problemType; + public final Class contextClass; + public final String memberName; + public final Class nonPublicType; + + Problem(ProblemType problemType, Class contextClass, String memberName, + Class nonPublicType) { + this.problemType = problemType.name(); + this.contextClass = contextClass; + this.memberName = memberName; + this.nonPublicType = nonPublicType; + } + +} diff --git a/src/main/java/net/revelc/code/apilyzer/problems/ProblemReporter.java b/src/main/java/net/revelc/code/apilyzer/problems/ProblemReporter.java new file mode 100644 index 0000000..235042b --- /dev/null +++ b/src/main/java/net/revelc/code/apilyzer/problems/ProblemReporter.java @@ -0,0 +1,92 @@ +/* + * 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.problems; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.function.Consumer; + +/** + * A utility for reporting the various problem types. + */ +public class ProblemReporter { + + private Consumer consumer; + + public ProblemReporter(Consumer consumer) { + this.consumer = consumer; + } + + /** + * Report a problem with a constructor exception's type. + */ + public void constructorException(Class contextClass, Class nonPublicException) { + Problem p = + new Problem(ProblemType.CTOR_EXCEPTION, contextClass, "(...) throws", nonPublicException); + consumer.accept(p); + } + + /** + * Report a problem with a constructor parameter's type. + */ + public void constructorParameter(Class contextClass, Class nonPublicParam) { + Problem p = new Problem(ProblemType.CTOR_PARAM, contextClass, "(...)", nonPublicParam); + consumer.accept(p); + } + + /** + * Report a problem with a field's type. + */ + public void field(Class contextClass, Field field) { + Problem p = new Problem(ProblemType.FIELD, contextClass, field.getName(), field.getType()); + consumer.accept(p); + } + + /** + * Report a problem within an inner class. + */ + public void innerClass(Class contextClass, Class nonPublicType) { + Problem p = new Problem(ProblemType.INNER_CLASS, contextClass, "N/A", nonPublicType); + consumer.accept(p); + } + + /** + * Report a problem with a method's exception type. + */ + public void methodException(Class contextClass, Method method, Class nonPublicException) { + Problem p = new Problem(ProblemType.METHOD_EXCEPTION, contextClass, + method.getName() + "(...) throws", nonPublicException.getClass()); + consumer.accept(p); + } + + /** + * Report a problem with a method parameter's type. + */ + public void methodParameter(Class contextClass, Method method, Class nonPublicParam) { + Problem p = new Problem(ProblemType.METHOD_PARAM, contextClass, method.getName() + "(...)", + nonPublicParam); + consumer.accept(p); + } + + /** + * Report a problem with a method's return type. + */ + public void methodReturn(Class contextClass, Method method) { + Problem p = new Problem(ProblemType.METHOD_RETURN, contextClass, method.getName() + "(...)", + method.getReturnType()); + consumer.accept(p); + } + +} diff --git a/src/main/java/net/revelc/code/apilyzer/problems/ProblemType.java b/src/main/java/net/revelc/code/apilyzer/problems/ProblemType.java new file mode 100644 index 0000000..d32034e --- /dev/null +++ b/src/main/java/net/revelc/code/apilyzer/problems/ProblemType.java @@ -0,0 +1,57 @@ +/* + * 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.problems; + +/** + * An enumeration of problem types that can be identified and reported. + */ +public enum ProblemType { + + /** + * A non-public API type was found while checking an inner class. + */ + INNER_CLASS, + + /** + * A method parameter's type isn't in the public API or in the allowed set. + */ + METHOD_PARAM, + + /** + * A method's return type isn't in the public API or in the allowed set. + */ + METHOD_RETURN, + + /** + * A field's type isn't in the public API or in the allowed set. + */ + FIELD, + + /** + * A constructor parameter's type isn't in the public API or in the allowed set. + */ + CTOR_PARAM, + + /** + * A constructor exception's type isn't in the public API or in the allowed set. + */ + CTOR_EXCEPTION, + + /** + * A method exception's type isn't in the public API or in the allowed set. + */ + METHOD_EXCEPTION + +} diff --git a/src/main/java/net/revelc/code/apilyzer/util/ClassUtils.java b/src/main/java/net/revelc/code/apilyzer/util/ClassUtils.java new file mode 100644 index 0000000..a9d2bb4 --- /dev/null +++ b/src/main/java/net/revelc/code/apilyzer/util/ClassUtils.java @@ -0,0 +1,100 @@ +/* + * 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.util; + +import com.google.common.reflect.ClassPath; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Some basic static utilities for searching and processing the class path. + */ +public class ClassUtils { + + private ClassUtils() { + // do not permit instantiation + } + + private static final Function TO_URL = item -> { + URI uri = new File(item).toURI(); + try { + return uri.toURL(); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Unable to convert string (" + item + ") to URL", e); + } + }; + + /** + * Construct a class path object from a list of local file system paths. + */ + public static ClassPath getClassPath(List paths) throws IOException { + URL[] urls = paths.stream().map(TO_URL).toArray(URL[]::new); + return ClassPath.from(new URLClassLoader(urls, null)); + } + + public static boolean isPublicOrProtected(Class clazz) { + return (clazz.getModifiers() & (Modifier.PUBLIC | Modifier.PROTECTED)) != 0; + } + + /** + * Get all inner classes and interfaces that are public (including inherited) or protected + * (currently, doesn't include inherited). + */ + public static List> getInnerClasses(Class clazz) { + // TODO need to also get inherited protected classes, deduping on name + + Stream> publicInners = Arrays.stream(clazz.getClasses()); + Stream> protectedInners = Arrays.stream(clazz.getDeclaredClasses()) + .filter(c -> Modifier.isProtected(c.getModifiers())); + return Stream.concat(publicInners, protectedInners).collect(Collectors.toList()); + } + + /** + * Get all public (including inherited) and protected (currently, excluding inherited) fields. + */ + public static List getFields(Class clazz) { + // TODO need to also get inherited protected fields, deduping on name + + Stream publicFields = Arrays.stream(clazz.getFields()); + Stream protectedFields = Arrays.stream(clazz.getDeclaredFields()) + .filter(f -> Modifier.isProtected(f.getModifiers())); + return Stream.concat(publicFields, protectedFields).collect(Collectors.toList()); + } + + /** + * Get all public (including inherited) and protected (currently, excluding inherited) methods. + */ + public static List getMethods(Class clazz) { + // TODO need to also get inherited protected methods, deduping on signature + + Stream publicFields = Arrays.stream(clazz.getMethods()); + Stream protectedFields = Arrays.stream(clazz.getDeclaredMethods()) + .filter(m -> Modifier.isProtected(m.getModifiers())); + return Stream.concat(publicFields, protectedFields).collect(Collectors.toList()); + } + +} diff --git a/src/test/java/net/revelc/code/apilyzer/maven/plugin/AnalyzeMojoTest.java b/src/test/java/net/revelc/code/apilyzer/maven/plugin/AnalyzeMojoTest.java new file mode 100644 index 0000000..b731153 --- /dev/null +++ b/src/test/java/net/revelc/code/apilyzer/maven/plugin/AnalyzeMojoTest.java @@ -0,0 +1,22 @@ +/* + * 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.maven.plugin; + +/** + * A basic test template. + */ +public class AnalyzeMojoTest { + +}