tools/javac/ApiComparator.java (261 lines of code) (raw):

/* * Copyright 2000-2024 JetBrains s.r.o. * * 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 * * http://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. */ import javax.lang.model.element.Modifier; import java.util.*; import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; import static javax.lang.model.element.Modifier.*; /** * Compares API computing compatibility status and textual description of API changes. * <a href="https://github.com/eclipse-platform/eclipse.platform/blob/master/docs/Evolving-Java-based-APIs-2.md">Useful link.</a> */ @SuppressWarnings("UnnecessaryUnicodeEscape") public class ApiComparator { public enum Message { BREAKING_CHANGES("\u2757 There are breaking changes which require extra attention.", "\u2757", "!!!"), NON_EXTENSION_METHOD_ADDED("\u2755 Non-extension methods added to existing types. " + "It is generally advised to add methods to existing types as @Extension methods.", "\u2755", "!!"); Message(String text, String mark, String simpleMark) { this.text = text; this.mark = mark; this.simpleMark = simpleMark; } public final String text, mark, simpleMark; } public record Digest(Compatibility compatibility, String diff, Set<Message> messages) {} public static class Node { public final String name; public final Set<Message> messages = EnumSet.noneOf(Message.class); public Diff diff; public Compatibility compatibility = Compatibility.SAME; public String note; public Node next, child; private Node(String name, Diff diff) { this.name = name; this.diff = diff; } private Node(Object a, Object b) { this((b == null ? a : b).toString(), b == null ? Diff.REMOVED : a == null ? Diff.ADDED : Diff.NONE); } private void check(boolean change, String note) { if (change) { if (diff == Diff.NONE) diff = Diff.MODIFIED; if (note != null) { this.note = this.note == null ? note : this.note + ", " + note; } } } /** * @return digest information with total compatibility of API changes. */ public Digest digest() { StringBuilder diff = new StringBuilder(); Set<Message> messages = EnumSet.noneOf(Message.class); Compatibility compatibility = traverse(diff, messages); return new Digest(compatibility, diff.toString(), messages); } public Compatibility traverse(StringBuilder out, Set<Message> messages) { if (name == null) { if (child == null) return compatibility; return Compatibility.max(compatibility, child.traverse(out, new StringBuilder(), messages)); } else { return traverse(out, new StringBuilder(), messages); } } private Compatibility traverse(StringBuilder out, StringBuilder indent, Set<Message> messages) { messages.addAll(this.messages); int indentDepth = indent.length(); Compatibility nextComp = Compatibility.SAME, comp = compatibility; if (next != null) nextComp = next.traverse(out, indent, messages); if (child != null) { indent.append(" "); comp = Compatibility.max(comp, child.traverse(out, indent, messages)); indent.setLength(indentDepth); } String marks = this.messages.stream().map(m -> m.mark != null ? " " + m.mark : "").collect(Collectors.joining()); if (comp != Compatibility.SAME || note != null || !marks.isEmpty()) { indent.append(diff.ch).append(' ').append(name); if (note != null) indent.append(" - ").append(note); indent.append(marks); out.insert(0, indent.append('\n')); indent.setLength(indentDepth); } return Compatibility.max(comp, nextComp); } } public enum Compatibility { SAME(v -> v), PATCH(v -> new Api.Version(v.major(), v.minor(), v.patch() + 1)), MINOR(v -> new Api.Version(v.major(), v.minor() + 1, 0)), MAJOR(v -> new Api.Version(v.major() + 1, 0, 0)); private final Function<Api.Version, Api.Version> versionIncrement; Compatibility(Function<Api.Version, Api.Version> versionIncrement) { this.versionIncrement = versionIncrement; } public Api.Version incrementVersion(Api.Version v) { return versionIncrement.apply(v); } public static Compatibility max(Compatibility a, Compatibility b) { return a.ordinal() >= b.ordinal() ? a : b; } } public enum Diff { NONE(' '), MODIFIED('*'), ADDED('+'), REMOVED('-'); public final char ch; Diff(char ch) { this.ch = ch; } } public static Node compare(Api.Module a, Api.Module b) { Node node; if (a == null || b == null) { node = new Node(null, Diff.MODIFIED); node.compatibility = Compatibility.MAJOR; node.messages.add(Message.BREAKING_CHANGES); } else { node = new Node(null, Diff.NONE); node.child = compare(a.types, b.types, ApiComparator::compare, node.child); if (a.hash != b.hash) node.compatibility = Compatibility.PATCH; } return node; } public static Node compare(Api.Type a, Api.Type b) { Node node = new Node(a, b); if (node.diff == Diff.ADDED) { node.compatibility = Compatibility.MINOR; } else if (node.diff == Diff.REMOVED) { node.compatibility = Compatibility.MAJOR; node.messages.add(Message.BREAKING_CHANGES); } else { node.child = compare(a.types, b.types, ApiComparator::compare, node.child); node.child = compare(a.methods, b.methods, ApiComparator::compare, node.child); node.child = compare(a.fields, b.fields, ApiComparator::compare, node.child); // Breaking changes node.check(a.kind != b.kind, "changed kind"); node.check(!b.supertypes.containsAll(a.supertypes), "contracted supertype set"); node.check(!Arrays.equals(a.typeParameters, b.typeParameters), "changed type parameters"); node.check(a.usage.inheritableByBackend && !b.usage.inheritableByBackend, "prohibited inheritance by backend"); node.check(a.usage.inheritableByClient && !b.usage.inheritableByClient, "prohibited inheritance by client"); if (node.diff != Diff.NONE) { node.compatibility = Compatibility.MAJOR; node.messages.add(Message.BREAKING_CHANGES); return node; } if (compareModifiers(node, a.modifiers, b.modifiers)) return node; // Compatible changes node.check(a.supertypes.size() != b.supertypes.size(), "expanded supertype set"); node.check(a.deprecation != b.deprecation, "changed deprecation state"); node.check(!a.usage.inheritableByBackend && b.usage.inheritableByBackend, "allowed inheritance by backend"); node.check(!a.usage.inheritableByClient && b.usage.inheritableByClient, "allowed inheritance by client"); if (node.diff != Diff.NONE) { node.compatibility = Compatibility.MINOR; return node; } } return node; } public static Node compare(Api.Field a, Api.Field b) { Node node = new Node(a, b); if (node.diff == Diff.ADDED) { node.compatibility = Compatibility.MINOR; } else if (node.diff == Diff.REMOVED) { node.compatibility = Compatibility.MAJOR; node.messages.add(Message.BREAKING_CHANGES); } else { // Breaking changes node.check(!Objects.equals(a.type, b.type), "changed type"); node.check(!Objects.equals(a.constantValue, b.constantValue), "changed value"); if (node.diff != Diff.NONE) { node.compatibility = Compatibility.MAJOR; node.messages.add(Message.BREAKING_CHANGES); return node; } if (compareModifiers(node, a.modifiers, b.modifiers)) return node; // Compatible changes node.check(a.deprecation != b.deprecation, "changed deprecation state"); if (node.diff != Diff.NONE) { node.compatibility = Compatibility.MINOR; return node; } } return node; } public static Node compare(Api.Method a, Api.Method b) { Node node = new Node(a, b); if (node.diff == Diff.ADDED) { if (b.parent.usage.inheritableByClient && b.modifiers.contains(ABSTRACT)) { node.compatibility = Compatibility.MAJOR; node.messages.add(Message.BREAKING_CHANGES); } else { // Adding a non-abstract method to a type inheritable by a client still may // break compatibility in some cases, but we consider this risk low enough. node.compatibility = Compatibility.MINOR; if (b.extension == null && b.parent.usage.inheritableByBackend && b.modifiers.contains(ABSTRACT) && !b.modifiers.contains(STATIC) && !b.modifiers.contains(FINAL)) { node.messages.add(Message.NON_EXTENSION_METHOD_ADDED); } } } else if (node.diff == Diff.REMOVED) { node.compatibility = Compatibility.MAJOR; node.messages.add(Message.BREAKING_CHANGES); } else { // Breaking changes node.check(!Objects.equals(a.returnType, b.returnType), "changed return type"); node.check(!Objects.equals(a.thrownTypes, b.thrownTypes), "changed thrown types"); node.check(!Arrays.equals(a.typeParameters, b.typeParameters), "changed type parameters"); if (node.diff != Diff.NONE) { node.compatibility = Compatibility.MAJOR; node.messages.add(Message.BREAKING_CHANGES); return node; } if (compareModifiers(node, a.modifiers, b.modifiers)) return node; // Compatible changes node.check(a.deprecation != b.deprecation, "changed deprecation state"); node.check(!Objects.equals(a.extension, b.extension), "changed extension"); if (node.diff != Diff.NONE) { node.compatibility = Compatibility.MINOR; return node; } } return node; } private static boolean compareModifiers(Node node, Set<Modifier> a, Set<Modifier> b) { if (node.diff != Diff.NONE) return true; // Breaking changes node.check(a.contains(PUBLIC) && !b.contains(PUBLIC), "decreased visibility"); node.check((!a.contains(ABSTRACT) && b.contains(ABSTRACT)) || (a.contains(DEFAULT) && !b.contains(DEFAULT)), "made abstract"); node.check(!a.contains(FINAL) && b.contains(FINAL), "made final"); node.check(a.contains(STATIC) != b.contains(STATIC), "changed static"); if (node.diff != Diff.NONE) { node.compatibility = Compatibility.MAJOR; node.messages.add(Message.BREAKING_CHANGES); return true; } // Compatible changes node.check(!a.contains(PUBLIC) && b.contains(PUBLIC), "increased visibility"); node.check((a.contains(ABSTRACT) && !b.contains(ABSTRACT)) || (!a.contains(DEFAULT) && b.contains(DEFAULT)), "made non-abstract"); node.check(a.contains(FINAL) && !b.contains(FINAL), "made non-final"); if (node.diff != Diff.NONE) { node.compatibility = Compatibility.MINOR; return true; } return false; } private static <T> Node compare(Map<T, T> a, Map<T, T> b, BiFunction<T, T, Node> comparator, Node next) { Node node = next; if (a == null) { for (T bt : b.values()) { Node n = comparator.apply(null, bt); n.next = node; node = n; } } else if (b == null) { for (T at : a.values()) { Node n = comparator.apply(at, null); n.next = node; node = n; } } else { Map<T, T> aTypes = new HashMap<>(a); for (T bt : b.values()) { T at = aTypes.remove(bt); Node n = comparator.apply(at, bt); n.next = node; node = n; } for (T at : aTypes.values()) { Node n = comparator.apply(at, null); n.next = node; node = n; } } return node; } }