Subject: [PATCH] Tests --- Index: build.gradle.kts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/build.gradle.kts b/build.gradle.kts --- a/build.gradle.kts (revision a7436695413628658730dc48fa0f564b511b9ec7) +++ b/build.gradle.kts (date 1713903499359) @@ -146,6 +146,7 @@ testImplementation(libs.mockito) testImplementation(libs.assertj) testImplementation(libs.commons.lang3) + testImplementation(libs.javaparser) } Index: src/test/java/net/dv8tion/jda/ParseUtil.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/test/java/net/dv8tion/jda/ParseUtil.java b/src/test/java/net/dv8tion/jda/ParseUtil.java new file mode 100644 --- /dev/null (date 1713903499368) +++ b/src/test/java/net/dv8tion/jda/ParseUtil.java (date 1713903499368) @@ -0,0 +1,171 @@ +package net.dv8tion.jda; + +import com.github.javaparser.ParseResult; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.PackageDeclaration; +import com.github.javaparser.ast.body.TypeDeclaration; +import com.github.javaparser.javadoc.Javadoc; +import com.github.javaparser.javadoc.description.JavadocDescription; +import com.github.javaparser.javadoc.description.JavadocDescriptionElement; +import com.github.javaparser.javadoc.description.JavadocInlineTag; +import com.github.javaparser.javadoc.description.JavadocSnippet; +import com.github.javaparser.utils.SourceRoot; +import net.dv8tion.jda.api.events.Event; +import net.dv8tion.jda.api.events.GenericEvent; +import net.dv8tion.jda.api.events.annotations.RequiresCachedMember; +import net.dv8tion.jda.internal.utils.JDALogger; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class ParseUtil +{ + private static final Logger LOGGER = JDALogger.getLog(ParseUtil.class); + private static final Pattern LINK_SPLIT_PATTERN = Pattern.compile("(? parseEventCompilationUnits() throws IOException + { + final SourceRoot root = new SourceRoot(Paths.get("src", "main", "java")); + final List> parseResults = root.tryToParse(Event.class.getPackage().getName()); + if (parseResults.isEmpty()) + throw new AssertionError("Could not find any source file"); + return parseResults.stream() + .filter(p -> + { + if (!p.getProblems().isEmpty()) + throw new RuntimeException("Problems when parsing were encountered:\n" + p.getProblems()); + else if (!p.getResult().isPresent()) + throw new AssertionError("No result was present but no problems were either"); + + return p.getResult().isPresent(); + }) + .map(r -> r.getResult().get()) + // Exclude annotations + .filter(c -> !c.getPackageDeclaration().map(PackageDeclaration::getNameAsString).equals(Optional.of(RequiresCachedMember.class.getPackage().getName()))) + .collect(Collectors.toList()); + } + + @SuppressWarnings("unchecked") + public static > Map, EnumSet> getEnumEntriesFromDocs(Class enumType, List compilationUnits) throws ClassNotFoundException + { + final Pattern enumEntryReferencePattern = Pattern.compile(Pattern.quote(enumType.getSimpleName()) + "#(\\w+)"); + final Map, EnumSet> enumEntriesByClass = new HashMap<>(); + + // Cannot simplify to "peek", see API notes + //noinspection SimplifyStreamApiCallChains + getAllJavadocDescriptions(compilationUnits) + .entrySet() + .stream() + .map(entry -> + { + entry.setValue(getDescriptionAfterRequirements(entry.getValue())); + return entry; + }) + .forEach(entry -> + { + final Class documentedClass = entry.getKey(); + final JavadocDescription description = entry.getValue(); + + final List links = getLinks(description); + final EnumSet expectedFlags = mapLinks(enumType, links, enumEntryReferencePattern); + enumEntriesByClass.put((Class) documentedClass, expectedFlags); + }); + + return enumEntriesByClass; + } + + @SuppressWarnings("unchecked") + public static List> getClassesRequiringMemberCacheFromDocs(List compilationUnits) throws ClassNotFoundException + { + // Cannot simplify to "peek", see API notes + //noinspection SimplifyStreamApiCallChains + return getAllJavadocDescriptions(compilationUnits).entrySet().stream() + .map(entry -> + { + entry.setValue(getDescriptionAfterRequirements(entry.getValue())); + return entry; + }) + .filter(entry -> getLinks(entry.getValue()).stream().anyMatch(s -> s.contains("MemberCachePolicy"))) + .map(entry -> (Class) entry.getKey()) + .collect(Collectors.toList()); + } + + private static Map, JavadocDescription> getAllJavadocDescriptions(List compilationUnits) throws ClassNotFoundException + { + final Map, JavadocDescription> descriptions = new HashMap<>(); + for (TypeDeclaration typeDeclaration : compilationUnits.stream().flatMap(c -> c.findAll(TypeDeclaration.class).stream()).collect(Collectors.toList())) + { + final String qualifiedClassName = typeDeclaration.getFullyQualifiedName().orElseThrow(AssertionError::new); + + final JavadocDescription description = typeDeclaration + .getJavadoc() + .map(Javadoc::getDescription) + .orElse(null); + if (description == null) + { + LOGGER.warn("Undocumented class at {}", qualifiedClassName); + continue; + } + + descriptions.put(Class.forName(qualifiedClassName), description); + } + + return descriptions; + } + + private static JavadocDescription getDescriptionAfterRequirements(JavadocDescription description) + { + final List elements = new ArrayList<>(); + boolean encountered = false; + for (JavadocDescriptionElement element : description.getElements()) + { + if (element.toText().contains("Requirements")) + { + if (!(element instanceof JavadocSnippet)) + throw new AssertionError("Plain text can only be a snippet!"); + elements.add(new JavadocSnippet(StringUtils.substringAfter(element.toText(), "Requirements"))); + encountered = true; + } else if (encountered) + elements.add(element); + } + + return new JavadocDescription(elements); + } + + @Nonnull + private static List getLinks(JavadocDescription description) + { + return description.getElements().stream() + .filter(JavadocInlineTag.class::isInstance) + .map(JavadocInlineTag.class::cast) + .filter(tag -> tag.getType() == JavadocInlineTag.Type.LINK) + .map(JavadocInlineTag::getContent) + .map(String::trim) + .map(s -> LINK_SPLIT_PATTERN.split(s, 2)[0]) //Take left part + .collect(Collectors.toList()); + } + + @Nonnull + private static > EnumSet mapLinks(Class type, List links, Pattern pattern) + { + final EnumSet enumSet = EnumSet.noneOf(type); + for (String link : links) + { + final Matcher matcher = pattern.matcher(link); + while (matcher.find()) + { + final String elementName = matcher.group(1); + enumSet.add(Enum.valueOf(type, elementName)); + } + } + return enumSet; + } +} Index: settings.gradle.kts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/settings.gradle.kts b/settings.gradle.kts --- a/settings.gradle.kts (revision a7436695413628658730dc48fa0f564b511b9ec7) +++ b/settings.gradle.kts (date 1713903499364) @@ -22,6 +22,7 @@ library("mockito", "org.mockito", "mockito-core" ).version("5.11.0") library("reflections", "org.reflections", "reflections" ).version("0.10.2") library("slf4j", "org.slf4j", "slf4j-api" ).version("1.7.36") + library("javaparser", "com.github.javaparser", "javaparser-core" ).version("3.25.10") } } } Index: src/test/java/net/dv8tion/jda/RequirementsWriter.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/test/java/net/dv8tion/jda/RequirementsWriter.java b/src/test/java/net/dv8tion/jda/RequirementsWriter.java new file mode 100644 --- /dev/null (date 1713903499355) +++ b/src/test/java/net/dv8tion/jda/RequirementsWriter.java (date 1713903499355) @@ -0,0 +1,103 @@ +package net.dv8tion.jda; + +import com.github.javaparser.ast.CompilationUnit; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.events.GenericEvent; +import net.dv8tion.jda.api.requests.GatewayIntent; +import net.dv8tion.jda.api.utils.cache.CacheFlag; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.*; +import java.util.stream.Collectors; + +public class RequirementsWriter +{ + public static void main(String[] args) throws Exception + { + final List compilationUnits = ParseUtil.parseEventCompilationUnits(); + + final Map, EnumSet> intentsByClass = ParseUtil.getEnumEntriesFromDocs(GatewayIntent.class, compilationUnits); + final Map, EnumSet> cacheFlagsByClass = ParseUtil.getEnumEntriesFromDocs(CacheFlag.class, compilationUnits); + final Map, EnumSet> permissionsByClass = ParseUtil.getEnumEntriesFromDocs(Permission.class, compilationUnits); + final List> classesRequiringMemberCache = ParseUtil.getClassesRequiringMemberCacheFromDocs(compilationUnits); + + // NOTE: this will write an annotation where everything is required, you have to move the optional ones. + for (CompilationUnit unit : compilationUnits) + { + final Path path = unit.getStorage().get().getPath(); + final List lines = Files.readAllLines(path); + final ListIterator listIterator = lines.listIterator(); + + int packageIndex = -1; + while (listIterator.hasNext()) { + final String line = listIterator.next(); + + if (line.startsWith("package")) { + packageIndex = listIterator.nextIndex() + 1; + } else if (line.startsWith("public class") || line.startsWith("public abstract class") || line.startsWith("public interface")) { + int classIndex = listIterator.previousIndex(); + final Class type = Class.forName(unit.getPrimaryType().get().getFullyQualifiedName().get()); + + final EnumSet intents = intentsByClass.get(type); + if (intents != null && !intents.isEmpty()) { + if (intents.size() == 1) + lines.add(classIndex, "@RequiredIntents(always = " + enumArray(intents) + ")"); + else + lines.add(classIndex, "@RequiredIntents(sometimes = " + enumArray(intents) + ")"); + lines.add(packageIndex, "import net.dv8tion.jda.api.events.annotations.RequiredIntents;"); + lines.add(packageIndex, "import net.dv8tion.jda.api.requests.GatewayIntent;"); + + classIndex += 3; + } + + final EnumSet cacheFlags = cacheFlagsByClass.get(type); + if (cacheFlags != null && !cacheFlags.isEmpty()) { + if (cacheFlags.size() == 1) + lines.add(classIndex, "@RequiredCacheFlags(always = " + enumArray(cacheFlags) + ")"); + else + lines.add(classIndex, "@RequiredCacheFlags(sometimes = " + enumArray(cacheFlags) + ")"); + lines.add(packageIndex, "import net.dv8tion.jda.api.events.annotations.RequiredCacheFlags;"); + lines.add(packageIndex, "import net.dv8tion.jda.api.utils.cache.CacheFlag;"); + + classIndex += 3; + } + + final EnumSet permissions = permissionsByClass.get(type); + if (permissions != null && !permissions.isEmpty()) { + lines.add(classIndex, "@RequiredPermissions(always = " + enumArray(permissions) + ")"); + lines.add(packageIndex, "import net.dv8tion.jda.api.events.annotations.RequiredPermissions;"); + lines.add(packageIndex, "import net.dv8tion.jda.api.Permission;"); + + classIndex += 3; + } + + if (classesRequiringMemberCache.contains(type)) { + lines.add(classIndex, "@RequiresCachedMember"); + lines.add(packageIndex, "import net.dv8tion.jda.api.events.annotations.RequiresCachedMember;"); + + classIndex += 2; + } + + break; + } + } + +// System.out.println(); + Files.write(path, lines, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } + } + + @NotNull + private static String enumArray(@NotNull EnumSet set) { + if (set.isEmpty()) { + throw new AssertionError(); + } else if (set.size() == 1) { + return set.stream().map(e -> e.getDeclaringClass().getSimpleName() + "." + e.name()).collect(Collectors.joining(", ")); + } else { + return "{" + set.stream().map(e -> e.getDeclaringClass().getSimpleName() + "." + e.name()).collect(Collectors.joining(", ")) + "}"; + } + } +}