improve source parsing to be more accurate

The previous way had several flaw and was not really future proof. This
new system is based on an iterative string reader similar to how brigadier parse
arguments which simplify a lot of things tracking the cursor internally.

The comments and javadoc are now properly skipped to prevent commented line to be counted as an import.

The first scope detection to stop the parser once all imports have been processed
now detect the first '{' char uncommented. Previously it only worked for api
gen and was really restrictive since it detected the following pattern: <class type> <class name>
as a workaround.
The annotations are also skipped to prevent curly bracket that could be in
it to be counted too (this include the arguments as well).

Type name extended on multi line are also supported and checked at
runtime as defined in the JLS:
https://docs.oracle.com/javase/specs/jls/se17/html/jls-3.html#jls-3.8
For fully qualified name, each part separated by a dot are checked. Keyword
are also checked to prevent @interface to be considered as a regular annotation
when it's more a class definition. Whitespace and comments in between type name are
stripped to collect the right import in the end. Annotation between the package
name and the class name are not supported but is not needed for
import and annotation since they are forbidden by the compiler.

Multi line stuff is handled using a list of tasks to process in the right order
for the imports or annotations with two types of task: repeat and basic.

This new system is now used in the marker comment detection and in the old generated
code test.

There's also a lot of tests to make sure no regression happen in the future
that can be enabled by inluding the tag 'parser' (disabled in CI runner by default).
Additionnaly marker stub are not anymore allowed by default while parsing import,
annotation and metadata stuff but can be reenabled with the system property
'paper.generator.rewriter.searchMarkerInMetadata' for experimental/testing
purpose.

Short type name resolution (based on the collected imports) has been redone to
supports static star import and inner class type referenced either implicitly or via import.
This commit is contained in:
Lulu13022002 2024-04-11 18:50:09 +02:00
parent a479d759be
commit 31fbaaa0c4
No known key found for this signature in database
GPG Key ID: 491C8F0B8ACDEB01
87 changed files with 2103 additions and 359 deletions

View File

