aboutsummaryrefslogtreecommitdiffstats
path: root/abi-check-plugin/src/main/java/com/yahoo/abicheck/mojo
diff options
context:
space:
mode:
authorIlpo Ruotsalainen <ilpo.ruotsalainen@oath.com>2018-11-27 11:41:58 +0100
committerIlpo Ruotsalainen <ilpo.ruotsalainen@oath.com>2018-11-28 15:54:45 +0100
commit809c075a6f082475f0af01145967aa6a4c29aa5f (patch)
treefb55533b17c2423707d531ab133b96ed18bb7b4b /abi-check-plugin/src/main/java/com/yahoo/abicheck/mojo
parent1178e3b87a6bf68f9de7a2d9b2907a2d19a88ae0 (diff)
Initial implementation of ABI checker plugin.
Diffstat (limited to 'abi-check-plugin/src/main/java/com/yahoo/abicheck/mojo')
-rw-r--r--abi-check-plugin/src/main/java/com/yahoo/abicheck/mojo/AbiCheck.java191
1 files changed, 191 insertions, 0 deletions
diff --git a/abi-check-plugin/src/main/java/com/yahoo/abicheck/mojo/AbiCheck.java b/abi-check-plugin/src/main/java/com/yahoo/abicheck/mojo/AbiCheck.java
new file mode 100644
index 00000000000..d1707f4bf28
--- /dev/null
+++ b/abi-check-plugin/src/main/java/com/yahoo/abicheck/mojo/AbiCheck.java
@@ -0,0 +1,191 @@
+package com.yahoo.abicheck.mojo;
+
+import com.google.common.collect.Sets;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
+import com.yahoo.abicheck.classtree.ClassFileTree;
+import com.yahoo.abicheck.classtree.ClassFileTree.Class;
+import com.yahoo.abicheck.classtree.ClassFileTree.Package;
+import com.yahoo.abicheck.collector.AnnotationCollector;
+import com.yahoo.abicheck.collector.PublicSignatureCollector;
+import com.yahoo.abicheck.signature.JavaClassSignature;
+import com.yahoo.abicheck.signature.JavaMethodSignature;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.Predicate;
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.plugins.annotations.ResolutionScope;
+import org.apache.maven.project.MavenProject;
+import org.objectweb.asm.ClassReader;
+
+@Mojo(
+ name = "abicheck",
+ defaultPhase = LifecyclePhase.PACKAGE,
+ requiresDependencyResolution = ResolutionScope.RUNTIME
+)
+public class AbiCheck extends AbstractMojo {
+
+ private static final String DEFAULT_SPEC_FILE = "abi-spec.json";
+ private static final String WRITE_SPEC_PROPERTY = "abicheck.writeSpec";
+ public static final String PACKAGE_INFO_CLASS_FILE_NAME = "package-info.class";
+
+ @Parameter(defaultValue = "${project}", readonly = true)
+ private MavenProject project = null;
+
+ @Parameter(required = true)
+ private String publicApiAnnotation = null;
+
+ @Parameter
+ private String specFileName = DEFAULT_SPEC_FILE;
+
+ @Override
+ public void execute() throws MojoExecutionException, MojoFailureException {
+ Artifact mainArtifact = project.getArtifact();
+ if (mainArtifact.getFile() == null) {
+ throw new MojoExecutionException("Missing project artifact file");
+ } else if (!mainArtifact.getType().equals("jar")) {
+ throw new MojoExecutionException("Project artifact is not a JAR");
+ }
+
+ getLog().debug("Analyzing " + mainArtifact.getFile());
+
+ try {
+ ClassFileTree tree = ClassFileTree.fromJar(mainArtifact.getFile());
+ Map<String, JavaClassSignature> signatures = new LinkedHashMap<>();
+ for (ClassFileTree.Package pkg : tree.getRootPackages()) {
+ signatures.putAll(collectPublicAbiSignatures(pkg));
+ }
+ if (System.getProperty(WRITE_SPEC_PROPERTY) != null) {
+ getLog().info("Writing ABI specs to " + specFileName);
+ writeSpec(signatures);
+ } else {
+ Gson gson = new GsonBuilder().create();
+ try (FileReader reader = new FileReader(specFileName)) {
+ TypeToken<Map<String, JavaClassSignature>> typeToken =
+ new TypeToken<Map<String, JavaClassSignature>>() {
+ };
+ Map<String, JavaClassSignature> abiSpec = gson
+ .fromJson(reader, typeToken.getType());
+ if (!matchingItemSets(abiSpec.keySet(), signatures.keySet(),
+ item -> matchingClasses(item, abiSpec.get(item), signatures.get(item)),
+ (item, error) -> getLog()
+ .error(String.format("%s class: %s", capitalizeFirst(error), item)))) {
+ throw new MojoFailureException("ABI spec mismatch");
+ }
+ }
+ }
+ } catch (IOException e) {
+ throw new MojoExecutionException("Error processing class signatures", e);
+ }
+ }
+
+ private static String capitalizeFirst(String s) {
+ return s.substring(0, 1).toUpperCase() + s.substring(1);
+ }
+
+ private void writeSpec(Map<String, JavaClassSignature> publicAbiSignatures) throws IOException {
+ Gson gson = new GsonBuilder().setPrettyPrinting().create();
+ try (FileWriter writer = new FileWriter(specFileName)) {
+ gson.toJson(publicAbiSignatures, writer);
+ }
+ }
+
+ private static <T> boolean matchingItemSets(Set<T> expected, Set<T> actual,
+ Predicate<T> itemsMatch, BiConsumer<T, String> onError) {
+ boolean mismatch = false;
+ Set<T> missing = Sets.difference(expected, actual);
+ for (T name : missing) {
+ mismatch = true;
+ onError.accept(name, "missing");
+ }
+ Set<T> extra = Sets.difference(actual, expected);
+ for (T name : extra) {
+ mismatch = true;
+ onError.accept(name, "extra");
+ }
+ Set<T> both = Sets.intersection(actual, expected);
+ for (T name : both) {
+ if (!itemsMatch.test(name)) {
+ mismatch = true;
+ }
+ }
+ return !mismatch;
+ }
+
+ private boolean matchingClasses(String className, JavaClassSignature expected,
+ JavaClassSignature actual) {
+ boolean match = true;
+ if (!matchingItemSets(new HashSet<>(expected.attributes), new HashSet<>(actual.attributes),
+ item -> true, (item, error) -> getLog().error(String
+ .format("Class %s: %s attribute %s", className, capitalizeFirst(error), item)))) {
+ match = false;
+ }
+ if (!matchingItemSets(expected.methods.keySet(), actual.methods.keySet(),
+ item -> matchingMethods(className + "." + item, expected.methods.get(item),
+ actual.methods.get(item)),
+ (item, error) -> getLog().error(
+ String.format("Class %s: %s method %s", className, capitalizeFirst(error), item)))) {
+ match = false;
+ }
+ return match;
+ }
+
+ private boolean matchingMethods(String methodName, JavaMethodSignature expected,
+ JavaMethodSignature actual) {
+ boolean match = true;
+ if (!expected.returnType.equals(actual.returnType)) {
+ match = false;
+ getLog().error(String
+ .format("Method %s: Expected return type %s, found %s", methodName, expected.returnType,
+ actual.returnType));
+ }
+ return match;
+ }
+
+ private boolean isPublicAbiPackage(ClassFileTree.Package pkg) throws IOException {
+ Optional<Class> pkgInfo = pkg.getClasses().stream()
+ .filter(klazz -> klazz.getName().equals(PACKAGE_INFO_CLASS_FILE_NAME)).findFirst();
+ if (!pkgInfo.isPresent()) {
+ return false;
+ }
+ try (InputStream is = pkgInfo.get().getInputStream()) {
+ AnnotationCollector visitor = new AnnotationCollector();
+ new ClassReader(is).accept(visitor,
+ ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
+ return visitor.getAnnotations().contains(publicApiAnnotation);
+ }
+ }
+
+ private Map<String, JavaClassSignature> collectPublicAbiSignatures(Package pkg)
+ throws IOException {
+ Map<String, JavaClassSignature> signatures = new LinkedHashMap<>();
+ if (isPublicAbiPackage(pkg)) {
+ PublicSignatureCollector collector = new PublicSignatureCollector();
+ for (ClassFileTree.Class klazz : pkg.getClasses()) {
+ try (InputStream is = klazz.getInputStream()) {
+ new ClassReader(is).accept(collector, 0);
+ }
+ }
+ signatures.putAll(collector.getClassSignatures());
+ }
+ for (ClassFileTree.Package subPkg : pkg.getSubPackages()) {
+ signatures.putAll(collectPublicAbiSignatures(subPkg));
+ }
+ return signatures;
+ }
+}