summaryrefslogtreecommitdiffstats
path: root/abi-check-plugin
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
parent1178e3b87a6bf68f9de7a2d9b2907a2d19a88ae0 (diff)
Initial implementation of ABI checker plugin.
Diffstat (limited to 'abi-check-plugin')
-rw-r--r--abi-check-plugin/pom.xml67
-rw-r--r--abi-check-plugin/src/main/java/com/yahoo/abicheck/classtree/ClassFileTree.java123
-rw-r--r--abi-check-plugin/src/main/java/com/yahoo/abicheck/collector/AnnotationCollector.java27
-rw-r--r--abi-check-plugin/src/main/java/com/yahoo/abicheck/collector/PublicSignatureCollector.java85
-rw-r--r--abi-check-plugin/src/main/java/com/yahoo/abicheck/collector/Util.java68
-rw-r--r--abi-check-plugin/src/main/java/com/yahoo/abicheck/mojo/AbiCheck.java191
-rw-r--r--abi-check-plugin/src/main/java/com/yahoo/abicheck/signature/JavaClassSignature.java17
-rw-r--r--abi-check-plugin/src/main/java/com/yahoo/abicheck/signature/JavaMethodSignature.java14
8 files changed, 592 insertions, 0 deletions
diff --git a/abi-check-plugin/pom.xml b/abi-check-plugin/pom.xml
new file mode 100644
index 00000000000..5e3a13629f2
--- /dev/null
+++ b/abi-check-plugin/pom.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0"?>
+<!-- Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>parent</artifactId>
+ <version>6-SNAPSHOT</version>
+ <relativePath>../parent/pom.xml</relativePath>
+ </parent>
+ <artifactId>abi-check-plugin</artifactId>
+ <version>6-SNAPSHOT</version>
+ <packaging>maven-plugin</packaging>
+ <name>${project.artifactId}</name>
+ <description>Maven Plugin for ensuring ABI stability.</description>
+ <prerequisites>
+ <maven>2.2.1</maven>
+ </prerequisites>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.maven</groupId>
+ <artifactId>maven-plugin-api</artifactId>
+ <version>3.5.0</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven.plugin-tools</groupId>
+ <artifactId>maven-plugin-annotations</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven.reporting</groupId>
+ <artifactId>maven-reporting-impl</artifactId>
+ <version>3.0.0</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven.reporting</groupId>
+ <artifactId>maven-reporting-api</artifactId>
+ <version>3.0</version>
+ </dependency>
+ <dependency>
+ <groupId>org.ow2.asm</groupId>
+ <artifactId>asm</artifactId>
+ <version>7.0</version>
+ </dependency>
+ <dependency>
+ <groupId>com.google.code.gson</groupId>
+ <artifactId>gson</artifactId>
+ <version>2.8.5</version>
+ </dependency>
+ </dependencies>
+ <build>
+ <finalName>${project.artifactId}</finalName>
+ <resources>
+ <resource>
+ <directory>src/main/resources</directory>
+ <filtering>true</filtering>
+ </resource>
+ </resources>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-plugin-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/abi-check-plugin/src/main/java/com/yahoo/abicheck/classtree/ClassFileTree.java b/abi-check-plugin/src/main/java/com/yahoo/abicheck/classtree/ClassFileTree.java
new file mode 100644
index 00000000000..8a2e6d609de
--- /dev/null
+++ b/abi-check-plugin/src/main/java/com/yahoo/abicheck/classtree/ClassFileTree.java
@@ -0,0 +1,123 @@
+package com.yahoo.abicheck.classtree;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Deque;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+public abstract class ClassFileTree implements AutoCloseable {
+
+ public static ClassFileTree fromJar(File file) throws IOException {
+ Map<String, Package> rootPackages = new HashMap<>();
+ JarFile jarFile = new JarFile(file);
+
+ Enumeration<JarEntry> jarEntries = jarFile.entries();
+ while (jarEntries.hasMoreElements()) {
+ JarEntry entry = jarEntries.nextElement();
+ if (!entry.isDirectory() && entry.getName().endsWith(".class")) {
+ Deque<String> parts = new ArrayDeque<>(Arrays.asList(entry.getName().split("/")));
+ String className = parts.removeLast();
+ Package pkg = rootPackages
+ .computeIfAbsent(parts.removeFirst(), name -> new Package(null, name));
+ for (String part : parts) {
+ pkg = pkg.getOrCreateSubPackage(part);
+ }
+ pkg.addClass(new Class(pkg, className) {
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ return jarFile.getInputStream(entry);
+ }
+ });
+ }
+ }
+
+ return new ClassFileTree() {
+ @Override
+ public Collection<Package> getRootPackages() {
+ return rootPackages.values();
+ }
+
+ @Override
+ public void close() throws IOException {
+ jarFile.close();
+ }
+ };
+ }
+
+ public abstract Collection<Package> getRootPackages();
+
+ public static abstract class Class {
+
+ private final Package parent;
+ private final String name;
+
+ private Class(Package parent, String name) {
+ this.parent = parent;
+ this.name = name;
+ }
+
+ public abstract InputStream getInputStream() throws IOException;
+
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String toString() {
+ return "Class(" + parent.getFullyQualifiedName() + "." + name + ")";
+ }
+ }
+
+ public static class Package {
+
+ private final Package parent;
+ private final String name;
+ private final Map<String, Package> subPackages = new HashMap<>();
+ private final Set<Class> classes = new HashSet<>();
+
+ private Package(Package parent, String name) {
+ this.parent = parent;
+ this.name = name;
+ }
+
+ private Package getOrCreateSubPackage(String name) {
+ return subPackages.computeIfAbsent(name, n -> new Package(this, n));
+ }
+
+ private void addClass(Class klazz) {
+ classes.add(klazz);
+ }
+
+ public String getFullyQualifiedName() {
+ if (parent == null) {
+ return name;
+ } else {
+ return parent.getFullyQualifiedName() + "." + name;
+ }
+ }
+
+ public Collection<Package> getSubPackages() {
+ return subPackages.values();
+ }
+
+ public Collection<Class> getClasses() {
+ return classes;
+ }
+
+ @Override
+ public String toString() {
+ return "Package(" + getFullyQualifiedName() + ")";
+ }
+ }
+}
diff --git a/abi-check-plugin/src/main/java/com/yahoo/abicheck/collector/AnnotationCollector.java b/abi-check-plugin/src/main/java/com/yahoo/abicheck/collector/AnnotationCollector.java
new file mode 100644
index 00000000000..5be66a4fbdd
--- /dev/null
+++ b/abi-check-plugin/src/main/java/com/yahoo/abicheck/collector/AnnotationCollector.java
@@ -0,0 +1,27 @@
+package com.yahoo.abicheck.collector;
+
+import java.util.HashSet;
+import java.util.Set;
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+public class AnnotationCollector extends ClassVisitor {
+
+ private final Set<String> annotations = new HashSet<>();
+
+ public AnnotationCollector() {
+ super(Opcodes.ASM6);
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
+ annotations.add(Type.getType(descriptor).getClassName());
+ return null;
+ }
+
+ public Set<String> getAnnotations() {
+ return annotations;
+ }
+}
diff --git a/abi-check-plugin/src/main/java/com/yahoo/abicheck/collector/PublicSignatureCollector.java b/abi-check-plugin/src/main/java/com/yahoo/abicheck/collector/PublicSignatureCollector.java
new file mode 100644
index 00000000000..4fee85c823a
--- /dev/null
+++ b/abi-check-plugin/src/main/java/com/yahoo/abicheck/collector/PublicSignatureCollector.java
@@ -0,0 +1,85 @@
+package com.yahoo.abicheck.collector;
+
+import com.yahoo.abicheck.signature.JavaClassSignature;
+import com.yahoo.abicheck.signature.JavaMethodSignature;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+public class PublicSignatureCollector extends ClassVisitor {
+
+ private final Map<String, JavaClassSignature> classSignatures = new LinkedHashMap<>();
+
+ private String currentName;
+ private int currentAccess;
+ private Map<String, JavaMethodSignature> currentMethods;
+
+ public PublicSignatureCollector() {
+ super(Opcodes.ASM6);
+ }
+
+ private static String methodNameWithArguments(String name, List<String> argumentTypes) {
+ return String.format("%s(%s)", name, String.join(", ", argumentTypes));
+ }
+
+ private static boolean testBit(long access, long mask) {
+ return (access & mask) != 0;
+ }
+
+ @Override
+ public void visit(int version, int access, String name, String signature, String superName,
+ String[] interfaces) {
+ currentName = Type.getObjectType(name).getClassName();
+ currentAccess = access;
+ currentMethods = new LinkedHashMap<>();
+ }
+
+ @Override
+ public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
+ String[] exceptions) {
+ if (isVisibleMethod(access)) {
+ Type method = Type.getMethodType(descriptor);
+ List<String> argumentTypes = Arrays.stream(method.getArgumentTypes()).map(Type::getClassName)
+ .collect(Collectors.toList());
+ name = methodNameWithArguments(name, argumentTypes);
+ currentMethods.put(name,
+ new JavaMethodSignature(Util.convertAccess(access, Util.methodFlags),
+ method.getReturnType().getClassName()));
+ }
+ return null;
+ }
+
+ private boolean isVisibleMethod(int access) {
+ // Public methods are visible
+ if (testBit(access, Opcodes.ACC_PUBLIC)) {
+ return true;
+ }
+ // Protected non-static methods are visible if the class is not final (can be called from
+ // extending classes)
+ if (!testBit(access, Opcodes.ACC_STATIC) && testBit(access, Opcodes.ACC_PROTECTED) && !testBit(
+ currentAccess, Opcodes.ACC_FINAL)) {
+ return true;
+ }
+ // Otherwise not visible
+ return false;
+ }
+
+ @Override
+ public void visitEnd() {
+ if ((currentAccess & Opcodes.ACC_PUBLIC) != 0) {
+ classSignatures.put(currentName,
+ new JavaClassSignature(Util.convertAccess(currentAccess, Util.classFlags),
+ currentMethods));
+ }
+ }
+
+ public Map<String, JavaClassSignature> getClassSignatures() {
+ return classSignatures;
+ }
+}
diff --git a/abi-check-plugin/src/main/java/com/yahoo/abicheck/collector/Util.java b/abi-check-plugin/src/main/java/com/yahoo/abicheck/collector/Util.java
new file mode 100644
index 00000000000..24c1b52aa04
--- /dev/null
+++ b/abi-check-plugin/src/main/java/com/yahoo/abicheck/collector/Util.java
@@ -0,0 +1,68 @@
+package com.yahoo.abicheck.collector;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.objectweb.asm.Opcodes;
+
+public class Util {
+
+ public static final List<AccessFlag> classFlags = Arrays.asList(
+ AccessFlag.make(Opcodes.ACC_PUBLIC, "public"),
+ AccessFlag.make(Opcodes.ACC_PRIVATE, "private"),
+ AccessFlag.make(Opcodes.ACC_PROTECTED, "protected"),
+ AccessFlag.make(Opcodes.ACC_FINAL, "final"),
+ AccessFlag.make(Opcodes.ACC_SUPER, null), // Ignored, always set by modern Java
+ AccessFlag.make(Opcodes.ACC_INTERFACE, "interface"),
+ AccessFlag.make(Opcodes.ACC_ABSTRACT, "abstract"),
+ AccessFlag.make(Opcodes.ACC_SYNTHETIC, "synthetic"), // FIXME: Do we want this?
+ AccessFlag.make(Opcodes.ACC_ANNOTATION, "annotation"),
+ AccessFlag.make(Opcodes.ACC_ENUM, "enum")
+// FIXME: Module support
+// AccessFlag.make(Opcodes.ACC_MODULE, "module")
+ );
+
+ public static final List<AccessFlag> methodFlags = Arrays.asList(
+ AccessFlag.make(Opcodes.ACC_PUBLIC, "public"),
+ AccessFlag.make(Opcodes.ACC_PRIVATE, "private"),
+ AccessFlag.make(Opcodes.ACC_PROTECTED, "protected"),
+ AccessFlag.make(Opcodes.ACC_STATIC, "static"),
+ AccessFlag.make(Opcodes.ACC_FINAL, "final"),
+ AccessFlag.make(Opcodes.ACC_SYNCHRONIZED, "synchronized"),
+ AccessFlag.make(Opcodes.ACC_BRIDGE, "bridge"),
+ AccessFlag.make(Opcodes.ACC_VARARGS, "varargs"), // FIXME: Do we want this?
+ AccessFlag.make(Opcodes.ACC_NATIVE, "native"),
+ AccessFlag.make(Opcodes.ACC_ABSTRACT, "abstract"),
+ AccessFlag.make(Opcodes.ACC_STRICT, "strict"), // FIXME: Do we want this?
+ AccessFlag.make(Opcodes.ACC_SYNTHETIC, "synthetic") // FIXME: Do we want this?
+ );
+
+ public static List<String> convertAccess(int access, List<AccessFlag> flags) {
+ List<String> result = new ArrayList<>();
+ for (AccessFlag flag : flags) {
+ if ((access & flag.bit) != 0 && flag.attribute != null) {
+ result.add(flag.attribute);
+ }
+ access &= ~flag.bit;
+ }
+ if (access != 0) {
+ throw new IllegalArgumentException(String.format("Unexpected access bits: 0x%x", access));
+ }
+ return result;
+ }
+
+ private static class AccessFlag {
+
+ public final int bit;
+ public final String attribute;
+
+ private AccessFlag(int bit, String attribute) {
+ this.bit = bit;
+ this.attribute = attribute;
+ }
+
+ private static AccessFlag make(int bit, String attribute) {
+ return new AccessFlag(bit, attribute);
+ }
+ }
+}
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;
+ }
+}
diff --git a/abi-check-plugin/src/main/java/com/yahoo/abicheck/signature/JavaClassSignature.java b/abi-check-plugin/src/main/java/com/yahoo/abicheck/signature/JavaClassSignature.java
new file mode 100644
index 00000000000..774792c0365
--- /dev/null
+++ b/abi-check-plugin/src/main/java/com/yahoo/abicheck/signature/JavaClassSignature.java
@@ -0,0 +1,17 @@
+package com.yahoo.abicheck.signature;
+
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class JavaClassSignature {
+
+ public final List<String> attributes;
+ public final Map<String, JavaMethodSignature> methods;
+
+ public JavaClassSignature(List<String> attributes, Map<String, JavaMethodSignature> methods) {
+ this.attributes = attributes;
+ this.methods = methods;
+ }
+}
diff --git a/abi-check-plugin/src/main/java/com/yahoo/abicheck/signature/JavaMethodSignature.java b/abi-check-plugin/src/main/java/com/yahoo/abicheck/signature/JavaMethodSignature.java
new file mode 100644
index 00000000000..0d562d0acb1
--- /dev/null
+++ b/abi-check-plugin/src/main/java/com/yahoo/abicheck/signature/JavaMethodSignature.java
@@ -0,0 +1,14 @@
+package com.yahoo.abicheck.signature;
+
+import java.util.List;
+
+public class JavaMethodSignature {
+
+ public final List<String> attributes;
+ public final String returnType;
+
+ public <T> JavaMethodSignature(List<String> attributes, String returnType) {
+ this.attributes = attributes;
+ this.returnType = returnType;
+ }
+}