@ -30,12 +30,25 @@ tasks.register<JavaExec>("generate") {
project(":paper-server").sourceSets["main"].java.srcDirs.first().toString())
}
tasks.test {
useJUnitPlatform()
systemProperty("paper.generator.rewriter.container.api", file("generated").toString()) // todo move to the sourceset
systemProperty("paper.generator.rewriter.container.server", file("generatedServerTest").toString()) // todo move to the sourceset
inputs.dir("generated")
inputs.dir("generatedServerTest")
tasks {
test {
useJUnitPlatform {
if (false && System.getenv()["CI"]?.toBoolean() == true) {
// the CI shouldn't run the test since it's not included by default but just in case this is moved to its own repo
excludeTags("parser")
} else {
// excludeTags("parser") // comment this line while working on parser related things
}
}
systemProperty("paper.generator.rewriter.container.api", file("generated").toString()) // todo move to the sourceset
systemProperty("paper.generator.rewriter.container.server", file("generatedServerTest").toString()) // todo move to the sourceset
inputs.dir("generated")
inputs.dir("generatedServerTest")
}
compileTestJava {
options.compilerArgs.add("-parameters")
}
}
group = "io.papermc.paper"

View File

@ -1,6 +1,6 @@
package io.papermc.generator;
import io.papermc.generator.rewriter.CompositeRewriter;
import io.papermc.generator.rewriter.replace.CompositeRewriter;
import io.papermc.generator.rewriter.SourceRewriter;
import io.papermc.generator.rewriter.types.EnumCloneRewriter;
import io.papermc.generator.rewriter.types.EnumRegistryRewriter;
@ -90,7 +90,6 @@ public interface Generators {
}
SourceRewriter[] API_REWRITE = {
//new EnumCloneRewriter(Pose.class, net.minecraft.world.entity.Pose.class, "Pose", false)
new EnumRegistryRewriter<>(Fluid.class, Registries.FLUID, "Fluid", false),
new EnumRegistryRewriter<>(Sound.class, Registries.SOUND_EVENT, "Sound", true) {
@Override
@ -191,7 +190,7 @@ public interface Generators {
new RegistryFieldRewriter<>(Wolf.Variant.class, Registries.WOLF_VARIANT, "WolfVariant", "getVariant"),
new MemoryKeyRewriter("MemoryKey"),
new TagRewriter(Tag.class, "Tag"),
new MapPaletteRewriter("MapPalette#colors"),
new MapPaletteRewriter("MapPalette#colors")
};

View File

@ -2,6 +2,7 @@ package io.papermc.generator.rewriter;
import io.papermc.generator.utils.ClassHelper;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
public record ClassNamed(String packageName, String simpleName, String dottedNestedName, @Nullable Class<?> knownClass) {
@ -42,6 +43,35 @@ public record ClassNamed(String packageName, String simpleName, String dottedNes
return this;
}
public boolean isRoot() {
return this.root() == this;
}
public ClassNamed parent() {
if (this.knownClass != null) {
Class<?> parentClass = this.knownClass.getEnclosingClass();
if (parentClass == null) {
return this;
}
return new ClassNamed(parentClass);
}
int dotIndex = this.dottedNestedName.lastIndexOf('.');
if (dotIndex != -1) {
String name = this.dottedNestedName.substring(0, dotIndex);
int lastDotIndex = name.lastIndexOf('.');
final String simpleName;
if (lastDotIndex != -1) {
simpleName = name.substring(lastDotIndex + 1);
} else {
simpleName = name; // root
}
return new ClassNamed(this.packageName, simpleName, name, null);
}
return this;
}
public String canonicalName() {
if (this.knownClass != null) {
return this.knownClass.getCanonicalName();
@ -50,4 +80,28 @@ public record ClassNamed(String packageName, String simpleName, String dottedNes
return this.packageName + '.' + this.dottedNestedName;
}
@Override
public int hashCode() {
if (this.knownClass != null) {
return this.knownClass.hashCode();
}
return Objects.hash(this.packageName, this.dottedNestedName);
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o == null || o.getClass() != this.getClass()) {
return false;
}
ClassNamed other = (ClassNamed) o;
if (this.knownClass != null && other.knownClass != null) {
return this.knownClass == other.knownClass;
}
return this.packageName.equals(other.packageName) &&
this.dottedNestedName.equals(other.dottedNestedName);
}
}

View File

@ -1,213 +0,0 @@
package io.papermc.generator.rewriter;
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
import io.papermc.generator.Main;
import io.papermc.generator.rewriter.utils.Annotations;
import io.papermc.generator.rewriter.context.ImportCollector;
import io.papermc.generator.rewriter.context.ImportTypeCollector;
import io.papermc.generator.utils.ClassHelper;
import io.papermc.generator.utils.Formatting;
import io.papermc.paper.generated.GeneratedFrom;
import net.minecraft.SharedConstants;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.VisibleForTesting;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
public class SearchReplaceRewriter implements SourceRewriter {
@VisibleForTesting
public static final String INDENT_UNIT = " ";
@VisibleForTesting
public static final String PAPER_START_FORMAT = "Paper start";
private static final String PAPER_END_FORMAT = "Paper end";
@VisibleForTesting
public static final String GENERATED_COMMENT_FORMAT = "// %s - Generated/%s"; // {0} = PAPER_START_FORMAT|PAPER_END_FORMAT {1} = pattern
protected final ClassNamed rewriteClass;
protected final String pattern;
protected final boolean equalsSize;
public SearchReplaceRewriter(Class<?> rewriteClass, String pattern, boolean equalsSize) {
this(new ClassNamed(rewriteClass), pattern, equalsSize);
}
public SearchReplaceRewriter(ClassNamed rewriteClass, String pattern, boolean equalsSize) {
this.rewriteClass = rewriteClass;
this.pattern = pattern;
this.equalsSize = equalsSize;
}
// only when equalsSize = false
@ApiStatus.OverrideOnly
protected void insert(SearchMetadata metadata, StringBuilder builder) {}
// only when equalsSize = true
@ApiStatus.OverrideOnly
protected void replaceLine(SearchMetadata metadata, StringBuilder builder) {}
protected void beginSearch() {}
protected SearchReplaceRewriter getRewriterFor(String pattern) {
return this;
}
protected Set<String> getPatterns() {
return Set.of(this.pattern);
}
private void searchAndReplace(BufferedReader reader, StringBuilder content) throws IOException {
Set<String> patterns = this.getPatterns();
Preconditions.checkState(!patterns.isEmpty());
this.beginSearch();
Set<String> foundPatterns = new HashSet<>();
StringBuilder strippedContent = null;
final ImportCollector importCollector = new ImportTypeCollector(this.rewriteClass.root());
String rootClassDeclaration = null;
if (this.rewriteClass.knownClass() != null) {
Class<?> rootClass = ClassHelper.getRootClass(this.rewriteClass.knownClass());
rootClassDeclaration = "%s %s".formatted(ClassHelper.getDeclaredType(rootClass), this.rewriteClass.root().simpleName()); // this should be improved to take in account server gen and comments
}
String indent = Formatting.incrementalIndent(INDENT_UNIT, this.rewriteClass);
SearchReplaceRewriter foundRewriter = null;
boolean inBody = false;
int i = 0;
while (true) {
String line = reader.readLine();
if (line == null) {
break;
}
// collect import to avoid fqn when not needed
if (importCollector != ImportCollector.NO_OP && !inBody) {
if (line.startsWith("import ") && line.endsWith(";")) {
importCollector.consume(line);
}
if (rootClassDeclaration != null && line.contains(rootClassDeclaration)) { // might fail on comments but good enough
inBody = true;
}
}
Optional<String> endPattern = this.searchPattern(line, indent, PAPER_END_FORMAT, patterns);
if (endPattern.isPresent()) {
if (foundRewriter == null) {
throw new IllegalStateException("Start generated comment missing for pattern " + endPattern.get() + " in class " + this.rewriteClass.simpleName() + " at line " + (i + 1));
}
if (foundRewriter.pattern.equals(endPattern.get())) {
if (!foundRewriter.equalsSize) {
appendGeneratedComment(content, indent);
foundRewriter.insert(new SearchMetadata(importCollector, indent, strippedContent.toString(), i), content);
strippedContent = null;
}
foundRewriter = null;
} else {
throw new IllegalStateException("End generated comment doesn't match for pattern " + foundRewriter.pattern + " in " + this.rewriteClass.simpleName() + " at line " + (i + 1));
}
}
if (foundRewriter == null) {
content.append(line);
content.append('\n');
} else {
if (foundRewriter.equalsSize) {
// there's no generated comment here since when the size is equals the replaced content doesn't depend on the game content
// if it does that means the replaced content might not be equals during MC update because of adding/removed content
foundRewriter.replaceLine(new SearchMetadata(importCollector, indent, line, i), content);
} else {
strippedContent.append(line);
strippedContent.append('\n');
}
}
Optional<String> startPattern = this.searchPattern(line, null, PAPER_START_FORMAT, patterns);
if (startPattern.isPresent()) {
if (foundRewriter != null) {
throw new IllegalStateException("Nested generated comments are not allowed for " + this.rewriteClass.simpleName() + " at line " + (i + 1));
}
int startPatternIndex = line.indexOf(GENERATED_COMMENT_FORMAT.formatted(PAPER_START_FORMAT, startPattern.get()));
indent = " ".repeat(startPatternIndex); // update indent based on the comments for flexibility
if (indent.length() % INDENT_UNIT.length() != 0) {
throw new IllegalStateException("Start generated comment is not properly indented at line " + (i + 1));
}
foundRewriter = this.getRewriterFor(startPattern.get());
foundPatterns.add(foundRewriter.pattern);
if (!foundRewriter.equalsSize) {
strippedContent = new StringBuilder();
}
}
i++;
}
if (foundRewriter != null) {
throw new IllegalStateException("End generated comment " + foundRewriter.pattern + " missing for " + this.rewriteClass.simpleName());
}
Set<String> diff = Sets.difference(patterns, foundPatterns);
if (!diff.isEmpty()) {
throw new IllegalStateException("SRT didn't found some expected generated comments: " + diff.toString());
}
}
protected String getFilePath() {
return "%s/%s.java".formatted(
this.rewriteClass.packageName().replace('.', '/'),
this.rewriteClass.root().simpleName()
);
}
@Override
public void writeToFile(Path parent) throws IOException {
String filePath = this.getFilePath();
Path path = parent.resolve(filePath);
StringBuilder content = new StringBuilder();
try (BufferedReader buffer = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
this.searchAndReplace(buffer, content);
}
// Files.writeString(path, content.toString(), StandardCharsets.UTF_8); // todo
Path createdPath;
if (path.toString().contains("Paper/Paper-API/src/")) {
createdPath = Main.generatedPath.resolve(filePath);
} else {
createdPath = Main.generatedServerPath.resolve(filePath);
}
Files.createDirectories(createdPath.getParent());
Files.writeString(createdPath, content, StandardCharsets.UTF_8);
}
private void appendGeneratedComment(StringBuilder builder, String indent) {
builder.append(indent).append("// %s %s".formatted(
Annotations.annotationStyle(GeneratedFrom.class),
SharedConstants.getCurrentVersion().getName()
));
builder.append('\n');
}
private Optional<String> searchPattern(String rawLine, @Nullable String indent, String prefix, Set<String> patterns) {
boolean strict = indent != null;
String line = strict ? rawLine : rawLine.stripLeading();
for (String pattern : patterns) {
String comment = GENERATED_COMMENT_FORMAT.formatted(prefix, pattern);
if (strict ? line.equals(indent + comment) : line.equals(comment)) {
return Optional.of(pattern);
}
}
return Optional.empty();
}
}

View File

@ -1,28 +1,42 @@
package io.papermc.generator.rewriter.context;
import io.papermc.generator.rewriter.ClassNamed;
public interface ImportCollector {
ImportCollector NO_OP = new ImportCollector() {
@Override
public String getStaticAlias(final String fqn) {
return fqn;
public void addImport(final String typeName) {
}
@Override
public String getTypeName(final Class<?> clazz) {
return clazz.getCanonicalName();
public void addStaticImport(final String typeName) {
}
@Override
public void consume(final String line) {
public String getStaticMemberShortName(final String fullName) {
return fullName;
}
@Override
public String getShortName(final ClassNamed type) {
return type.canonicalName();
}
};
String getStaticAlias(String fqn);
void addImport(String typeName);
String getTypeName(Class<?> clazz);
void addStaticImport(String typeName);
void consume(String line);
String getStaticMemberShortName(String fullName);
default String getShortName(Class<?> type) {
return this.getShortName(new ClassNamed(type));
}
String getShortName(ClassNamed type);
}

View File

@ -1,8 +1,11 @@
package io.papermc.generator.rewriter.context;
import io.papermc.generator.rewriter.ClassNamed;
import io.papermc.generator.utils.ClassHelper;
import org.jetbrains.annotations.VisibleForTesting;
import io.papermc.generator.rewriter.parser.StringReader;
import io.papermc.generator.utils.Formatting;
import it.unimi.dsi.fastutil.Pair;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
@ -10,11 +13,13 @@ import java.util.Set;
public class ImportTypeCollector implements ImportCollector {
private final Map<Class<?>, String> typeCache = new HashMap<>();
private final Map<ClassNamed, String> typeCache = new HashMap<>();
private final Set<String> imports = new HashSet<>();
private final Set<String> globalImports = new HashSet<>();
private final Map<String, String> staticImports = new HashMap<>(); // <fqn.alias:alias>
private final Map<String, String> staticImports = new HashMap<>(); // <fqn.id:id>
private final Set<String> globalStaticImports = new HashSet<>();
private final ClassNamed rewriteClass;
@ -22,56 +27,130 @@ public class ImportTypeCollector implements ImportCollector {
this.rewriteClass = rewriteClass;
}
@VisibleForTesting
public void addImport(String fqn) {
if (fqn.endsWith("*")) {
this.globalImports.add(fqn.substring(0, fqn.lastIndexOf('.')));
@Override
public void addImport(String typeName) {
if (typeName.endsWith("*")) {
this.globalImports.add(typeName.substring(0, typeName.lastIndexOf('.')));
} else {
this.imports.add(fqn);
this.imports.add(typeName);
}
}
@VisibleForTesting
public void addStaticImport(String fqn) { // todo support star import?
this.staticImports.put(fqn, fqn.substring(fqn.lastIndexOf('.') + 1));
@Override
public void addStaticImport(String typeName) {
if (typeName.endsWith("*")) {
this.globalStaticImports.add(typeName.substring(0, typeName.lastIndexOf('.')));
} else {
this.staticImports.put(typeName, typeName.substring(typeName.lastIndexOf('.') + 1));
}
}
@Override
public String getStaticAlias(String fqn) {
return this.staticImports.getOrDefault(fqn, fqn);
public String getStaticMemberShortName(String fullName) {
if (this.staticImports.containsKey(fullName)) {
return this.staticImports.get(fullName);
}
// global imports
int lastDotIndex = fullName.lastIndexOf('.');
if (lastDotIndex == -1) {
return fullName;
}
String parentCanonicalName = fullName.substring(0, lastDotIndex);
if (this.globalStaticImports.contains(parentCanonicalName)) {
return fullName.substring(lastDotIndex + 1);
}
return fullName;
}
@Override
public String getTypeName(Class<?> clazz) {
return this.typeCache.computeIfAbsent(clazz, type -> {
Class<?> rootClass = ClassHelper.getRootClass(type);
final String typeName;
if (this.imports.contains(rootClass.getName()) ||
type.getPackageName().equals(this.rewriteClass.packageName()) || // same package don't need fqn too (include self class too)
this.globalImports.contains(type.getPackageName())) { // star import
typeName = ClassHelper.retrieveFullNestedName(type);
} else {
typeName = type.getCanonicalName();
private String getShortName0(ClassNamed type, Set<String> imports, Set<String> globalImports, boolean fetchStatic) {
ClassNamed foundClass = type;
int advancedNode = 0;
while (!imports.contains(foundClass.canonicalName()) &&
!globalImports.contains(foundClass.parent().canonicalName())) {
if (foundClass.isRoot() || // top classes with package check is handled before
(fetchStatic && !Modifier.isStatic(foundClass.knownClass().getModifiers())) // static imports are allowed for regular class too but only when the inner classes are all static
) {
foundClass = null;
break;
}
foundClass = foundClass.parent();
advancedNode++;
}
if (foundClass != null) {
String typeName;
if (advancedNode > 0) { // direct inner class import
String originalNestedName = type.dottedNestedName();
int skipNode = Formatting.countOccurrences(originalNestedName, '.') - advancedNode;
StringReader reader = new StringReader(originalNestedName);
while (skipNode > 0) {
reader.skipString('.');
reader.skip(); // skip dot
skipNode--;
}
typeName = reader.readRemainingString();
} else {
typeName = type.simpleName();
}
return typeName;
}
return type.canonicalName();
}
@Override
public String getShortName(ClassNamed type) {
return this.typeCache.computeIfAbsent(type, key -> {
if (key.knownClass() != null && Modifier.isStatic(key.knownClass().getModifiers())) {
// this is only supported when the class is known for now but generally static imports should stick to member of class not the class itself
String name = getShortName0(key, this.staticImports.keySet(), this.globalStaticImports, true);
if (!name.equals(key.canonicalName())) {
return name;
}
}
if ((key.parent().isRoot() && this.globalImports.contains(key.packageName())) || // star import on package for top classes and one level classes only!
(key.isRoot() && key.packageName().equals(this.rewriteClass.packageName()))) { // same package don't need fqn too for top classes
return key.dottedNestedName();
}
// self classes (with inner classes)
// todo rework this logic order should be smth like: root stuff -> regular import -> inner stuff (-> static import if valid)
// and remove the implicit part too
Set<String> currentImports = this.imports;
if (key.packageName().equals(this.rewriteClass.packageName()) &&
this.rewriteClass.root().equals(key.root())) {
int depth = Formatting.countOccurrences(key.dottedNestedName(), '.');
int fromDepth = Formatting.countOccurrences(this.rewriteClass.dottedNestedName(), '.');
if (fromDepth < depth) {
ClassNamed parent = key;
while (true) {
ClassNamed up = parent.parent();
if (this.rewriteClass.equals(up)) {
break;
}
parent = up;
}
currentImports = new HashSet<>(this.imports);
currentImports.add(parent.canonicalName()); // implicit import
} else {
return type.simpleName();
}
}
return getShortName0(key, currentImports, this.globalImports, false);
});
}
private void addImportLine(String importLine) {
if (importLine.startsWith("import static ")) {
addStaticImport(importLine.substring("import static ".length()));
} else {
addImport(importLine.substring("import ".length()));
}
@VisibleForTesting
public Pair<Set<String>, Set<String>> getImports() {
return Pair.of(this.imports, this.globalImports);
}
@Override
public void consume(String line) {
for (String rawImport : line.split(";")) {
String importLine = rawImport.trim();
if (importLine.startsWith("import ")) {
addImportLine(importLine);
}
}
@VisibleForTesting
public Pair<Set<String>, Set<String>> getStaticImports() {
return Pair.of(this.staticImports.keySet(), this.globalStaticImports);
}
}

View File

@ -0,0 +1,16 @@
package io.papermc.generator.rewriter.parser;
public enum ClosureType {
PARENTHESIS("(", ")"),
CURLY_BRACKET("{", "}"),
BRACKET("[", "]"),
COMMENT("/*", "*/");
public final String start;
public final String end;
ClosureType(String start, String end) {
this.start = start;
this.end = end;
}
}

View File

@ -0,0 +1,139 @@
package io.papermc.generator.rewriter.parser;
import io.papermc.generator.rewriter.context.ImportCollector;
import io.papermc.generator.rewriter.parser.step.IterativeStep;
import io.papermc.generator.rewriter.parser.step.StepHolder;
import io.papermc.generator.rewriter.parser.step.factory.AnnotationSteps;
import io.papermc.generator.rewriter.parser.step.factory.ImportSteps;
import org.jetbrains.annotations.ApiStatus;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.EnumSet;
import java.util.Set;
@ApiStatus.Internal
public class LineParser {
private final Set<ClosureType> closures = EnumSet.noneOf(ClosureType.class);
private final Deque<IterativeStep> steps = new ArrayDeque<>(10);
public boolean advanceEnclosure(ClosureType type, StringReader line) {
boolean inside = this.closures.contains(type);
String closure = !inside ? type.start : type.end;
if (line.trySkipString(closure)) { // closure has been consumed
if (!inside) {
return this.closures.add(type);
} else {
return this.closures.remove(type);
}
}
return false;
}
public boolean skipComment(StringReader line) {
int previousCursor = line.getCursor();
if (this.closures.contains(ClosureType.COMMENT) || this.advanceEnclosure(ClosureType.COMMENT, line)) { // open comment?
while (!this.advanceEnclosure(ClosureType.COMMENT, line) && line.canRead()) { // closed comment?
line.skip();
}
return line.getCursor() > previousCursor;
}
return false;
}
public boolean skipCommentOrWhitespace(StringReader line) {
boolean skipped = false;
while (this.skipComment(line) || line.skipWhitespace() > 0) {
skipped = true;
}
return skipped;
}
public boolean trySkipCommentOrWhitespaceUntil(StringReader line, char terminator) {
int previousCursor = line.getCursor();
boolean skipped = this.skipCommentOrWhitespace(line);
if (skipped && line.canRead() && line.peek() != terminator) {
line.setCursor(previousCursor);
skipped = false;
}
return skipped;
}
public boolean skipCommentOrWhitespaceInName(StringReader line, NameCursorState state) {
if (state == NameCursorState.AFTER_DOT) {
return this.skipCommentOrWhitespace(line);
} else if (state == NameCursorState.INVALID_CHAR) { // this is tricky todo redo this part later
boolean skipped = this.trySkipCommentOrWhitespaceUntil(line, '.');
int previousCursor = line.getCursor();
if (!skipped && this.skipCommentOrWhitespace(line) && line.canRead() && this.nextSingleLineComment(line)) {
// ignore single line comment at the end of the name
line.setCursor(line.getTotalLength());
skipped = true;
} else {
line.setCursor(previousCursor);
}
return skipped;
}
return false;
}
public boolean nextSingleLineComment(StringReader line) {
return line.peek() == '/' && line.canRead(2) && line.peek(1) == '/';
}
public boolean consumeImports(StringReader line, ImportCollector collector) {
outerLoop:
while (line.canRead()) {
IterativeStep step;
while ((step = this.steps.poll()) != null) {
step.run(line, this);
if (!line.canRead()) {
break outerLoop;
}
}
if (this.skipCommentOrWhitespace(line)) {
continue;
}
if (this.nextSingleLineComment(line)) {
// check single line comment only after multi line to avoid ignoring the end of multi line comment starting with // on the newline
break;
}
// not commented
char c = line.peek();
if (AnnotationSteps.canStart(c)) { // handle annotation with param to avoid open curly bracket that occur in array argument
this.enqueue(new AnnotationSteps());
continue;
} else if (c == '{') {
return true;
} else if (ImportSteps.canStart(line)) {
this.enqueue(new ImportSteps(collector));
continue;
}
line.skip();
}
return false;
}
private void enqueue(StepHolder holder) {
for (IterativeStep step : holder.initialSteps()) {
if (!this.steps.offerLast(step)) {
throw new IllegalStateException("Cannot add a step into the queue!");
}
}
}
public void addPriorityStep(IterativeStep step) {
if (!this.steps.offerFirst(step)) {
throw new IllegalStateException("Cannot add a priority step into the queue!");
}
}
public void clearRemainingSteps() {
this.steps.clear();
}
}

View File

@ -0,0 +1,6 @@
package io.papermc.generator.rewriter.parser;
public enum NameCursorState {
AFTER_DOT,
INVALID_CHAR
}

View File

@ -0,0 +1,34 @@
package io.papermc.generator.rewriter.parser;
public class ProtoTypeName {
private final String initialName;
private StringBuilder currentName;
private ProtoTypeName(String name) {
this.initialName = name;
}
public void append(String part) {
if (this.currentName == null) {
this.currentName = new StringBuilder(this.initialName);
}
this.currentName.append(part);
}
public String getName() {
return this.currentName != null ? this.currentName.toString() : this.initialName;
}
public boolean shouldCheckStartIdentifier() {
if (this.currentName != null) {
return this.currentName.isEmpty() || this.currentName.lastIndexOf(".") == this.currentName.length() - 1;
}
return this.initialName.isEmpty() || this.initialName.lastIndexOf('.') == this.initialName.length() - 1;
}
public static ProtoTypeName create(String name) {
return new ProtoTypeName(name);
}
}

View File

@ -0,0 +1,216 @@
package io.papermc.generator.rewriter.parser;
import com.mojang.brigadier.ImmutableStringReader;
import org.jetbrains.annotations.Nullable;
import java.util.function.BiPredicate;
import java.util.function.BooleanSupplier;
// based on brigadier string reader with some extra/removed features for rewriter
public class StringReader implements ImmutableStringReader {
private final String string;
private int cursor;
public StringReader(final StringReader other) {
this.string = other.string;
this.cursor = other.cursor;
}
public StringReader(final String string) {
this.string = string;
}
@Override
public String getString() {
return string;
}
public void setCursor(final int cursor) {
this.cursor = cursor;
}
@Override
public int getRemainingLength() {
return string.length() - cursor;
}
@Override
public int getTotalLength() {
return string.length();
}
@Override
public int getCursor() {
return cursor;
}
@Override
public String getRead() {
return string.substring(0, cursor);
}
@Override
public String getRemaining() {
return string.substring(cursor);
}
@Override
public boolean canRead(final int length) {
return cursor + length <= string.length();
}
@Override
public boolean canRead() {
return canRead(1);
}
@Override
public char peek() {
return string.charAt(cursor);
}
@Override
public char peek(final int offset) {
return string.charAt(cursor + offset);
}
public char read() {
return string.charAt(cursor++);
}
public void skip() {
cursor++;
}
// new features
public int peekPoint() {
return this.string.codePointAt(this.cursor);
}
public int skipWhitespace() {
int i = 0;
while (canRead() && Character.isWhitespace(peek())) {
this.skip();
i++;
}
return i;
}
public int skipChars(final char value) {
int i = 0;
while (this.canRead() && this.peek() == value) {
this.skip();
i++;
}
return i;
}
public void skipString(final char terminator) {
while (this.canRead() && this.peek() != terminator) {
this.skip();
}
}
public boolean trySkipWhitespace(final int size) {
int delta = 0;
int previousCursor = this.cursor;
while (delta < size && this.canRead() && Character.isWhitespace(this.peek())) {
this.skip();
delta++;
}
if (delta == size) {
return true;
}
this.setCursor(previousCursor);
return false;
}
public boolean trySkipChars(final int size, final char value) {
int delta = 0;
int previousCursor = this.cursor;
while (delta < size && this.canRead() && this.peek() == value) {
this.skip();
delta++;
}
if (delta == size) {
return true;
}
this.setCursor(previousCursor);
return false;
}
public boolean trySkipString(final String value) {
char[] chars = value.toCharArray();
int delta = 0;
int previousCursor = this.cursor;
while (this.canRead() && delta < chars.length && chars[delta] == this.peek()) {
this.skip();
delta++;
}
if (delta == chars.length) {
return true;
}
this.setCursor(previousCursor);
return false;
}
public String readRemainingString() {
final int start = this.cursor;
this.cursor = this.getTotalLength();
return this.string.substring(start);
}
public String readStringUntil(final char terminator) {
final int start = this.cursor;
this.skipString(terminator);
return this.string.substring(start, this.cursor);
}
@Nullable
public String getStringUntil(final char terminator) {
final int start = this.cursor;
this.skipString(terminator);
if (!this.canRead()) {
return null;
}
return this.string.substring(start, this.cursor);
}
// cleaner is used to skip stuff like : net/* hi */./**/kyori.adventure.translation/**/.Translatable within the type name
public String getPartNameUntil(final char terminator, final BiPredicate<StringReader, NameCursorState> cleaner, final boolean forImport, final BooleanSupplier checkStartGetter) { // this break the concept of this a class a bit but it's not worth making a code point equivalent for only this method
boolean hasCleaner = cleaner != null;
boolean checkStart = checkStartGetter.getAsBoolean();
StringBuilder name = new StringBuilder();
while (this.canRead()) {
int c = this.peekPoint();
if (c == terminator) {
break;
}
if (checkStart) { // had a dot before
if (hasCleaner && cleaner.test(this, NameCursorState.AFTER_DOT)) {
continue;
}
}
boolean isJavaIdChar = checkStart ? Character.isJavaIdentifierStart(c) : Character.isJavaIdentifierPart(c);
if (!isJavaIdChar && (checkStart || c != '.') && !(c == '*' && forImport)) { // star should be allowed only at the end for import todo
if (hasCleaner && cleaner.test(this, NameCursorState.INVALID_CHAR)) {
continue;
} else {
break;
}
}
char[] chars = Character.toChars(c);
name.append(chars);
this.cursor += chars.length;
checkStart = c == '.';
}
return name.toString();
}
}

View File

@ -0,0 +1,19 @@
package io.papermc.generator.rewriter.parser.step;
import io.papermc.generator.rewriter.parser.LineParser;
import io.papermc.generator.rewriter.parser.StringReader;
import java.util.function.BiConsumer;
public class BasicIterativeStep implements IterativeStep {
private final BiConsumer<StringReader, LineParser> runner;
protected BasicIterativeStep(BiConsumer<StringReader, LineParser> runner) {
this.runner = runner;
}
@Override
public void run(StringReader line, LineParser parser) {
this.runner.accept(line, parser);
}
}

View File

@ -0,0 +1,20 @@
package io.papermc.generator.rewriter.parser.step;
import io.papermc.generator.rewriter.parser.LineParser;
import io.papermc.generator.rewriter.parser.StringReader;
import java.util.function.BiConsumer;
import java.util.function.BiPredicate;
public interface IterativeStep {
void run(StringReader line, LineParser parser);
static IterativeStep create(BiConsumer<StringReader, LineParser> runner) {
return new BasicIterativeStep(runner);
}
static IterativeStep createUntil(BiPredicate<StringReader, LineParser> runner) {
return new RepeatIterativeStep(runner);
}
}

View File

@ -0,0 +1,21 @@
package io.papermc.generator.rewriter.parser.step;
import io.papermc.generator.rewriter.parser.LineParser;
import io.papermc.generator.rewriter.parser.StringReader;
import java.util.function.BiPredicate;
public class RepeatIterativeStep implements IterativeStep {
private final BiPredicate<StringReader, LineParser> runner;
protected RepeatIterativeStep(BiPredicate<StringReader, LineParser> runner) {
this.runner = runner;
}
@Override
public void run(StringReader line, LineParser parser) {
if (this.runner.test(line, parser)) {
parser.addPriorityStep(this);
}
}
}

View File

@ -0,0 +1,6 @@
package io.papermc.generator.rewriter.parser.step;
public interface StepHolder {
IterativeStep[] initialSteps();
}

View File

@ -0,0 +1,97 @@
package io.papermc.generator.rewriter.parser.step.factory;
import io.papermc.generator.rewriter.parser.ClosureType;
import io.papermc.generator.rewriter.parser.LineParser;
import io.papermc.generator.rewriter.parser.ProtoTypeName;
import io.papermc.generator.rewriter.parser.step.IterativeStep;
import io.papermc.generator.rewriter.parser.step.StepHolder;
import io.papermc.generator.rewriter.parser.StringReader;
import io.papermc.generator.utils.NamingManager;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// start once "@" is detected unless commented
// order is: skipAtSign -> skipPartName -> checkOpenParenthesis (-> skipParentheses)
public final class AnnotationSteps implements StepHolder {
public static boolean canStart(char currentChar) {
return currentChar == '@';
}
private final IterativeStep skipParenthesesStep = IterativeStep.createUntil(this::skipParentheses);
private @MonotonicNonNull ProtoTypeName name;
public void skipAtSign(StringReader line, LineParser parser) {
line.skip(); // skip @
}
public boolean skipPartName(StringReader line, LineParser parser) {
boolean checkStartId = this.name == null || this.name.shouldCheckStartIdentifier();
if (!checkStartId) { // this part is not in the import steps since import always need a semicolon at their end so it's easier to parse them
if (!parser.trySkipCommentOrWhitespaceUntil(line, '.')) { // expect a dot for multi line annotation when the previous line doesn't end by a dot itself
return false;
}
} else {
parser.skipCommentOrWhitespace(line);
}
if (!line.canRead()) {
return true;
}
String name = line.getPartNameUntil('(', parser::skipCommentOrWhitespaceInName, false,
() -> checkStartId);
if (line.canRead() && parser.nextSingleLineComment(line)) {
// ignore single line comment at the end and allow the name to continue
line.setCursor(line.getTotalLength());
}
if (this.name == null) {
this.name = ProtoTypeName.create(name);
} else {
this.name.append(name);
}
return !line.canRead();
}
public boolean skipParentheses(StringReader line, LineParser parser) {
while (!parser.advanceEnclosure(ClosureType.PARENTHESIS, line)) { // closed parenthesis?
if (!line.canRead()) { // parenthesis on another line?
return true;
}
line.skip();
}
return false;
}
// filter out @interface
public void checkAnnotationName(StringReader line, LineParser parser) {
String name = this.name.getName();
if (name.isEmpty() || NamingManager.hasIllegalKeyword(name)) { // keyword are checked after to simplify things
parser.clearRemainingSteps();
}
}
public boolean checkOpenParenthesis(StringReader line, LineParser parser) {
parser.skipCommentOrWhitespace(line); // since skipPartName fail fast this is needed for space between the typeName and the parenthesis
if (!line.canRead()) {
return true;
}
if (parser.advanceEnclosure(ClosureType.PARENTHESIS, line)) { // open parenthesis?
parser.addPriorityStep(this.skipParenthesesStep);
}
return false;
}
@Override
public IterativeStep[] initialSteps() {
return new IterativeStep[] {
IterativeStep.create(this::skipAtSign),
IterativeStep.createUntil(this::skipPartName),
IterativeStep.create(this::checkAnnotationName),
IterativeStep.createUntil(this::checkOpenParenthesis),
};
}
}

View File

@ -0,0 +1,101 @@
package io.papermc.generator.rewriter.parser.step.factory;
import io.papermc.generator.rewriter.context.ImportCollector;
import io.papermc.generator.rewriter.parser.LineParser;
import io.papermc.generator.rewriter.parser.ProtoTypeName;
import io.papermc.generator.rewriter.parser.step.IterativeStep;
import io.papermc.generator.rewriter.parser.step.StepHolder;
import io.papermc.generator.rewriter.parser.StringReader;
import io.papermc.generator.utils.NamingManager;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// start once "import" is detected unless commented
// order is: enforceSpace -> checkStatic (-> enforceSpace) -> getPartName -> collectImport
public final class ImportSteps implements StepHolder {
public static boolean canStart(StringReader line) {
return line.trySkipString("import");
}
private final IterativeStep enforceSpaceStep = IterativeStep.create(this::enforceSpace);
private final ImportCollector collector;
private boolean isStatic;
private @MonotonicNonNull ProtoTypeName name;
public ImportSteps(ImportCollector collector) {
this.collector = collector;
}
public void enforceSpace(StringReader line, LineParser parser) {
if (line.canRead() && parser.nextSingleLineComment(line)) {
// ignore single line comment at the end of import/static
line.setCursor(line.getTotalLength());
return;
}
if (!parser.skipComment(line)) {
if (line.skipWhitespace() == 0) { // expect at least one space between import, static and type name unless a multi comment is here to fill the gap
parser.clearRemainingSteps();
}
}
}
public boolean checkStatic(StringReader line, LineParser parser) {
parser.skipCommentOrWhitespace(line);
if (!line.canRead()) {
return true;
}
if (line.trySkipString("static")) {
parser.addPriorityStep(this.enforceSpaceStep);
this.isStatic = true;
}
return false;
}
public void collectImport(StringReader line, LineParser parser) {
String name = this.name.getName();
if (name.isEmpty() || NamingManager.hasIllegalKeyword(name)) { // keyword are checked after to simplify things
return;
}
if (this.isStatic) {
this.collector.addStaticImport(name);
} else {
this.collector.addImport(name);
}
}
public boolean getPartName(StringReader line, LineParser parser) {
parser.skipCommentOrWhitespace(line);
if (!line.canRead()) {
return true;
}
String name = line.getPartNameUntil(';', parser::skipCommentOrWhitespaceInName, true,
() -> this.name == null || this.name.shouldCheckStartIdentifier());
if (line.canRead() && parser.nextSingleLineComment(line)) {
// ignore single line comment at the end of the name
line.setCursor(line.getTotalLength());
}
if (this.name == null) {
this.name = ProtoTypeName.create(name);
} else {
this.name.append(name);
}
return !line.canRead();
}
@Override
public IterativeStep[] initialSteps() {
return new IterativeStep[] {
this.enforceSpaceStep,
IterativeStep.createUntil(this::checkStatic),
IterativeStep.createUntil(this::getPartName),
IterativeStep.create(this::collectImport)
};
}
}

View File

@ -0,0 +1,10 @@
package io.papermc.generator.rewriter.replace;
public record CommentMarker(String pattern, boolean start, int indent) {
private CommentMarker() {
this("", false, 0);
}
public static final CommentMarker EMPTY_MARKER = new CommentMarker();
}

View File

@ -1,6 +1,7 @@
package io.papermc.generator.rewriter;
package io.papermc.generator.rewriter.replace;
import com.google.common.base.Preconditions;
import io.papermc.generator.rewriter.ClassNamed;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
@ -24,13 +25,23 @@ public class CompositeRewriter extends SearchReplaceRewriter {
}
}
@Override
public boolean isVersionDependant() {
for (SearchReplaceRewriter rewriter : this.getRewriters()) {
if (rewriter.isVersionDependant()) {
return true;
}
}
return false;
}
@Override
protected SearchReplaceRewriter getRewriterFor(String pattern) {
return this.rewriterByPattern.get(pattern);
}
@Override
protected Set<String> getPatterns() {
public Set<String> getPatterns() {
return this.rewriterByPattern.keySet();
}

View File

@ -1,4 +1,4 @@
package io.papermc.generator.rewriter;
package io.papermc.generator.rewriter.replace;
import io.papermc.generator.rewriter.context.ImportCollector;

View File

@ -0,0 +1,252 @@
package io.papermc.generator.rewriter.replace;
import com.google.common.base.Preconditions;
import io.papermc.generator.Main;
import io.papermc.generator.rewriter.ClassNamed;
import io.papermc.generator.rewriter.SourceRewriter;
import io.papermc.generator.rewriter.parser.LineParser;
import io.papermc.generator.rewriter.utils.Annotations;
import io.papermc.generator.rewriter.context.ImportCollector;
import io.papermc.generator.rewriter.context.ImportTypeCollector;
import io.papermc.generator.rewriter.parser.StringReader;
import io.papermc.generator.utils.Formatting;
import io.papermc.paper.generated.GeneratedFrom;
import net.minecraft.SharedConstants;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.VisibleForTesting;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Set;
import static io.papermc.generator.rewriter.replace.CommentMarker.EMPTY_MARKER;
public class SearchReplaceRewriter implements SourceRewriter {
protected static final String INDENT_UNIT = " ";
@VisibleForTesting
public static final int INDENT_SIZE = INDENT_UNIT.length();
@VisibleForTesting
public static final char INDENT_CHAR = INDENT_UNIT.charAt(0);
@VisibleForTesting
public static final String PAPER_START_FORMAT = "Paper start";
private static final String PAPER_END_FORMAT = "Paper end";
@VisibleForTesting
public static final String GENERATED_COMMENT_FORMAT = "// %s - Generated/%s"; // {0} = PAPER_START_FORMAT|PAPER_END_FORMAT {1} = pattern
@ApiStatus.Experimental // import collector is probably outdated in that case and new typeName are not handled
private static final boolean SEARCH_MARKER_IN_METADATA = Boolean.getBoolean("paper.generator.rewriter.searchMarkerInMetadata");
protected final ClassNamed rewriteClass;
protected final String pattern;
private final boolean exactReplacement;
public SearchReplaceRewriter(Class<?> rewriteClass, String pattern, boolean exactReplacement) {
this(new ClassNamed(rewriteClass), pattern, exactReplacement);
}
public SearchReplaceRewriter(ClassNamed rewriteClass, String pattern, boolean exactReplacement) {
this.rewriteClass = rewriteClass;
this.pattern = pattern;
this.exactReplacement = exactReplacement;
}
// only when exactReplacement = false
@ApiStatus.OverrideOnly
protected void insert(SearchMetadata metadata, StringBuilder builder) {
throw new UnsupportedOperationException("This rewriter (" + this.getClass().getCanonicalName() + ") doesn't support removal and insertion!");
}
// only when exactReplacement = true
@ApiStatus.OverrideOnly
protected void replaceLine(SearchMetadata metadata, StringBuilder builder) {
throw new UnsupportedOperationException("This rewriter (" + this.getClass().getCanonicalName() + ") doesn't support exact replacement!");
}
protected void beginSearch() {}
protected SearchReplaceRewriter getRewriterFor(String pattern) {
return this;
}
protected Set<String> getPatterns() {
return Set.of(this.pattern);
}
private void searchAndReplace(BufferedReader reader, StringBuilder content) throws IOException {
Set<String> patterns = this.getPatterns();
Preconditions.checkState(!patterns.isEmpty());
this.beginSearch();
Set<String> remainingPatterns = new HashSet<>(patterns);
StringBuilder strippedContent = null;
ClassNamed root = this.rewriteClass.root();
final ImportCollector importCollector = new ImportTypeCollector(root);
final LineParser lineParser = new LineParser();
String indent = Formatting.incrementalIndent(INDENT_UNIT, this.rewriteClass);
SearchReplaceRewriter foundRewriter = null;
boolean inBody = false;
int i = 0;
while (true) {
String line = reader.readLine();
if (line == null) {
break;
}
StringReader lineIterator = new StringReader(line);
// collect import to avoid fqn when not needed
int previousCursor = lineIterator.getCursor();
if (importCollector != ImportCollector.NO_OP && !inBody && !line.isEmpty()) {
if (lineParser.consumeImports(lineIterator, importCollector)) {
inBody = true;
}
}
if (SEARCH_MARKER_IN_METADATA) {
lineIterator.setCursor(previousCursor);
}
CommentMarker marker = EMPTY_MARKER;
if (!line.isEmpty() && (inBody || SEARCH_MARKER_IN_METADATA)) {
marker = this.searchMarker(lineIterator, foundRewriter == null ? null : indent, remainingPatterns); // change this to patterns if needed to allow two rewrite of the same pattern in the same file
}
if (marker != EMPTY_MARKER) {
if (!marker.start()) {
if (foundRewriter == null) {
throw new IllegalStateException("Generated start comment is missing for pattern " + marker.pattern() + " in " + this.rewriteClass.canonicalName() + " at line " + (i + 1));
}
if (foundRewriter.pattern.equals(marker.pattern())) {
if (!foundRewriter.exactReplacement) {
appendGeneratedComment(content, indent);
foundRewriter.insert(new SearchMetadata(importCollector, indent, strippedContent.toString(), i), content);
strippedContent = null;
}
remainingPatterns.remove(foundRewriter.pattern);
foundRewriter = null;
// regular line
content.append(line);
content.append('\n');
} else {
throw new IllegalStateException("Generated end comment doesn't match for pattern " + foundRewriter.pattern + " in " + this.rewriteClass.canonicalName() + " at line " + (i + 1));
}
} else {
if (foundRewriter != null) {
throw new IllegalStateException("Nested generated comments are not allowed for " + this.rewriteClass.canonicalName() + " at line " + (i + 1));
}
if (marker.indent() % INDENT_SIZE != 0) {
throw new IllegalStateException("Generated start comment is not properly indented at line " + (i + 1) + " for pattern " + marker.pattern() + " in " + this.rewriteClass.canonicalName());
}
indent = " ".repeat(marker.indent()); // update indent based on the comments for flexibility
foundRewriter = this.getRewriterFor(marker.pattern());
if (!foundRewriter.exactReplacement) {
strippedContent = new StringBuilder();
}
// regular line
content.append(line);
content.append('\n');
}
} else {
if (foundRewriter == null) {
content.append(line);
content.append('\n');
} else {
if (foundRewriter.exactReplacement) {
// there's no generated comment here since when the size is equals the replaced content doesn't depend on the game content
// if it does that means the replaced content might not be equals during MC update because of adding/removed content
foundRewriter.replaceLine(new SearchMetadata(importCollector, indent, line, i), content);
} else {
strippedContent.append(line);
strippedContent.append('\n');
}
}
}
i++;
}
if (foundRewriter != null) {
throw new IllegalStateException("Generated end comment is missing for pattern " + foundRewriter.pattern + " in " + this.rewriteClass.canonicalName());
}
if (!remainingPatterns.isEmpty()) {
throw new IllegalStateException("SRT didn't found some expected generated comment for the following patterns: " + remainingPatterns.toString());
}
}
public String getRelativeFilePath() {
return "%s/%s.java".formatted(
this.rewriteClass.packageName().replace('.', '/'),
this.rewriteClass.root().simpleName()
);
}
public boolean isVersionDependant() {
return !this.exactReplacement;
}
public ClassNamed getRewrittenClass() {
return this.rewriteClass;
}
@Override
public void writeToFile(Path parent) throws IOException {
String filePath = this.getRelativeFilePath();
Path path = parent.resolve(filePath);
StringBuilder content = new StringBuilder();
try (BufferedReader buffer = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
this.searchAndReplace(buffer, content);
}
// Files.writeString(path, content.toString(), StandardCharsets.UTF_8); // todo
Path createdPath;
if (this.rewriteClass.knownClass() != null) {
createdPath = Main.generatedPath.resolve(filePath);
} else {
createdPath = Main.generatedServerPath.resolve(filePath);
}
Files.createDirectories(createdPath.getParent());
Files.writeString(createdPath, content, StandardCharsets.UTF_8);
}
private void appendGeneratedComment(StringBuilder builder, String indent) {
builder.append(indent).append("// %s %s".formatted(
Annotations.annotationStyle(GeneratedFrom.class),
SharedConstants.getCurrentVersion().getName()
));
builder.append('\n');
}
public CommentMarker searchMarker(StringReader lineIterator, @Nullable String indent, @Nullable Set<String> patterns) {
boolean strict = indent != null;
final int indentSize;
if (strict) {
if (!lineIterator.trySkipChars(indent.length(), indent.charAt(0))) {
return EMPTY_MARKER;
}
indentSize = indent.length();
} else {
indentSize = lineIterator.skipChars(INDENT_CHAR);
}
boolean foundStart = lineIterator.trySkipString(GENERATED_COMMENT_FORMAT.formatted(PAPER_START_FORMAT, ""));
boolean foundEnd = !foundStart && lineIterator.trySkipString(GENERATED_COMMENT_FORMAT.formatted(PAPER_END_FORMAT, ""));
if (!foundStart && !foundEnd) {
return EMPTY_MARKER;
}
String pattern = lineIterator.readRemainingString();
if (patterns == null || patterns.contains(pattern)) { // patterns will be null only for tests
return new CommentMarker(pattern, foundStart, indentSize);
}
return EMPTY_MARKER;
}
}

View File

@ -7,12 +7,12 @@ public class EnumCloneRewriter<T extends Enum<T>, A extends Enum<A>> extends Enu
private final Class<T> basedOn;
public EnumCloneRewriter(final Class<A> rewriteClass, final Class<T> basedOn, final String pattern, boolean equalsSize) {
this(new ClassNamed(rewriteClass), basedOn, pattern, equalsSize);
public EnumCloneRewriter(final Class<A> rewriteClass, final Class<T> basedOn, final String pattern, boolean exactReplacement) {
this(new ClassNamed(rewriteClass), basedOn, pattern, exactReplacement);
}
public EnumCloneRewriter(final ClassNamed rewriteClass, final Class<T> basedOn, final String pattern, boolean equalsSize) {
super(rewriteClass, pattern, equalsSize);
public EnumCloneRewriter(final ClassNamed rewriteClass, final Class<T> basedOn, final String pattern, boolean exactReplacement) {
super(rewriteClass, pattern, exactReplacement);
this.basedOn = basedOn;
}

View File

@ -2,7 +2,7 @@ package io.papermc.generator.rewriter.types;
import com.google.common.base.Suppliers;
import io.papermc.generator.Main;
import io.papermc.generator.rewriter.SearchMetadata;
import io.papermc.generator.rewriter.replace.SearchMetadata;
import io.papermc.generator.rewriter.utils.Annotations;
import io.papermc.generator.rewriter.ClassNamed;
import io.papermc.generator.utils.Formatting;

View File

@ -1,7 +1,7 @@
package io.papermc.generator.rewriter.types;
import io.papermc.generator.rewriter.SearchMetadata;
import io.papermc.generator.rewriter.SearchReplaceRewriter;
import io.papermc.generator.rewriter.replace.SearchMetadata;
import io.papermc.generator.rewriter.replace.SearchReplaceRewriter;
import io.papermc.generator.rewriter.ClassNamed;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
@ -12,12 +12,12 @@ public abstract class EnumRewriter<T, A extends Enum<A>> extends SearchReplaceRe
@MonotonicNonNull
private Iterator<T> values;
protected EnumRewriter(final Class<A> rewriteClass, final String pattern, final boolean equalsSize) {
super(rewriteClass, pattern, equalsSize);
protected EnumRewriter(final Class<A> rewriteClass, final String pattern, final boolean exactReplacement) {
super(rewriteClass, pattern, exactReplacement);
}
protected EnumRewriter(final ClassNamed rewriteClass, final String pattern, final boolean equalsSize) {
super(rewriteClass, pattern, equalsSize);
protected EnumRewriter(final ClassNamed rewriteClass, final String pattern, final boolean exactReplacement) {
super(rewriteClass, pattern, exactReplacement);
}
protected abstract Iterable<T> getValues();

View File

@ -3,8 +3,8 @@ package io.papermc.generator.rewriter.types;
import com.google.common.base.Preconditions;
import com.google.common.base.Suppliers;
import io.papermc.generator.Main;
import io.papermc.generator.rewriter.SearchMetadata;
import io.papermc.generator.rewriter.SearchReplaceRewriter;
import io.papermc.generator.rewriter.replace.SearchMetadata;
import io.papermc.generator.rewriter.replace.SearchReplaceRewriter;
import io.papermc.generator.rewriter.utils.Annotations;
import io.papermc.generator.utils.Formatting;
import io.papermc.generator.utils.RegistryUtils;

View File

@ -1,7 +1,7 @@
package io.papermc.generator.rewriter.types;
import io.papermc.generator.rewriter.SearchMetadata;
import io.papermc.generator.rewriter.SearchReplaceRewriter;
import io.papermc.generator.rewriter.replace.SearchMetadata;
import io.papermc.generator.rewriter.replace.SearchReplaceRewriter;
import io.papermc.generator.rewriter.ClassNamed;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import java.util.Iterator;
@ -11,12 +11,12 @@ public abstract class SwitchCaseRewriter extends SearchReplaceRewriter {
@MonotonicNonNull
private Iterator<String> cases;
protected SwitchCaseRewriter(final Class<?> rewriteClass, final String pattern, final boolean equalsSize) {
super(rewriteClass, pattern, equalsSize);
protected SwitchCaseRewriter(final Class<?> rewriteClass, final String pattern, final boolean exactReplacement) {
super(rewriteClass, pattern, exactReplacement);
}
protected SwitchCaseRewriter(final ClassNamed rewriteClass, final String pattern, final boolean equalsSize) {
super(rewriteClass, pattern, equalsSize);
protected SwitchCaseRewriter(final ClassNamed rewriteClass, final String pattern, final boolean exactReplacement) {
super(rewriteClass, pattern, exactReplacement);
}
protected abstract Iterable<String> getCases();

View File

@ -2,8 +2,8 @@ package io.papermc.generator.rewriter.types;
import com.google.common.collect.Multimap;
import io.papermc.generator.rewriter.ClassNamed;
import io.papermc.generator.rewriter.SearchMetadata;
import io.papermc.generator.rewriter.SearchReplaceRewriter;
import io.papermc.generator.rewriter.replace.SearchMetadata;
import io.papermc.generator.rewriter.replace.SearchReplaceRewriter;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import java.util.Collection;
@ -12,12 +12,12 @@ public abstract class SwitchRewriter<T> extends SearchReplaceRewriter {
protected @MonotonicNonNull Return<T> defaultValue;
protected SwitchRewriter(final Class<?> rewriteClass, final String pattern, final boolean equalsSize) {
super(rewriteClass, pattern, equalsSize);
protected SwitchRewriter(final Class<?> rewriteClass, final String pattern, final boolean exactReplacement) {
super(rewriteClass, pattern, exactReplacement);
}
protected SwitchRewriter(final ClassNamed rewriteClass, final String pattern, final boolean equalsSize) {
super(rewriteClass, pattern, equalsSize);
protected SwitchRewriter(final ClassNamed rewriteClass, final String pattern, final boolean exactReplacement) {
super(rewriteClass, pattern, exactReplacement);
}
protected record Return<T>(T object, String code) {}

View File

@ -1,8 +1,8 @@
package io.papermc.generator.rewriter.types;
import io.papermc.generator.Main;
import io.papermc.generator.rewriter.SearchMetadata;
import io.papermc.generator.rewriter.SearchReplaceRewriter;
import io.papermc.generator.rewriter.replace.SearchMetadata;
import io.papermc.generator.rewriter.replace.SearchReplaceRewriter;
import io.papermc.generator.rewriter.utils.Annotations;
import io.papermc.generator.utils.Formatting;
import java.util.Collection;

View File

@ -1,7 +1,7 @@
package io.papermc.generator.rewriter.types.simple;
import io.papermc.generator.rewriter.SearchMetadata;
import io.papermc.generator.rewriter.SearchReplaceRewriter;
import io.papermc.generator.rewriter.replace.SearchMetadata;
import io.papermc.generator.rewriter.replace.SearchReplaceRewriter;
import io.papermc.generator.rewriter.types.Types;
import io.papermc.generator.utils.BlockStateMapping;
import java.util.Comparator;

View File

@ -1,7 +1,7 @@
package io.papermc.generator.rewriter.types.simple;
import io.papermc.generator.rewriter.SearchMetadata;
import io.papermc.generator.rewriter.SearchReplaceRewriter;
import io.papermc.generator.rewriter.replace.SearchMetadata;
import io.papermc.generator.rewriter.replace.SearchReplaceRewriter;
import io.papermc.generator.rewriter.types.Types;
import io.papermc.generator.utils.BlockEntityMapping;
import io.papermc.generator.utils.Formatting;

View File

@ -1,7 +1,7 @@
package io.papermc.generator.rewriter.types.simple;
import io.papermc.generator.rewriter.SearchMetadata;
import io.papermc.generator.rewriter.SearchReplaceRewriter;
import io.papermc.generator.rewriter.replace.SearchMetadata;
import io.papermc.generator.rewriter.replace.SearchReplaceRewriter;
import io.papermc.generator.rewriter.types.Types;
import io.papermc.generator.utils.Formatting;
import net.minecraft.core.registries.BuiltInRegistries;

View File

@ -1,7 +1,7 @@
package io.papermc.generator.rewriter.types.simple;
import io.papermc.generator.rewriter.SearchMetadata;
import io.papermc.generator.rewriter.SearchReplaceRewriter;
import io.papermc.generator.rewriter.replace.SearchMetadata;
import io.papermc.generator.rewriter.replace.SearchReplaceRewriter;
import net.minecraft.world.level.material.MapColor;
import org.bukkit.map.MapPalette;

View File

@ -2,8 +2,8 @@ package io.papermc.generator.rewriter.types.simple;
import com.google.common.base.Suppliers;
import com.google.gson.internal.Primitives;
import io.papermc.generator.rewriter.SearchMetadata;
import io.papermc.generator.rewriter.SearchReplaceRewriter;
import io.papermc.generator.rewriter.replace.SearchMetadata;
import io.papermc.generator.rewriter.replace.SearchReplaceRewriter;
import io.papermc.generator.rewriter.utils.Annotations;
import io.papermc.generator.utils.ClassHelper;
import io.papermc.generator.utils.Formatting;

View File

@ -1,6 +1,6 @@
package io.papermc.generator.rewriter.utils;
import io.papermc.generator.rewriter.SearchMetadata;
import io.papermc.generator.rewriter.replace.SearchMetadata;
import io.papermc.generator.rewriter.context.ImportCollector;
import io.papermc.generator.utils.ClassHelper;
import io.papermc.generator.utils.Formatting;
@ -14,7 +14,7 @@ import static io.papermc.generator.utils.Formatting.quoted;
public final class Annotations {
public static String annotation(Class<? extends Annotation> clazz, ImportCollector collector) {
return "@%s".formatted(collector.getTypeName(clazz));
return "@%s".formatted(collector.getShortName(clazz));
}
public static String annotationStyle(Class<? extends Annotation> clazz) {

View File

@ -1,5 +1,6 @@
package io.papermc.generator.types.craftblockdata;
import com.google.common.base.Preconditions;
import io.papermc.generator.utils.BlockStateMapping;
import net.minecraft.world.level.block.Block;
import org.bukkit.block.data.BlockData;
@ -11,10 +12,8 @@ public final class CraftBlockDataGenerators {
public static void generate(Path container) throws IOException {
for (Map.Entry<Class<? extends Block>, BlockStateMapping.BlockData> entry : BlockStateMapping.MAPPING.entrySet()) {
Class<? extends BlockData> api = BlockStateMapping.getBestSuitedApiClass(entry.getKey());
if (api == null) {
continue;
}
Class<? extends BlockData> api = BlockStateMapping.getBestSuitedApiClass(entry.getValue());
Preconditions.checkState(api != null, "Unknown custom BlockData api class for " + entry.getKey().getCanonicalName());
new CraftBlockDataGenerator<>(entry.getKey(), entry.getValue(), api).writeToFile(container);
}

View File

@ -13,8 +13,8 @@ import java.util.Map;
public abstract class DataPropertyWriterBase<T extends Property<?>> implements DataPropertyMaker {
protected final Class<? extends Block> blockClass;
protected final Collection<T> properties;
protected final Class<? extends Block> blockClass;
protected DataPropertyWriterBase(Collection<T> properties, Class<? extends Block> blockClass) {
this.properties = properties;
@ -42,6 +42,7 @@ public abstract class DataPropertyWriterBase<T extends Property<?>> implements D
}
protected void createSyntheticMap(CodeBlock.Builder code, Class<?> indexClass, Map<String, String> fieldNames) {
// assume indexClass is an enum with its values matching the property names
code.add("$T.of(\n", Map.class).indent();
Iterator<T> it = this.properties.iterator();
while (it.hasNext()) {

View File

@ -321,7 +321,11 @@ public final class BlockStateMapping {
return null;
}
BlockData data = MAPPING.get(block);
return getBestSuitedApiClass(MAPPING.get(block));
}
@Nullable
public static Class<? extends org.bukkit.block.data.BlockData> getBestSuitedApiClass(BlockData data) {
if (data.api() != null) {
return data.api();
}

View File

@ -7,22 +7,6 @@ import java.util.Set;
public final class ClassHelper {
public static String getDeclaredType(Class<?> clazz) {
if (clazz.isAnnotation()) {
return "@interface";
}
if (clazz.isInterface()) {
return "interface";
}
if (clazz.isEnum()) {
return "enum";
}
if (clazz.isRecord()) {
return "record";
}
return "class";
}
public static Class<?> getRootClass(Class<?> clazz) {
Class<?> rootClass = clazz;
Class<?> parentClass = clazz;

View File

@ -71,7 +71,7 @@ public final class Formatting {
return Optional.of(resourcePath.substring(tagsIndex + tagDir.length() + 1, dotIndex)); // namespace/tags/registry_key/[tag_key].json
}
private static int countOccurrences(String value, char match) {
public static int countOccurrences(String value, char match) {
int count = 0;
for (int i = 0, len = value.length(); i < len; i++) {
if (value.charAt(i) == match) {
@ -96,7 +96,7 @@ public final class Formatting {
}
public static String quoted(String value) {
return String.format("\"%s\"", value);
return "\"" + value + "\"";
}
public static String floatStr(float value) {

View File

@ -3,6 +3,7 @@ package io.papermc.generator.utils;
import com.google.common.base.CaseFormat;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.Nullable;
import javax.lang.model.SourceVersion;
@ -82,6 +83,17 @@ public class NamingManager {
return name;
}
public static final Pattern FULLY_QUALIFIED_NAME_SEPARATOR = Pattern.compile(".", Pattern.LITERAL);
public static boolean hasIllegalKeyword(String typeName) {
for (String part : FULLY_QUALIFIED_NAME_SEPARATOR.split(typeName)) {
if (SourceVersion.isKeyword(part)) {
return true;
}
}
return false;
}
public static class NameWrapper {
private final String keyword;

View File

@ -1,11 +1,14 @@
package io.papermc.generator.rewriter;
import io.papermc.generator.Generators;
import io.papermc.generator.rewriter.parser.StringReader;
import io.papermc.generator.rewriter.replace.CommentMarker;
import io.papermc.generator.rewriter.replace.SearchReplaceRewriter;
import io.papermc.generator.rewriter.utils.Annotations;
import io.papermc.paper.generated.GeneratedFrom;
import net.minecraft.SharedConstants;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.io.BufferedReader;
import java.io.IOException;
@ -13,6 +16,12 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import static io.papermc.generator.rewriter.replace.CommentMarker.EMPTY_MARKER;
import static io.papermc.generator.rewriter.replace.SearchReplaceRewriter.INDENT_CHAR;
import static io.papermc.generator.rewriter.replace.SearchReplaceRewriter.INDENT_SIZE;
import static org.junit.jupiter.api.Assertions.assertEquals;
@Disabled
public class OldGeneratedCodeTest {
private static final String API_CONTAINER = System.getProperty("paper.generator.rewriter.container.api");
@ -26,27 +35,13 @@ public class OldGeneratedCodeTest {
CURRENT_VERSION = SharedConstants.getCurrentVersion().getName();
}
private boolean versionDependent(SearchReplaceRewriter srt) {
if (srt instanceof CompositeRewriter compositeRewriter) {
boolean versionDependent = false;
for (SearchReplaceRewriter rewriter : compositeRewriter.getRewriters()) {
if (!rewriter.equalsSize) {
versionDependent = true;
break;
}
}
return versionDependent;
}
return !srt.equalsSize;
}
private void checkOutdated(String container, SourceRewriter[] rewriters) throws IOException {
for (SourceRewriter rewriter : rewriters) {
if (!(rewriter instanceof SearchReplaceRewriter srt) || !versionDependent(srt)) {
if (!(rewriter instanceof SearchReplaceRewriter srt) || !srt.isVersionDependant()) {
continue;
}
Path path = Path.of(container, srt.getFilePath());
Path path = Path.of(container, srt.getRelativeFilePath());
if (Files.notExists(path)) { // todo remove after the repo change
continue;
}
@ -58,21 +53,42 @@ public class OldGeneratedCodeTest {
break;
}
lineCount++;
if (line.isEmpty()) {
continue;
}
StringReader lineIterator = new StringReader(line);
CommentMarker marker = srt.searchMarker(lineIterator, null, null);
if (marker != EMPTY_MARKER) {
if (!marker.start()) {
continue;
}
int startIndentSize = marker.indent();
if (startIndentSize % INDENT_SIZE != 0) {
continue;
}
int startPatternIndex = line.indexOf(SearchReplaceRewriter.GENERATED_COMMENT_FORMAT.formatted(SearchReplaceRewriter.PAPER_START_FORMAT, ""));
if (startPatternIndex != -1 && (startPatternIndex % SearchReplaceRewriter.INDENT_UNIT.length()) == 0 && line.stripTrailing().equals(line)) {
String nextLine = reader.readLine();
if (nextLine == null) {
break;
}
lineCount++;
if (nextLine.isEmpty()) {
continue;
}
StringReader nextLineIterator = new StringReader(nextLine);
int indentSize = nextLineIterator.skipChars(INDENT_CHAR);
if (indentSize != startIndentSize) {
continue;
}
String generatedComment = "// %s ".formatted(Annotations.annotationStyle(GeneratedFrom.class));
int generatedIndex = nextLine.indexOf(generatedComment);
if (generatedIndex != -1 && (generatedIndex % SearchReplaceRewriter.INDENT_UNIT.length()) == 0 && nextLine.stripTrailing().equals(nextLine)) {
String generatedVersion = nextLine.substring(generatedIndex + generatedComment.length());
Assertions.assertEquals(CURRENT_VERSION, generatedVersion,
if (nextLineIterator.trySkipString(generatedComment) && nextLineIterator.canRead()) {
String generatedVersion = nextLineIterator.readRemainingString();
assertEquals(CURRENT_VERSION, generatedVersion,
"Code at line %s in %s is marked as being generated in version %s when the current version is %s".formatted(
lineCount, srt.rewriteClass.canonicalName(),
lineCount, srt.getRewrittenClass().canonicalName(),
generatedVersion, CURRENT_VERSION));
}
}

View File

@ -0,0 +1,6 @@
package io.papermc.generator.rewriter.data.sample.parser.area;//{
public @interface AnnotationClass {
// @interface should be invalidated and not detected as an annotation
// via its keyword: interface
}

View File

@ -0,0 +1,6 @@
package io.papermc.generator.rewriter.data.sample.parser.area;
@SuppressWarnings({"DeprecatedIsStillUsed", "deprecation"}) // Paper
public class AnnotationPresentClass {
}

View File

@ -0,0 +1,14 @@
package io.papermc.generator.rewriter.data.sample.parser.area;
@SuppressWarnings
(
{
"DeprecatedIsStillUsed"
,
"deprecation"
}
)
// Paper
public class FancyNewlineAnnotationPresentClass {
}

View File

@ -0,0 +1,5 @@
package io.papermc.generator.rewriter.data.sample.parser.area;//{
public/* d */class/* d */FancyScopeClass/* d */
/* d */{//d
}

View File

@ -0,0 +1,4 @@
package io.papermc.generator.rewriter.data.sample.parser.area;//{
public/* d */class/* d */FancyScopeClass2/* d */ /* d */{//d
}

View File

@ -0,0 +1,23 @@
package io.papermc.generator.rewriter.data.sample.parser.area;
import org.jetbrains.annotations.ApiStatus;
@SuppressWarnings//discord /**/
/* a*/ ( /* a b*/
{ /*a*/
"DeprecatedIsStillUsed" /*a*/
/*a*/ , /**/// a
"deprecation"
} // dq
) // a
// Paper
@//a
ApiStatus//b
.//c
Internal//d
@/* a*/
ApiStatus/* a*/./* a*/
Experimental/* a*///a/* a*/
public class MixedAnnotationPresentClass {
}

View File

@ -0,0 +1,4 @@
package io.papermc.generator.rewriter.data.sample.parser.area;//{
public class NearScopeClass{
}

View File

@ -0,0 +1,5 @@
package io.papermc.generator.rewriter.data.sample.parser.area;//{
public class NewlineScopedClass
{
}

View File

@ -0,0 +1,15 @@
package io.papermc.generator.rewriter.data.sample.parser.area;//{
// { //{
//{
/*
{
*/
/**
* {
* }
* {@linkplain SimpleTrapClass}
*/
public class SimpleTrapClass {
}

View File

@ -0,0 +1,29 @@
package io.papermc.generator.rewriter.data.sample.parser.imports;
import/* hi */org.bukkit.NamespacedKey; // multi line comment filled gap between import and typeName
import org.bukkit/* my */./*hidden*/Statistic/*message*/.Type/* ! */; // comment in type name
// import org.bukkit.block.Commented;
/*
import org.bukkit.block.Commented;
*/
/*
import org.bukkit.block.Commented;
*//*
import org.bukkit.block.Commented;
*/// import org.bukkit.block.Commented;import org.bukkit.block.Commented;
import/* hi */static/* ! */org.bukkit. Statistic.ANIMALS_BRED; // multi line comment filled gap between static and typeName
// import static org.bukkit.Statistic.COMMENTED;
/*
import static org.bukkit.Statistic.COMMENTED;
*/
/*
import static org.bukkit.Statistic.COMMENTED;
*//*
import static org.bukkit.Statistic.COMMENTED;
*/// import static org.bukkit.Statistic.COMMENTED;import static org.bukkit.Statistic.COMMENTED;
import static org.bukkit.Material./* hi */* /* ! */;
public class FancyCommentImportType {
}

View File

@ -0,0 +1,3 @@
package io.papermc.generator.rewriter.data.sample.parser.imports;import org.bukkit.NamespacedKey; import org.bukkit.Statistic.Type;import org.bukkit.*;/**/import static org.bukkit.Statistic.ANIMALS_BRED; /* */ import static org.bukkit.Material.*;public class FancyInlinedImportType {
}

View File

@ -0,0 +1,52 @@
package io.papermc.generator.rewriter.data.sample.parser.imports;
import
org
.
bukkit
.
NamespacedKey
; // multi line import
import org
.bukkit
.
Statistic.
Type; // multi line import with dot art
import
org
.
bukkit
.
*
; // multi line star import
import
static
org
.
bukkit
.
Statistic
.
ANIMALS_BRED
; // multi line static import
import
static org
.
bukkit
.
Material
.
*
; // multi line star import
public class FancyNewlineImportType {
}

View File

@ -0,0 +1,11 @@
package io.papermc.generator.rewriter.data.sample.parser.imports;
import org. bukkit.NamespacedKey ;
import org.bukkit .Statistic. Type;
import org .bukkit.* ;
import static org. bukkit. Statistic.ANIMALS_BRED ;
import static org.bukkit. Material. * ;
public class FancySpaceImportType {
}

View File

@ -0,0 +1,30 @@
package io.papermc.generator.rewriter.data.sample.parser.imports;
import// discard?
org /* hi */
/* hi */ .
bukkit /* hi */ // /*/
/* hi */ .
NamespacedKey// discard?
/* hi */; // multi line import
import// discard?
static// discard?
org// discard?
/* hi */ . /* hi */
/* hi */ bukkit /* hi */
/* hi */ .
/* a */
// a
Material
.
*// discard?
; // multi line star import
public class MixedCommentImportType {
}

View File

@ -0,0 +1,11 @@
package io.papermc.generator.rewriter.data.sample.parser.imports;
import org.bukkit.NamespacedKey; // root class import
import org.bukkit.Statistic.Type; // inner class import (directly referenced)
import org.bukkit.*; // star import (whole package + one level)
import static org.bukkit.Statistic.ANIMALS_BRED; // static import
import static org.bukkit.Material.*; // static star import
public class StandardImportType {
}

View File

@ -0,0 +1,11 @@
package io.papermc.generator.rewriter.data.sample.parser.imports.name;
import org.bukkit.*;
import static org.bukkit.Statistic.*;
public class GlobalImportType {
{
var a = Material.VINE;
var b = ARMOR_CLEANED;
}
}

View File

@ -0,0 +1,12 @@
package io.papermc.generator.rewriter.data.sample.parser.imports.name;
import io.papermc.generator.rewriter.data.sample.parser.imports.name.one.*;
public class PackageClassImportType {
{
var a = SamePackageClass.class;
var b = OneDepthClass.class;
var c = OneDepthClass.NonStaticClass.class;
var d = OneDepthClass.StaticClass.class;
}
}

View File

@ -0,0 +1,13 @@
package io.papermc.generator.rewriter.data.sample.parser.imports.name;
import org.bukkit.Material;
import static org.bukkit.Statistic.ARMOR_CLEANED;
import static org.bukkit.Statistic.valueOf;
public class RegularImportType {
{
var a = Material.VINE;
var b = ARMOR_CLEANED;
valueOf(b.name());
}
}

View File

@ -0,0 +1,10 @@
package io.papermc.generator.rewriter.data.sample.parser.imports.name;
import io.papermc.generator.rewriter.data.sample.parser.imports.name.one.OneDepthClass.*;
public class RemoteGlobalInnerClassImportType {
{
var c = NonStaticClass.class;
var d = StaticClass.class;
}
}

View File

@ -0,0 +1,11 @@
package io.papermc.generator.rewriter.data.sample.parser.imports.name;
import io.papermc.generator.rewriter.data.sample.parser.imports.name.one.OneDepthClass.NonStaticClass;
import static io.papermc.generator.rewriter.data.sample.parser.imports.name.one.OneDepthClass.StaticClass;
public class RemoteInnerClassImportType {
{
var b = NonStaticClass.class;
var c = StaticClass.class;
}
}

View File

@ -0,0 +1,10 @@
package io.papermc.generator.rewriter.data.sample.parser.imports.name;
import static io.papermc.generator.rewriter.data.sample.parser.imports.name.one.OneDepthClass.*;
public class RemoteStaticGlobalInnerClassImportType {
{
var b = io.papermc.generator.rewriter.data.sample.parser.imports.name.one.OneDepthClass.NonStaticClass.class;
var c = StaticClass.class;
}
}

View File

@ -0,0 +1,4 @@
package io.papermc.generator.rewriter.data.sample.parser.imports.name;
public class SamePackageClass {
}

View File

@ -0,0 +1,21 @@
package io.papermc.generator.rewriter.data.sample.parser.imports.name;
public class SelfInnerClassImportType {
public class A {
public class B {
public class C {
{
var a = A.class;
var b = B.class;
var c = C.class;
}
}
}
}
{
var a = A.class;
var b = A.B.class;
var c = A.B.C.class;
}
}

View File

@ -0,0 +1,12 @@
package io.papermc.generator.rewriter.data.sample.parser.imports.name.one;
public class OneDepthClass {
public static class StaticClass {
}
public class NonStaticClass {
}
}

View File

@ -0,0 +1,15 @@
package io.papermc.generator.rewriter.data.yml;
public class ImportMapping {
public ImportSet imports;
public ImportSet staticImports;
public ImportSet getImports() {
return this.imports;
}
public ImportSet getStaticImports() {
return this.staticImports;
}
}

View File

@ -0,0 +1,18 @@
package io.papermc.generator.rewriter.data.yml;
import java.util.Collections;
import java.util.Set;
public class ImportSet {
public Set<String> single;
public Set<String> global;
public Set<String> single() {
return this.single == null ? Collections.emptySet() : this.single;
}
public Set<String> global() {
return this.global == null ? Collections.emptySet() : this.global;
}
}

View File

@ -0,0 +1,21 @@
package io.papermc.generator.rewriter.data.yml;
import java.util.Map;
public class ImportShortNameMapping {
public Map<String, String> imports;
public Map<String, String> staticImports;
/**
* Note: unlike {@link #getStaticImports()} (which only use a dot), nested class must be identified using '$' since the typeName is then
* converted into a class object
*/
public Map<String, String> getImports() {
return this.imports;
}
public Map<String, String> getStaticImports() {
return this.staticImports;
}
}

View File

@ -0,0 +1,49 @@
package io.papermc.generator.rewriter.data.yml;
import org.junit.jupiter.params.converter.TypedArgumentConverter;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.nodes.ScalarNode;
import org.yaml.snakeyaml.nodes.Tag;
import java.io.IOException;
import java.io.InputStream;
public class YmlMappingConverter<T> extends TypedArgumentConverter<String, T> {
private static final LoaderOptions OPTIONS;
static {
OPTIONS = new LoaderOptions();
OPTIONS.setNestingDepthLimit(3);
}
private final Constructor yamlConstructor;
protected YmlMappingConverter(Class<T> clazz, String relativePackage) {
super(String.class, clazz);
if (relativePackage == null) {
this.yamlConstructor = new Constructor(clazz, OPTIONS);
} else {
this.yamlConstructor = new Constructor(clazz, OPTIONS) {
@Override
public String constructScalar(ScalarNode node) {
if (node.getTag() == Tag.STR && node.getValue().startsWith("(_).")) {
return node.getValue().replaceFirst("\\(_\\)", relativePackage);
}
return super.constructScalar(node);
}
};
}
}
@Override
protected T convert(String path) {
try (InputStream input = this.getClass().getClassLoader().getResourceAsStream(path)) {
return new Yaml(this.yamlConstructor).load(input);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}

View File

@ -0,0 +1,74 @@
package io.papermc.generator.rewriter.parser;
import io.papermc.generator.rewriter.ClassNamed;
import io.papermc.generator.rewriter.context.ImportTypeCollector;
import io.papermc.generator.rewriter.data.sample.parser.imports.FancyInlinedImportType;
import io.papermc.generator.rewriter.data.yml.ImportMapping;
import io.papermc.generator.rewriter.data.yml.ImportSet;
import io.papermc.generator.rewriter.data.sample.parser.imports.FancyCommentImportType;
import io.papermc.generator.rewriter.data.sample.parser.imports.FancyNewlineImportType;
import io.papermc.generator.rewriter.data.sample.parser.imports.FancySpaceImportType;
import io.papermc.generator.rewriter.data.sample.parser.imports.MixedCommentImportType;
import io.papermc.generator.rewriter.data.sample.parser.imports.StandardImportType;
import io.papermc.generator.rewriter.data.yml.YmlMappingConverter;
import it.unimi.dsi.fastutil.Pair;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.converter.ConvertWith;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Set;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class ImportCollectTest extends ParserTest {
private static Arguments fileToArgs(Class<?> sampleClass) {
return Arguments.of(
CONTAINER.resolve(sampleClass.getCanonicalName().replace('.', '/') + ".java"),
sampleClass,
"parser/expected/imports/%s.yml".formatted(sampleClass.getSimpleName())
);
}
private static Stream<Arguments> fileProvider() {
return Stream.of(
StandardImportType.class,
FancySpaceImportType.class,
FancyCommentImportType.class,
FancyNewlineImportType.class,
FancyInlinedImportType.class,
MixedCommentImportType.class
).map(ImportCollectTest::fileToArgs);
}
@ParameterizedTest
@MethodSource("fileProvider")
public void testImports(Path path,
Class<?> sampleClass,
@ConvertWith(ImportMappingConverter.class) ImportMapping expected) throws IOException {
final ImportTypeCollector importCollector = new ImportTypeCollector(new ClassNamed(sampleClass));
parseFile(path, importCollector);
String name = sampleClass.getSimpleName();
Pair<Set<String>, Set<String>> imports = importCollector.getImports();
ImportSet expectedImports = expected.getImports();
assertEquals(expectedImports.single(), imports.left(), "Regular imports doesn't match for " + name);
assertEquals(expectedImports.global(), imports.right(), "Regular global imports doesn't match for " + name);
Pair<Set<String>, Set<String>> staticImports = importCollector.getStaticImports();
ImportSet expectedStaticImports = expected.getStaticImports();
assertEquals(expectedStaticImports.single(), staticImports.left(), "Static imports doesn't match for " + name);
assertEquals(expectedStaticImports.global(), staticImports.right(), "Static global imports doesn't match for " + name);
}
private static class ImportMappingConverter extends YmlMappingConverter<ImportMapping> {
protected ImportMappingConverter() {
super(ImportMapping.class, null);
}
}
}

View File

@ -0,0 +1,94 @@
package io.papermc.generator.rewriter.parser;
import io.papermc.generator.rewriter.ClassNamed;
import io.papermc.generator.rewriter.context.ImportTypeCollector;
import io.papermc.generator.rewriter.data.sample.parser.area.AnnotationClass;
import io.papermc.generator.rewriter.data.sample.parser.area.AnnotationPresentClass;
import io.papermc.generator.rewriter.data.sample.parser.area.FancyNewlineAnnotationPresentClass;
import io.papermc.generator.rewriter.data.sample.parser.area.FancyScopeClass;
import io.papermc.generator.rewriter.data.sample.parser.area.FancyScopeClass2;
import io.papermc.generator.rewriter.data.sample.parser.area.MixedAnnotationPresentClass;
import io.papermc.generator.rewriter.data.sample.parser.area.NearScopeClass;
import io.papermc.generator.rewriter.data.sample.parser.area.NewlineScopedClass;
import io.papermc.generator.rewriter.data.sample.parser.area.SimpleTrapClass;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.io.IOException;
import java.nio.file.Path;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
public class ParserMetadataAreaTest extends ParserTest {
private static final Path CONTAINER = Path.of(System.getProperty("user.dir"), "src/test/java");
private static Arguments file(Class<?> sampleClass, String expectedLastLine) {
String name = sampleClass.getSimpleName();
return Arguments.of(
CONTAINER.resolve(sampleClass.getCanonicalName().replace('.', '/') + ".java"),
sampleClass,
name,
expectedLastLine
);
}
private static Stream<Arguments> fileProvider() {
return Stream.of(
file(
SimpleTrapClass.class,
"public class SimpleTrapClass {"
),
file(
AnnotationClass.class,
"public @interface AnnotationClass {"
),
file(
AnnotationPresentClass.class,
"public class AnnotationPresentClass {"
),
file(
FancyNewlineAnnotationPresentClass.class,
"public class FancyNewlineAnnotationPresentClass {"
),
file(
MixedAnnotationPresentClass.class,
"public class MixedAnnotationPresentClass {"
),
file(
NewlineScopedClass.class,
"{"
),
file(
NearScopeClass.class,
"public class NearScopeClass{"
),
file(
FancyScopeClass.class,
" /* d */{//d"
),
file(
FancyScopeClass2.class,
"public/* d */class/* d */FancyScopeClass2/* d */ /* d */{//d"
)
);
}
@ParameterizedTest
@MethodSource("fileProvider")
public void testAreaEnd(Path path,
Class<?> sampleClass,
String name,
String expectedLastLine) throws IOException {
final ImportTypeCollector importCollector = new ImportTypeCollector(new ClassNamed(sampleClass));
parseFile(path, importCollector, line -> {
assertEquals(expectedLastLine, line, "Parser didn't stop at the expected line for " + name);
},
() -> {
fail("File is empty or doesn't contains the required top scope needed for this test to run");
}
);
}
}

View File

@ -0,0 +1,38 @@
package io.papermc.generator.rewriter.parser;
import io.papermc.generator.rewriter.context.ImportCollector;
import org.junit.jupiter.api.Tag;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.function.Consumer;
@Tag("parser")
public class ParserTest {
protected static final Path CONTAINER = Path.of(System.getProperty("user.dir"), "src/test/java");
protected void parseFile(Path path, ImportCollector importCollector) throws IOException {
parseFile(path, importCollector, str -> {}, () -> {});
}
protected void parseFile(Path path, ImportCollector importCollector, Consumer<String> enterBodyCallback, Runnable eofCallback) throws IOException {
final LineParser lineParser = new LineParser();
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
while (true) {
String line = reader.readLine();
if (line == null) {
eofCallback.run();
break;
}
if (!line.isEmpty() && lineParser.consumeImports(new StringReader(line), importCollector)) {
enterBodyCallback.accept(line);
break;
}
}
}
}
}

View File

@ -0,0 +1,92 @@
package io.papermc.generator.rewriter.parser;
import io.papermc.generator.rewriter.ClassNamed;
import io.papermc.generator.rewriter.context.ImportTypeCollector;
import io.papermc.generator.rewriter.data.sample.parser.imports.name.GlobalImportType;
import io.papermc.generator.rewriter.data.sample.parser.imports.name.PackageClassImportType;
import io.papermc.generator.rewriter.data.sample.parser.imports.name.RegularImportType;
import io.papermc.generator.rewriter.data.sample.parser.imports.name.RemoteGlobalInnerClassImportType;
import io.papermc.generator.rewriter.data.sample.parser.imports.name.RemoteInnerClassImportType;
import io.papermc.generator.rewriter.data.sample.parser.imports.name.RemoteStaticGlobalInnerClassImportType;
import io.papermc.generator.rewriter.data.sample.parser.imports.name.SamePackageClass;
import io.papermc.generator.rewriter.data.sample.parser.imports.name.SelfInnerClassImportType;
import io.papermc.generator.rewriter.data.yml.ImportShortNameMapping;
import io.papermc.generator.rewriter.data.yml.YmlMappingConverter;
import io.papermc.generator.utils.ClassHelper;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Map;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.converter.ConvertWith;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class TypeShortNameTest extends ParserTest {
private static Arguments rootClass(Class<?> sampleClass) {
return innerClass(sampleClass, sampleClass);
}
private static Arguments innerClass(Class<?> sampleClass, Class<?> sampleInnerClass) {
String name = sampleClass.getSimpleName();
return Arguments.of(
CONTAINER.resolve(sampleClass.getCanonicalName().replace('.', '/') + ".java"),
sampleInnerClass,
name,
"parser/expected/imports/name/%s.yml".formatted(sampleInnerClass.getName().substring(sampleInnerClass.getPackageName().length() + 1))
);
}
private static Stream<Arguments> fileProvider() {
return Stream.of(
rootClass(RegularImportType.class),
rootClass(GlobalImportType.class),
rootClass(PackageClassImportType.class),
rootClass(RemoteGlobalInnerClassImportType.class),
rootClass(RemoteStaticGlobalInnerClassImportType.class),
rootClass(RemoteInnerClassImportType.class),
rootClass(SelfInnerClassImportType.class),
innerClass(SelfInnerClassImportType.class, SelfInnerClassImportType.A.B.C.class)
);
}
@ParameterizedTest
@MethodSource("fileProvider")
public void testTypeName(Path path,
Class<?> sampleClass,
String name,
@ConvertWith(ImportShortNameMappingConverter.class) ImportShortNameMapping mapping) throws IOException {
final ImportTypeCollector importCollector = new ImportTypeCollector(new ClassNamed(sampleClass));
parseFile(path, importCollector);
assertFalse(mapping.getImports() == null && mapping.getStaticImports() == null, "Empty expected import mapping!");
if (mapping.getImports() != null) {
for (Map.Entry<String, String> expect : mapping.getImports().entrySet()) {
Class<?> runtimeClass = ClassHelper.classOr(expect.getKey(), null);
assertNotNull(runtimeClass, "Runtime class cannot be null for import " + expect.getKey());
assertEquals(expect.getValue(), importCollector.getShortName(runtimeClass),
() -> "Short name of " + expect.getKey() + " doesn't match with collected imports for " + name + "! Import found: " + importCollector.getImports());
}
}
if (mapping.getStaticImports() != null) {
for (Map.Entry<String, String> expect : mapping.getStaticImports().entrySet()) {
assertEquals(expect.getValue(), importCollector.getStaticMemberShortName(expect.getKey()),
() -> "Short name of static member/class " + expect.getKey() + " doesn't match with collected imports for " + name + "! Static imports found: " + importCollector.getStaticImports());
}
}
}
private static class ImportShortNameMappingConverter extends YmlMappingConverter<ImportShortNameMapping> {
protected ImportShortNameMappingConverter() {
super(ImportShortNameMapping.class, SamePackageClass.class.getPackageName());
}
}
}

View File

@ -0,0 +1,10 @@
imports:
single:
- "org.bukkit.NamespacedKey"
- "org.bukkit.Statistic.Type"
staticImports:
single:
- "org.bukkit.Statistic.ANIMALS_BRED"
global:
- "org.bukkit.Material"

View File

@ -0,0 +1,12 @@
imports:
single:
- "org.bukkit.NamespacedKey"
- "org.bukkit.Statistic.Type"
global:
- "org.bukkit"
staticImports:
single:
- "org.bukkit.Statistic.ANIMALS_BRED"
global:
- "org.bukkit.Material"

View File

@ -0,0 +1,12 @@
imports:
single:
- "org.bukkit.NamespacedKey"
- "org.bukkit.Statistic.Type"
global:
- "org.bukkit"
staticImports:
single:
- "org.bukkit.Statistic.ANIMALS_BRED"
global:
- "org.bukkit.Material"

View File

@ -0,0 +1,12 @@
imports:
single:
- "org.bukkit.NamespacedKey"
- "org.bukkit.Statistic.Type"
global:
- "org.bukkit"
staticImports:
single:
- "org.bukkit.Statistic.ANIMALS_BRED"
global:
- "org.bukkit.Material"

View File

@ -0,0 +1,7 @@
imports:
single:
- "org.bukkit.NamespacedKey"
staticImports:
global:
- "org.bukkit.Material"

View File

@ -0,0 +1,12 @@
imports:
single:
- "org.bukkit.NamespacedKey"
- "org.bukkit.Statistic.Type"
global:
- "org.bukkit"
staticImports:
single:
- "org.bukkit.Statistic.ANIMALS_BRED"
global:
- "org.bukkit.Material"

View File

@ -0,0 +1,7 @@
imports: {
"org.bukkit.Material": "Material",
}
staticImports: {
"org.bukkit.Statistic.ARMOR_CLEANED": "ARMOR_CLEANED"
}

View File

@ -0,0 +1,6 @@
imports: {
"(_).SamePackageClass": "SamePackageClass",
"(_).one.OneDepthClass": "OneDepthClass",
"(_).one.OneDepthClass$NonStaticClass": "OneDepthClass.NonStaticClass",
"(_).one.OneDepthClass$StaticClass": "OneDepthClass.StaticClass"
}

View File

@ -0,0 +1,8 @@
imports: {
"org.bukkit.Material": "Material",
}
staticImports: {
"org.bukkit.Statistic.ARMOR_CLEANED": "ARMOR_CLEANED",
"org.bukkit.Statistic.valueOf": "valueOf",
}

View File

@ -0,0 +1,4 @@
imports: {
"(_).one.OneDepthClass$NonStaticClass": "NonStaticClass",
"(_).one.OneDepthClass$StaticClass": "StaticClass"
}

View File

@ -0,0 +1,7 @@
imports: {
"(_).one.OneDepthClass$NonStaticClass": "NonStaticClass"
}
staticImports: {
"(_).one.OneDepthClass.StaticClass": "StaticClass"
}

View File

@ -0,0 +1,4 @@
imports: {
"(_).one.OneDepthClass$NonStaticClass": "(_).one.OneDepthClass.NonStaticClass",
"(_).one.OneDepthClass$StaticClass": "StaticClass"
}

View File

@ -0,0 +1,5 @@
imports: {
"(_).SelfInnerClassImportType$A": "A",
"(_).SelfInnerClassImportType$A$B": "B",
"(_).SelfInnerClassImportType$A$B$C": "C",
}

View File

@ -0,0 +1,5 @@
imports: {
"(_).SelfInnerClassImportType$A": "A",
"(_).SelfInnerClassImportType$A$B": "A.B",
"(_).SelfInnerClassImportType$A$B$C": "A.B.C",
}