Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use local var context with static position methods #14

Merged
merged 1 commit into from
Jan 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions codebook-lvt/src/main/java/io/papermc/codebook/lvt/LvtUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
import java.util.List;
import java.util.function.Predicate;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;

public final class LvtUtil {

Expand All @@ -38,6 +40,9 @@ public static JvmType toJvmType(final String desc) {
}

public static String capitalize(final String name, final int index) {
if (name.isEmpty()) {
return name;
}
return Character.toUpperCase(name.charAt(index)) + name.substring(index + 1);
}

Expand Down Expand Up @@ -153,4 +158,14 @@ public static String parseSimpleTypeNameFromMethod(final String methodName, int
return LvtUtil.parseSimpleTypeName(methodName.substring(prefix));
}
}

public static @Nullable AbstractInsnNode prevInsnIgnoringConvertCast(final AbstractInsnNode insn) {
@Nullable AbstractInsnNode prev = insn.getPrevious();
while (prev != null
&& (prev.getOpcode() == Opcodes.CHECKCAST
|| (prev.getOpcode() >= Opcodes.I2L && prev.getOpcode() <= Opcodes.I2S))) {
prev = prev.getPrevious();
}
return prev;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import io.papermc.codebook.lvt.suggestion.LvtSuggester;
import io.papermc.codebook.lvt.suggestion.MathSuggester;
import io.papermc.codebook.lvt.suggestion.NewPrefixSuggester;
import io.papermc.codebook.lvt.suggestion.PositionsSuggester;
import io.papermc.codebook.lvt.suggestion.RecordComponentSuggester;
import io.papermc.codebook.lvt.suggestion.SingleVerbBooleanSuggester;
import io.papermc.codebook.lvt.suggestion.SingleVerbSuggester;
Expand Down Expand Up @@ -73,6 +74,7 @@ public final class RootLvtSuggester extends AbstractModule implements LvtSuggest
MthRandomSuggester.class,
MathSuggester.class,
StringSuggester.class,
PositionsSuggester.class,
NewPrefixSuggester.class,
SingleVerbSuggester.class,
VerbPrefixBooleanSuggester.class,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
/*
* codebook is a remapper utility for the PaperMC project.
*
* Copyright (c) 2023 Kyle Wood (DenWav)
* Contributors
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 3 only, no later versions.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*/

package io.papermc.codebook.lvt.suggestion;

import static io.papermc.codebook.lvt.LvtUtil.capitalize;
import static io.papermc.codebook.lvt.LvtUtil.decapitalize;
import static io.papermc.codebook.lvt.LvtUtil.prevInsnIgnoringConvertCast;
import static java.util.Objects.requireNonNull;

import dev.denwav.hypo.model.data.MethodData;
import dev.denwav.hypo.model.data.types.PrimitiveType;
import io.papermc.codebook.lvt.suggestion.context.ContainerContext;
import io.papermc.codebook.lvt.suggestion.context.method.MethodCallContext;
import io.papermc.codebook.lvt.suggestion.context.method.MethodInsnContext;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.LabelNode;
import org.objectweb.asm.tree.LocalVariableNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.VarInsnNode;

public class PositionsSuggester implements LvtSuggester {

@Override
public @Nullable String suggestFromMethod(
final MethodCallContext call, final MethodInsnContext insn, final ContainerContext container)
throws IOException {
if ("net/minecraft/core/SectionPos".equals(insn.owner().name())) {
return suggestNameForSectionPos(container.node(), call.data(), insn.node());
} else if ("net/minecraft/core/QuartPos".equals(insn.owner().name())) {
return suggestNameForQuartPos(container.node(), call.data(), insn.node());
} else if ("net/minecraft/core/BlockPos".equals(insn.owner().name())) {
return suggestNameForBlockPos(call.data());
} else if ("net/minecraft/world/level/ChunkPos".equals(insn.owner().name())) {
return suggestNameForChunkPos(call.data());
}
return null;
}

enum PosType {
BLOCK("block", "blockpos"),
QUART("quart", "quartpos", "biome", "biomepos"),
SECTION("section", "sectionpos");

final Set<String> possibleNames;
final String localName;

PosType(final String... possibleNames) {
this.possibleNames = Set.of(possibleNames);
this.localName = this.name().toLowerCase(Locale.ENGLISH) + "Pos";
}
}

private record MethodConfig(PosType returnType, PosType paramType, String prefix) {

private MethodConfig(final PosType returnType, final PosType paramType) {
this(returnType, paramType, "");
}

String varName(final String suffix) {
if (this.prefix.isEmpty()) {
return this.returnType.localName + suffix;
} else {
return this.prefix + capitalize(this.returnType.localName, 0) + suffix;
}
}
}

private static @Nullable String suggestNameForSectionPos(
final MethodNode enclosingMethodNode, final MethodData method, final MethodInsnNode insn) {
// this matches 2 methods for each x, y, z. One static that takes the packed position, the output names are
// appropriate for both method types
final @Nullable String possibleSimpleName =
switch (method.name()) {
case "x" -> "sectionX";
case "y" -> "sectionY";
case "z" -> "sectionZ";
case "blockToSection", "asLong" -> "packedSectionPos";
default -> null;
};
if (possibleSimpleName != null) {
return possibleSimpleName;
}

final @Nullable MethodConfig methodConfig =
switch (method.name()) {
case "blockToSectionCoord", "posToSectionCoord" -> new MethodConfig(PosType.SECTION, PosType.BLOCK);
case "sectionToBlockCoord" -> new MethodConfig(PosType.BLOCK, PosType.SECTION);
case "sectionRelative" -> new MethodConfig(PosType.BLOCK, PosType.BLOCK, "relative");
default -> null;
};

return getCoordLocalNameFromMethodPair(enclosingMethodNode, insn, method, methodConfig);
}

private static @Nullable String suggestNameForQuartPos(
final MethodNode enclosingMethodNode, final MethodData method, final MethodInsnNode insn) {
// all methods in QuartPos have a single int param and return int
if (method.params().size() != 1
|| method.param(0) != PrimitiveType.INT
|| method.returnType() != PrimitiveType.INT) {
return null;
}

final @Nullable MethodConfig methodConfig =
switch (method.name()) {
case "fromBlock" -> new MethodConfig(PosType.QUART, PosType.BLOCK);
case "toBlock" -> new MethodConfig(PosType.BLOCK, PosType.QUART);
case "fromSection" -> new MethodConfig(PosType.QUART, PosType.SECTION);
case "toSection" -> new MethodConfig(PosType.SECTION, PosType.QUART);
default -> null;
};
if (methodConfig == null) {
return null;
}

return getCoordLocalNameFromMethodPair(enclosingMethodNode, insn, method, methodConfig);
}

private static @Nullable String suggestNameForBlockPos(final MethodData method) {
final String suggestion;
if (method.name().equals("asLong")) {
suggestion = "packedBlockPos";
} else if (method.isStatic() && method.name().equals("offset") && method.returnType() == PrimitiveType.LONG) {
suggestion = "offsetPackedBlockPos";
} else {
return null;
}
return suggestion;
}

private static @Nullable String suggestNameForChunkPos(final MethodData method) {
final String suggestion;
if (method.name().equals("asLong") || method.name().equals("toLong")) {
suggestion = "packedChunkPos";
} else {
return null;
}
return suggestion;
}

private static final String[] COMMON_PERSISTENT_PREFIXES = new String[] {"min", "max"};

private static @Nullable String getCoordLocalNameFromMethodPair(
final MethodNode enclosingMethodNode,
final MethodInsnNode insn,
final MethodData method,
final @Nullable MethodConfig methodConfig) {
if (methodConfig == null) {
return null;
}

if (method.params().size() != 1) {
// add "Coord" since we don't know if its x, y, or z and more
// than 1 param makes it too complex to figure out
return methodConfig.varName("Coord");
}

final AbstractInsnNode prev = requireNonNull(prevInsnIgnoringConvertCast(insn));
@Nullable String suggestion = null;
if (prev instanceof final VarInsnNode varNode) {
final LocalVariableNode paramVarNode = findLocalVar(enclosingMethodNode, insn, varNode.var);
suggestion = suggestSpecificCoordName(methodConfig, paramVarNode.name, COMMON_PERSISTENT_PREFIXES);
} else if (prev instanceof final MethodInsnNode methodNode) {
final @Nullable String strippedName =
methodNode.name.startsWith("get") ? decapitalize(methodNode.name.substring(3)) : methodNode.name;
if (strippedName != null) {
suggestion = suggestSpecificCoordName(methodConfig, strippedName, COMMON_PERSISTENT_PREFIXES);
}
} else if (prev instanceof final FieldInsnNode fieldNode && fieldNode.getOpcode() == Opcodes.GETFIELD) {
suggestion = suggestSpecificCoordName(methodConfig, fieldNode.name, COMMON_PERSISTENT_PREFIXES);
}
return Objects.requireNonNullElseGet(
suggestion, () -> methodConfig.varName("Coord")); // add "Coord" since we don't know if its x, y, or z
}

private static @Nullable String suggestSpecificCoordName(
final MethodConfig methodConfig, final String fullName, final String... persistentPrefixes) {
String prefix = "";
if (fullName.length() > 1) {
for (final String persistentPrefix : persistentPrefixes) {
if (fullName.startsWith(persistentPrefix)) {
prefix = persistentPrefix;
break;
}
}
}
final String nameWithoutPrefix = fullName.substring(prefix.length());
final int possibleCoordIdx = getPossibleCoordIdx(nameWithoutPrefix);
if (possibleCoordIdx > -1
&& (nameWithoutPrefix.length() == 1
|| methodConfig.paramType.possibleNames.contains(
nameWithoutPrefix.substring(0, possibleCoordIdx).toLowerCase(Locale.ENGLISH)))) {
return methodConfig.varName(
capitalize(prefix, 0) + Character.toUpperCase(nameWithoutPrefix.charAt(possibleCoordIdx)));
}
return null;
}

private static int getPossibleCoordIdx(final String name) {
for (int i = name.length() - 1; i >= 0; i--) {
final char ch = name.charAt(i);
if (!Character.isAlphabetic(ch)) {
continue;
}
if (isCoord(ch)) {
return i;
}
return -1;
}
return -1; // don't think this is possible
}

private static boolean isCoord(final char ch) {
return ch == 'X' || ch == 'Y' || ch == 'Z' || ch == 'x' || ch == 'y' || ch == 'z';
}

private static LocalVariableNode findLocalVar(
final MethodNode enclosingMethod, final AbstractInsnNode insn, final int varIdx) {
final List<LocalVariableNode> matching = new ArrayList<>();
for (final LocalVariableNode lvn : requireNonNull(enclosingMethod.localVariables)) {
if (lvn.index == varIdx) {
matching.add(lvn);
}
}
if (matching.isEmpty()) {
throw new IllegalStateException("Cannot find idx " + varIdx + " on " + enclosingMethod.name + " "
+ enclosingMethod.desc + " (no match)");
} else if (matching.size() == 1) {
return matching.get(0);
} else {
@Nullable AbstractInsnNode prev = insn.getPrevious();
if (prev == null) {
throw new IllegalStateException("Cannot find idx " + varIdx + " on " + enclosingMethod.name + " "
+ enclosingMethod.desc + " (multiple matches)");
}
while (true) {
while (!(prev instanceof final LabelNode labelNode)) {
prev = prev.getPrevious();
}
for (final LocalVariableNode match : matching) {
if (match.start.getLabel() == labelNode.getLabel()) {
return match;
}
}
prev = prev.getPrevious();
}
}
}
}