tools/javac/ApiProcessor.java (116 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.annotation.processing.*; import javax.lang.model.SourceVersion; import javax.lang.model.element.*; import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.util.Objects; import java.util.Set; @SuppressWarnings({"UnnecessaryUnicodeEscape", "unused"}) @SupportedOptions({"output", "version"}) @SupportedSourceVersion(SourceVersion.RELEASE_9) @SupportedAnnotationTypes("*") public class ApiProcessor extends AbstractProcessor { private Path output; private Api.Version versionOverride; private SourceGenerator sourceGenerator; private ApiCollector apiCollector; @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); output = Path.of(Objects.requireNonNull(processingEnv.getOptions().get("output"), "-Aoutput option is missing")); String version = processingEnv.getOptions().get("version"); if (version != null) versionOverride = Api.Version.parse(version); sourceGenerator = new SourceGenerator(processingEnv); apiCollector = new ApiCollector(processingEnv); } private static ExecutableElement findAnnotationValue(TypeElement e) { for (Element t : e.getEnclosedElements()) { if (t instanceof ExecutableElement executable) { if (t.getSimpleName().toString().equals("value")) { return executable; } } } throw new Error(e.getQualifiedName() + ".value() method not found"); } @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { // Find our annotation elements. Round round = new Round(roundEnvironment); for (TypeElement e : set) { switch (e.getQualifiedName().toString()) { case "com.jetbrains.Service" -> round.annotations.service = e; case "com.jetbrains.Provided" -> round.annotations.provided = e; case "com.jetbrains.Provides" -> round.annotations.provides = e; case "com.jetbrains.Fallback" -> { round.annotations.fallback = e; round.annotations.fallbackValue = findAnnotationValue(e); } case "com.jetbrains.Extension" -> { round.annotations.extension = e; round.annotations.extensionValue = findAnnotationValue(e); } } } // Generate sources on first round. if (sourceGenerator != null) { sourceGenerator.generate(round); sourceGenerator = null; } // Collect API info. Api.Module newApi = apiCollector.collect(round); if (newApi == null) return true; try { String message; if (versionOverride != null) { // Override API version from options. newApi.version = versionOverride; message = "\u2757 Skipping API checks, version override specified: " + versionOverride + "\n"; } else { // Read old API info. Api.Module oldApi; try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("api-blob"))) { oldApi = (Api.Module) in.readObject(); } catch (IOException | ClassNotFoundException e) { throw new RuntimeException(e); } // Compare API changes. ApiComparator.Node result = ApiComparator.compare(oldApi, newApi); ApiComparator.Digest digest = result.digest(); newApi.version = digest.compatibility().incrementVersion(oldApi.version); if (digest.compatibility() == ApiComparator.Compatibility.SAME) { // Do not print anything if there were no changes. message = null; } else { // Put changes into code block. StringBuilder out = new StringBuilder(); if (!digest.diff().isEmpty()) out.append("```\n").append(digest.diff()).append("```\n"); // Print messages. for (ApiComparator.Message msg : digest.messages()) out.append(msg.text).append('\n'); out.append("Compatibility status of API changes: ").append(digest.compatibility()).append(' '); out.append(switch (digest.compatibility()) { case MAJOR -> "\uD83E\uDD2F"; case MINOR -> "\uD83D\uDD27"; case PATCH -> "\uD83D\uDC85"; default -> ""; }); out.append("\nVersion increment: ").append(oldApi.version).append(" -> ").append(newApi.version).append('\n'); message = out.toString(); } } if (message != null) { // Get rid of unicode symbols when printing to stdout. String simple = message; for (ApiComparator.Message msg : ApiComparator.Message.values()) { if (msg.mark != null && msg.simpleMark != null) simple = simple.replaceAll(msg.mark, msg.simpleMark); } simple = simple .replaceAll("[^\\x00-\\x7F]", "") .replaceAll("```\n", "") .stripTrailing(); System.out.println(simple); } // Save metadata. Files.createDirectories(output); try (ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(output.resolve("api-blob")))) { out.writeObject(newApi); } Files.writeString(output.resolve("version.txt"), newApi.version.toString()); Files.writeString(output.resolve("message.txt"), message != null ? message : ""); Files.write(output.resolve("sourcelist8.txt"), apiCollector.getJava8CompilationUnitPaths()); } catch (IOException e) { throw new RuntimeException(e); } return true; } }