blob: b543b8dfdacf6ac073cecfa89e7aed7e81ba1f59 [file] [log] [blame]
// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.keepanno.utils;
import static com.android.tools.r8.keepanno.utils.KeepItemAnnotationGenerator.quote;
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.keepanno.annotations.AnnotationPattern;
import com.android.tools.r8.keepanno.annotations.FieldAccessFlags;
import com.android.tools.r8.keepanno.annotations.KeepBinding;
import com.android.tools.r8.keepanno.annotations.KeepCondition;
import com.android.tools.r8.keepanno.annotations.KeepConstraint;
import com.android.tools.r8.keepanno.annotations.KeepEdge;
import com.android.tools.r8.keepanno.annotations.KeepForApi;
import com.android.tools.r8.keepanno.annotations.KeepItemKind;
import com.android.tools.r8.keepanno.annotations.KeepTarget;
import com.android.tools.r8.keepanno.annotations.MemberAccessFlags;
import com.android.tools.r8.keepanno.annotations.MethodAccessFlags;
import com.android.tools.r8.keepanno.annotations.StringPattern;
import com.android.tools.r8.keepanno.annotations.TypePattern;
import com.android.tools.r8.keepanno.annotations.UsedByNative;
import com.android.tools.r8.keepanno.annotations.UsedByReflection;
import com.android.tools.r8.keepanno.annotations.UsesReflection;
import com.android.tools.r8.keepanno.doctests.ForApiDocumentationTest;
import com.android.tools.r8.keepanno.doctests.MainMethodsDocumentationTest;
import com.android.tools.r8.keepanno.doctests.UsesReflectionAnnotationsDocumentationTest;
import com.android.tools.r8.keepanno.doctests.UsesReflectionDocumentationTest;
import com.android.tools.r8.keepanno.utils.KeepItemAnnotationGenerator.Generator;
import com.android.tools.r8.utils.FileUtils;
import com.android.tools.r8.utils.StringUtils;
import com.google.common.collect.ImmutableMap;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class KeepAnnoMarkdownGenerator {
public static void generateMarkdownDoc(Generator generator, Path projectRoot) {
try {
new KeepAnnoMarkdownGenerator(generator).internalGenerateMarkdownDoc(projectRoot);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static String JAVADOC_URL =
"https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/";
private static final String TOC_MARKER = "[[[TOC]]]";
private static final String INCLUDE_MD_START = "[[[INCLUDE";
private static final String INCLUDE_MD_DOC_START = "[[[INCLUDE DOC";
private static final String INCLUDE_MD_CODE_START = "[[[INCLUDE CODE";
private static final String INCLUDE_MD_END = "]]]";
private static final String INCLUDE_DOC_START = "INCLUDE DOC:";
private static final String INCLUDE_DOC_END = "INCLUDE END";
private static final String INCLUDE_CODE_START = "INCLUDE CODE:";
private static final String INCLUDE_CODE_END = "INCLUDE END";
private final Generator generator;
private final Map<String, String> typeLinkReplacements;
private Map<String, String> docReplacements = new HashMap<>();
private Map<String, String> codeReplacements = new HashMap<>();
public KeepAnnoMarkdownGenerator(Generator generator) {
this.generator = generator;
typeLinkReplacements =
getTypeLinkReplacements(
// Annotations.
KeepEdge.class,
KeepBinding.class,
KeepTarget.class,
KeepCondition.class,
UsesReflection.class,
UsedByReflection.class,
UsedByNative.class,
KeepForApi.class,
StringPattern.class,
TypePattern.class,
AnnotationPattern.class,
// Enums.
KeepConstraint.class,
KeepItemKind.class,
MemberAccessFlags.class,
MethodAccessFlags.class,
FieldAccessFlags.class);
populateCodeAndDocReplacements(
UsesReflectionDocumentationTest.class,
UsesReflectionAnnotationsDocumentationTest.class,
ForApiDocumentationTest.class,
MainMethodsDocumentationTest.class);
}
private Map<String, String> getTypeLinkReplacements(Class<?>... classes) {
ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
for (Class<?> clazz : classes) {
String prefix = "`@" + clazz.getSimpleName();
String suffix = "`";
if (clazz.isAnnotation()) {
builder.put(prefix + suffix, getMdAnnotationLink(clazz));
for (Method method : clazz.getDeclaredMethods()) {
builder.put(
prefix + "#" + method.getName() + suffix, getMdAnnotationPropertyLink(method));
}
} else if (clazz.isEnum()) {
builder.put(prefix + suffix, getMdEnumLink(clazz));
for (Field field : clazz.getDeclaredFields()) {
builder.put(prefix + "#" + field.getName() + suffix, getMdEnumFieldLink(field));
}
} else {
throw new RuntimeException("Unexpected type of class for doc links");
}
}
return builder.build();
}
private void populateCodeAndDocReplacements(Class<?>... classes) {
try {
for (Class<?> clazz : classes) {
Path sourceFile = ToolHelper.getSourceFileForTestClass(clazz);
String text = FileUtils.readTextFile(sourceFile, StandardCharsets.UTF_8);
extractMarkers(text, INCLUDE_DOC_START, INCLUDE_DOC_END, docReplacements);
extractMarkers(text, INCLUDE_CODE_START, INCLUDE_CODE_END, codeReplacements);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void extractMarkers(
String text, String markerKeyStart, String markerKeyEnd, Map<String, String> replacementMap) {
int index = text.indexOf(markerKeyStart);
while (index >= 0) {
int markerTitleEnd = text.indexOf('\n', index);
if (markerTitleEnd < 0) {
throw new RuntimeException("Failed to find end marker title");
}
int end = text.indexOf(markerKeyEnd, index);
if (end < 0) {
throw new RuntimeException("Failed to find end marker");
}
int endBeforeNewLine = text.lastIndexOf('\n', end);
if (endBeforeNewLine < markerTitleEnd) {
throw new RuntimeException("No new-line before end marker");
}
String markerTitle = text.substring(index + markerKeyStart.length(), markerTitleEnd);
String includeContent = text.substring(markerTitleEnd + 1, endBeforeNewLine);
String old = replacementMap.put(markerTitle.trim(), includeContent);
if (old != null) {
throw new RuntimeException("Duplicate definition of marker");
}
index = text.indexOf(markerKeyStart, end);
}
}
private static String getClassJavaDocUrl(Class<?> clazz) {
return JAVADOC_URL + clazz.getTypeName().replace('.', '/') + ".html";
}
private String getMdAnnotationLink(Class<?> clazz) {
return "[@" + clazz.getSimpleName() + "](" + getClassJavaDocUrl(clazz) + ")";
}
private String getMdAnnotationPropertyLink(Method method) {
Class<?> clazz = method.getDeclaringClass();
String methodName = method.getName();
String url = getClassJavaDocUrl(clazz) + "#" + methodName + "()";
return "[@" + clazz.getSimpleName() + "." + methodName + "](" + url + ")";
}
private String getMdEnumLink(Class<?> clazz) {
return "[" + clazz.getSimpleName() + "](" + getClassJavaDocUrl(clazz) + ")";
}
private String getMdEnumFieldLink(Field field) {
Class<?> clazz = field.getDeclaringClass();
String fieldName = field.getName();
String url = getClassJavaDocUrl(clazz) + "#" + fieldName;
return "[" + clazz.getSimpleName() + "." + fieldName + "](" + url + ")";
}
private void println() {
generator.println("");
}
private void println(String line) {
generator.println(line);
}
private void internalGenerateMarkdownDoc(Path projectRoot) throws IOException {
String relativeUnixPath = "doc/keepanno-guide.template.md";
Path template = projectRoot.resolve(relativeUnixPath);
println("[comment]: <> (DO NOT EDIT - GENERATED FILE)");
println("[comment]: <> (Changes should be made in " + relativeUnixPath + ")");
println();
List<String> readAllLines = FileUtils.readAllLines(template);
TableEntry root = new TableEntry(0, "root", "root", null);
readAllLines = collectTableOfContents(readAllLines, root);
for (int i = 0; i < readAllLines.size(); i++) {
String line = readAllLines.get(i);
try {
if (line.trim().equals(TOC_MARKER)) {
printTableOfContents(root);
} else {
processLine(line, generator);
}
} catch (Exception e) {
System.err.println("Parse error on line " + (i + 1) + ":");
System.err.println(line);
System.err.println(e.getMessage());
}
}
}
private void printTableOfContents(TableEntry root) {
println("## Table of contents");
println();
printTableSubEntries(root.subSections.values());
println();
}
private void printTableSubEntries(Collection<TableEntry> entries) {
for (TableEntry entry : entries) {
println("- " + entry.getHrefLink());
generator.withIndent(() -> printTableSubEntries(entry.subSections.values()));
}
}
private List<String> collectTableOfContents(List<String> lines, TableEntry root) {
Set<String> seen = new HashSet<>();
TableEntry current = root;
List<String> newLines = new ArrayList<>(lines.size());
Iterator<String> iterator = lines.iterator();
// Skip forward until the TOC insertion.
while (iterator.hasNext()) {
String line = iterator.next();
newLines.add(line);
if (line.trim().equals(TOC_MARKER)) {
break;
}
}
// Find TOC entries and replace the headings with links.
while (iterator.hasNext()) {
String line = iterator.next();
int headingDepth = 0;
for (int i = 0; i < line.length(); i++) {
char c = line.charAt(i);
if (c != '#') {
headingDepth = i;
break;
}
}
if (headingDepth == 0) {
newLines.add(line);
continue;
}
String headingPrefix = line.substring(0, headingDepth);
String headingContent = line.substring(headingDepth).trim();
int splitIndex = headingContent.indexOf("](");
if (splitIndex < 0 || !headingContent.startsWith("[") || !headingContent.endsWith(")")) {
throw new RuntimeException("Invalid heading format. Use [Heading Text](heading-id)");
}
String headingText = headingContent.substring(1, splitIndex);
String headingId = headingContent.substring(splitIndex + 2, headingContent.length() - 1);
if (!seen.add(headingId)) {
throw new RuntimeException("Duplicate heading id: " + headingText);
}
while (headingDepth <= current.depth) {
current = current.parent;
}
TableEntry entry = new TableEntry(headingDepth, headingText, headingId, current);
current.subSections.put(headingText, entry);
current = entry;
newLines.add(headingPrefix + " " + entry.getIdAnchor());
}
return newLines;
}
private String replaceCodeAndDocMarkers(String line) {
String originalLine = line;
line = line.trim();
if (!line.startsWith(INCLUDE_MD_START)) {
return originalLine;
}
int keyStartIndex = line.indexOf(':');
if (!line.endsWith(INCLUDE_MD_END) || keyStartIndex < 0) {
throw new RuntimeException("Invalid include directive");
}
String key = line.substring(keyStartIndex + 1, line.length() - INCLUDE_MD_END.length());
if (line.startsWith(INCLUDE_MD_DOC_START)) {
return replaceDoc(key);
}
if (line.startsWith(INCLUDE_MD_CODE_START)) {
return replaceCode(key);
}
throw new RuntimeException("Unknown replacement marker");
}
private String replaceDoc(String key) {
String replacement = docReplacements.get(key);
if (replacement == null) {
throw new RuntimeException("No replacement defined for " + key);
}
return unindentLines(replacement, new StringBuilder()).toString();
}
private String replaceCode(String key) {
String replacement = codeReplacements.get(key);
if (replacement == null) {
throw new RuntimeException("No replacement defined for " + key);
}
StringBuilder builder = new StringBuilder();
builder.append("```\n");
unindentLines(replacement, builder);
builder.append("```\n");
return builder.toString();
}
private StringBuilder unindentLines(String replacement, StringBuilder builder) {
int shortestSpacePrefix = Integer.MAX_VALUE;
List<String> lines = StringUtils.split(replacement, '\n');
lines = trimEmptyLines(lines);
for (String line : lines) {
if (!line.isEmpty()) {
shortestSpacePrefix = Math.min(shortestSpacePrefix, findFirstNonSpaceIndex(line));
}
}
for (String line : lines) {
if (!line.isEmpty()) {
builder.append(line.substring(shortestSpacePrefix));
}
builder.append('\n');
}
return builder;
}
private static List<String> trimEmptyLines(List<String> lines) {
int startLineIndex = 0;
int endLineIndex = lines.size() - 1;
while (true) {
String line = lines.get(startLineIndex);
if (line.trim().isEmpty()) {
startLineIndex++;
} else {
break;
}
}
while (true) {
String line = lines.get(endLineIndex);
if (line.trim().isEmpty()) {
--endLineIndex;
} else {
break;
}
}
if (startLineIndex != 0 || endLineIndex != lines.size() - 1) {
lines = lines.subList(startLineIndex, endLineIndex + 1);
}
return lines;
}
private int findFirstNonSpaceIndex(String line) {
for (int i = 0; i < line.length(); i++) {
if (line.charAt(i) != ' ') {
return i;
}
}
return line.length();
}
private String tryLinkReplacements(String line) {
int index = line.indexOf("`@");
if (index < 0) {
return null;
}
int end = line.indexOf('`', index + 1);
if (end < 0) {
throw new RuntimeException("No end marker on line: " + line);
}
String typeLink = line.substring(index, end + 1);
String replacement = typeLinkReplacements.get(typeLink);
if (replacement == null) {
throw new RuntimeException("Unknown type link: " + typeLink);
}
return line.replace(typeLink, replacement);
}
private void processLine(String line, Generator generator) {
line = replaceCodeAndDocMarkers(line);
String replacement = tryLinkReplacements(line);
while (replacement != null) {
line = replacement;
replacement = tryLinkReplacements(line);
}
generator.println(line);
}
private static class TableEntry {
final int depth;
final String name;
final String id;
final TableEntry parent;
final Map<String, TableEntry> subSections = new LinkedHashMap<>();
public TableEntry(int depth, String name, String id, TableEntry parent) {
this.depth = depth;
this.name = name;
this.id = id;
this.parent = parent;
}
public String getHrefLink() {
return "[" + name + "](#" + id + ")";
}
public String getIdAnchor() {
return name + "<a name=" + quote(id) + "></a>";
}
}
}