diff options
author | Ilpo Ruotsalainen <ilpo.ruotsalainen@oath.com> | 2018-11-27 11:41:58 +0100 |
---|---|---|
committer | Ilpo Ruotsalainen <ilpo.ruotsalainen@oath.com> | 2018-11-28 15:54:45 +0100 |
commit | 809c075a6f082475f0af01145967aa6a4c29aa5f (patch) | |
tree | fb55533b17c2423707d531ab133b96ed18bb7b4b /abi-check-plugin/src/main/java/com/yahoo/abicheck/mojo | |
parent | 1178e3b87a6bf68f9de7a2d9b2907a2d19a88ae0 (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.java | 191 |
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; + } +} |