diff --git a/enigma_test/simple_type_field_names.json5 b/enigma_test/simple_type_field_names.json5 index 8a1e5e0d..5c93fa92 100644 --- a/enigma_test/simple_type_field_names.json5 +++ b/enigma_test/simple_type_field_names.json5 @@ -1,7 +1,7 @@ { $schema: "../src/main/resources/simple_type_field_names_schema.json", - "com/a/a/a": "config", + "com/a/c/a": "config", // for conflict fix test "com/a/n": { // Identifiable @@ -17,15 +17,15 @@ }, // Position - "com/a/a/b": "pos", // Pos - "com/a/a/c": { // Position + "com/a/c/b": "pos", // Pos + "com/a/c/c": { // Position local_name: "pos", exclusive: true, fallback: [ "position" ] }, - "com/a/a/d": { // RandomPosition + "com/a/c/d": { // RandomPosition local_name: "pos", exclusive: true, fallback: [ @@ -35,7 +35,7 @@ }, // State - "com/a/a/e": { // StateA + "com/a/c/e": { // StateA local_name: "state", static_name: "STATIC_STATE", exclusive: true, @@ -46,7 +46,7 @@ } ] }, - "com/a/a/f": { // StateB + "com/a/c/f": { // StateB local_name: "state", static_name: "STATIC_STATE", exclusive: true, @@ -59,35 +59,52 @@ }, // Value - "com/a/a/g": { // ValueA + "com/a/c/g": { // ValueA local_name: "value", exclusive: true, fallback: [ "valueA" ] }, - "com/a/a/h": { // ValueB + "com/a/c/h": { // ValueB local_name: "value", exclusive: true, fallback: [ "valueB" ] }, - "com/a/a/i": { // ValueC + "com/a/c/i": { // ValueC local_name: "value", exclusive: true, fallback: [ "valueC" ] }, - "com/a/a/j": { // ValueD + "com/a/c/j": { // ValueD local_name: "valueD", exclusive: true, inherit: true }, - "com/a/a/l": { // ValueE + "com/a/c/l": { // ValueE local_name: "valueE", exclusive: true }, + + "com/a/b/b": { // Entity + local_name: "entity", + inherit: { + type: "TRUNCATED_SUBTYPE_NAME", + suffix: "Entity" + } + }, + "com/a/a/a": { // BlockEntityRenderer + local_name: "blockEntityRenderer", + inherit: { + type: "TRANSFORMED_SUBTYPE_NAME", + "pattern": "(.+)BlockEntityRenderer", + "replacement": "$1Renderer" + } + }, + "not/present": "missing" } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ef92cb8a..dfea977b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -enigma = "2.7.1" +enigma = "2.7.2" asm = "9.9" quilt_json_parser = "0.3.1" diff --git a/src/main/java/org/quiltmc/enigma_plugin/QuiltEnigmaPlugin.java b/src/main/java/org/quiltmc/enigma_plugin/QuiltEnigmaPlugin.java index 286815ab..b99bd857 100644 --- a/src/main/java/org/quiltmc/enigma_plugin/QuiltEnigmaPlugin.java +++ b/src/main/java/org/quiltmc/enigma_plugin/QuiltEnigmaPlugin.java @@ -17,7 +17,6 @@ package org.quiltmc.enigma_plugin; import org.jspecify.annotations.NonNull; -import org.quiltmc.enigma.api.Enigma; import org.quiltmc.enigma.api.EnigmaPlugin; import org.quiltmc.enigma.api.EnigmaPluginContext; import org.quiltmc.enigma.api.service.JarIndexerService; @@ -50,8 +49,10 @@ public void init(EnigmaPluginContext ctx) { @Override public boolean supportsEnigmaVersion(@NonNull Version enigmaVersion) { - return Enigma.MAJOR_VERSION == enigmaVersion.major() - && Enigma.MINOR_VERSION == enigmaVersion.minor(); + return enigmaVersion.major() == 2 + && enigmaVersion.minor() == 7 + // Enigma 2.7.2 adds InheritanceIndex::streamAncestors + && enigmaVersion.patch() >= 2; } @Override diff --git a/src/main/java/org/quiltmc/enigma_plugin/index/JarIndexer.java b/src/main/java/org/quiltmc/enigma_plugin/index/JarIndexer.java index bfb537aa..d9f673ed 100644 --- a/src/main/java/org/quiltmc/enigma_plugin/index/JarIndexer.java +++ b/src/main/java/org/quiltmc/enigma_plugin/index/JarIndexer.java @@ -24,6 +24,7 @@ import org.quiltmc.enigma.api.service.JarIndexerService; import org.quiltmc.enigma_plugin.QuiltEnigmaPlugin; import org.quiltmc.enigma_plugin.index.constant_fields.ConstantFieldIndex; +import org.quiltmc.enigma_plugin.index.simple_type_single.SimpleSubtypeSingleIndex; import org.quiltmc.enigma_plugin.index.simple_type_single.SimpleTypeSingleIndex; import java.util.ArrayList; @@ -42,6 +43,7 @@ public JarIndexer() { this.addIndex(new ConstructorParametersIndex()); this.addIndex(new GetterSetterIndex()); this.addIndex(new SimpleTypeSingleIndex()); + this.addIndex(new SimpleSubtypeSingleIndex()); this.addIndex(new DelegateParametersIndex()); this.addIndex(new LoggerIndex()); this.addIndex(new LambdaParametersIndex()); diff --git a/src/main/java/org/quiltmc/enigma_plugin/index/simple_type_single/SimpleSubtypeSingleIndex.java b/src/main/java/org/quiltmc/enigma_plugin/index/simple_type_single/SimpleSubtypeSingleIndex.java new file mode 100644 index 00000000..147f24e5 --- /dev/null +++ b/src/main/java/org/quiltmc/enigma_plugin/index/simple_type_single/SimpleSubtypeSingleIndex.java @@ -0,0 +1,351 @@ +/* + * Copyright 2022 QuiltMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.enigma_plugin.index.simple_type_single; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.MethodNode; +import org.quiltmc.enigma.api.analysis.index.jar.InheritanceIndex; +import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; +import org.quiltmc.enigma.api.class_provider.ClassProvider; +import org.quiltmc.enigma.api.service.EnigmaServiceContext; +import org.quiltmc.enigma.api.service.JarIndexerService; +import org.quiltmc.enigma.api.translation.representation.MethodDescriptor; +import org.quiltmc.enigma.api.translation.representation.TypeDescriptor; +import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; +import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; +import org.quiltmc.enigma.api.translation.representation.entry.LocalVariableEntry; +import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; +import org.quiltmc.enigma_plugin.Arguments; +import org.quiltmc.enigma_plugin.index.Index; +import org.quiltmc.enigma_plugin.index.simple_type_single.SimpleTypeFieldNamesRegistry.Inherit; +import org.quiltmc.enigma_plugin.util.AsmUtil; +import org.quiltmc.enigma_plugin.util.Descriptors; + +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.quiltmc.enigma_plugin.util.StringUtil.getObjectTypeOrNull; + +/** + * Index of fields/local variables that whose names can be derived from their types and which + * are entirely unique within their context (no other fields/local vars in the same scope have the same type). + */ +public class SimpleSubtypeSingleIndex extends Index { + private final Map> paramsByType = new HashMap<>(); + private final Map> fieldsByType = new HashMap<>(); + private final Map fieldCacheByParent = new HashMap<>(); + private SimpleTypeFieldNamesRegistry registry; + + private InheritanceIndex inheritance; + + public SimpleSubtypeSingleIndex() { + super(null); + } + + @Override + public void withContext(EnigmaServiceContext context) { + super.withContext(context); + + this.loadRegistry(context.getSingleArgument(Arguments.SIMPLE_TYPE_FIELD_NAMES_PATH) + .map(context::getPath).orElse(null)); + } + + @Override + public void setIndexingContext(Set classes, JarIndex jarIndex) { + this.inheritance = jarIndex.getIndex(InheritanceIndex.class); + } + + private void loadRegistry(Path path) { + if (path == null) { + this.registry = null; + return; + } + + this.registry = SimpleTypeFieldNamesRegistry.readFrom(path); + } + + @Override + public boolean isEnabled() { + return this.registry != null; + } + + public void forEachField(MemberAction action) { + this.fieldsByType.forEach((type, fields) -> { + fields.forEach((field, info) -> action.run(type, field, info)); + }); + } + + public void forEachFieldOfType(ClassEntry type, BiConsumer action) { + this.fieldsByType.getOrDefault(type, Map.of()).forEach(action); + } + + public void forEachParam(MemberAction action) { + this.paramsByType.forEach((type, params) -> { + params.forEach((param, entry) -> action.run(type, param, entry)); + }); + } + + public void forEachParamOfType(ClassEntry type, BiConsumer action) { + this.paramsByType.getOrDefault(type, Map.of()).forEach(action); + } + + @Override + public void onIndexingEnded() { + this.fieldCacheByParent.clear(); + } + + @Override + public void visitClassNode(ClassProvider provider, ClassNode node) { + if (!this.isEnabled()) return; + + var parentEntry = new ClassEntry(node.name); + + this.collectMatchingFields(provider, node, parentEntry).build().forEach((type, fields) -> { + this.fieldsByType.computeIfAbsent(type, ignored -> new HashMap<>()).putAll(fields); + }); + + for (var method : node.methods) { + if (method.parameters == null) continue; + + var methodDescriptor = new MethodDescriptor(method.desc); + var methodEntry = new MethodEntry(parentEntry, method.name, methodDescriptor); + var parameters = Descriptors.getParameters(method); + + // Count the times a type is used in the descriptor + var types = new HashMap(); + + for (var param : parameters) { + types.compute(param.type(), (t, old) -> { + if (old == null) { + return 1; + } else { + return old + 1; + } + }); + } + + // Don't propose names for types appearing more than once + final Set bannedTypes = types.entrySet().stream() + .filter(entry -> entry.getValue() > 1) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + + this.collectMatchingParameters(provider, method, methodEntry, bannedTypes, parameters).forEach((param, builder) -> { + this.paramsByType.computeIfAbsent(new ClassEntry(builder.type()), ignored -> new HashMap<>()).put(param, builder.entry()); + }); + } + } + + private FieldBuilders collectMatchingFields(ClassProvider classProvider, ClassNode classNode, ClassEntry parentEntry) { + var existing = this.fieldCacheByParent.get(classNode); + + if (existing != null) return existing; + + var builders = FieldBuilders.of(); + + for (var field : classNode.fields) { + // Collect names from the outer class as initial context + if (classNode.outerClass != null) { + ClassNode outerClass = classProvider.get(classNode.outerClass); + + if (outerClass != null) { + builders.merge(this.collectMatchingFields(classProvider, outerClass, new ClassEntry(outerClass.name))); + } + } + + String type = getObjectTypeOrNull(field.desc); + if (type == null) { + continue; + } + + var entry = this.getEntry(classProvider, type); + if (entry != null) { + boolean isConstant = AsmUtil.matchAccess(field, ACC_STATIC, ACC_FINAL); + Map destination = isConstant ? builders.constantsByType : builders.fieldsByType; + if (destination.containsKey(type)) { + destination.put(type, FieldBuilderEntry.DUPLICATE); + } else { + destination.put(type, new FieldBuilderEntry(parentEntry, field, type, entry, isConstant)); + } + } + } + + this.fieldCacheByParent.put(classNode, builders); + + return builders; + } + + private Map collectMatchingParameters( + ClassProvider classProvider, MethodNode parentNode, MethodEntry parentEntry, + Set bannedTypes, List parameters + ) { + var matchingParameters = new HashMap(); + + for (int index = 0, lvtIndex = 0; index < parentNode.parameters.size(); index++) { + if (index > 0) { + lvtIndex += parameters.get(index - 1).getSize(); + } + + Descriptors.ParameterEntry paramEntry = parameters.get(index); + if (bannedTypes.contains(paramEntry.type())) { + continue; + } + + String descriptor = paramEntry.getDescriptor(); + String type = getObjectTypeOrNull(descriptor); + if (type == null) { + continue; + } + + var entry = this.getEntry(classProvider, type); + if (entry != null) { + boolean isStatic = AsmUtil.matchAccess(parentNode, ACC_STATIC); + matchingParameters.put(new LocalVariableEntry(parentEntry, lvtIndex + (isStatic ? 0 : 1)), new ParamBuilderEntry(entry, type)); + } + } + + return matchingParameters; + } + + @Nullable + private SubtypeEntry getEntry(ClassProvider classProvider, String type) { + if (this.registry.getEntry(type) != null) { + // do not propose names for the super type + // this also skips any type with a simple type name + return null; + } + + final ClassNode typeClass = classProvider.get(type); + if (typeClass == null || AsmUtil.matchAccess(typeClass, ACC_ABSTRACT) || AsmUtil.matchAccess(typeClass, ACC_INTERFACE)) { + // skip non-concrete types + return null; + } + + // Check all parent classes for an entry. This goes in order of super/interface, supersuper/interfacesuper, etc + final ClassEntry typeEntry = new ClassEntry(type); + return this.inheritance + .streamAncestors(typeEntry) + .flatMap(ancestor -> { + var entry = this.registry.getEntry(ancestor.getFullName()); + + if (entry != null) { + if (entry.inherit() instanceof Inherit.TruncatedSubtypeName truncated) { + return Stream.of(new SubtypeEntry(entry.type(), new Renamer.Truncate(truncated.suffix()))); + } else if (entry.inherit() instanceof Inherit.TransformedSubtypeName transformed) { + return Stream.of(new SubtypeEntry(entry.type(), new Renamer.Transform(transformed.pattern(), transformed.replacement()))); + } + } + + return Stream.empty(); + }) + .findFirst() + .orElse(null); + } + + public record FieldInfo(SubtypeEntry entry, boolean isConstant) { } + + public record SubtypeEntry(String type, Renamer renamer) { } + + public sealed interface Renamer { + Optional rename(String original); + + record Truncate(String suffix) implements Renamer { + public Truncate { + if (suffix.isEmpty()) { + throw new IllegalArgumentException("suffix must not be empty"); + } + } + + @Override + public Optional rename(String original) { + final int lengthDiff = original.length() - this.suffix.length(); + return lengthDiff > 0 && original.endsWith(this.suffix) + ? Optional.of(original.substring(0, lengthDiff)) + : Optional.empty(); + } + } + + record Transform(Pattern pattern, String replacement) implements Renamer { + @Override + public Optional rename(String original) { + final Matcher matcher = this.pattern.matcher(original); + return matcher.matches() + ? Optional.of(matcher.replaceFirst(this.replacement)) + : Optional.empty(); + } + } + } + + private record ParamBuilderEntry(SubtypeEntry entry, String type) { } + + private record FieldBuilderEntry(ClassEntry parent, FieldNode field, String type, SubtypeEntry subtypeEntry, boolean isConstant) { + static final FieldBuilderEntry DUPLICATE = new FieldBuilderEntry(null, null, null, null, false); + + static boolean isNotDuplicate(FieldBuilderEntry entry) { + return entry != DUPLICATE; + } + + FieldInfo toInfo() { + return new FieldInfo(this.subtypeEntry, this.isConstant); + } + + FieldEntry toEntry() { + return new FieldEntry(this.parent, this.field.name, new TypeDescriptor(this.field.desc)); + } + } + + private record FieldBuilders(Map fieldsByType, Map constantsByType) { + static FieldBuilders of() { + return new FieldBuilders(new HashMap<>(), new HashMap<>()); + } + + void merge(FieldBuilders other) { + this.fieldsByType.putAll(other.fieldsByType); + this.constantsByType.putAll(other.constantsByType); + } + + Map> build() { + return Stream + .concat(this.fieldsByType.values().stream(), this.constantsByType.values().stream()) + .filter(FieldBuilderEntry::isNotDuplicate) + .collect(Collectors.groupingBy( + builder -> new ClassEntry(builder.type), + Collectors.toMap( + FieldBuilderEntry::toEntry, + FieldBuilderEntry::toInfo + ) + )); + } + } + + @FunctionalInterface + public interface MemberAction { + void run(ClassEntry type, M member, I info); + } +} diff --git a/src/main/java/org/quiltmc/enigma_plugin/index/simple_type_single/SimpleTypeFieldNamesRegistry.java b/src/main/java/org/quiltmc/enigma_plugin/index/simple_type_single/SimpleTypeFieldNamesRegistry.java index dec06b08..4daf6d27 100644 --- a/src/main/java/org/quiltmc/enigma_plugin/index/simple_type_single/SimpleTypeFieldNamesRegistry.java +++ b/src/main/java/org/quiltmc/enigma_plugin/index/simple_type_single/SimpleTypeFieldNamesRegistry.java @@ -16,6 +16,7 @@ package org.quiltmc.enigma_plugin.index.simple_type_single; +import org.quiltmc.enigma.util.Result; import org.jspecify.annotations.Nullable; import org.quiltmc.enigma_plugin.util.CasingUtil; import org.quiltmc.parsers.json.JsonReader; @@ -25,21 +26,49 @@ import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.stream.Collectors; import java.util.stream.Stream; -public class SimpleTypeFieldNamesRegistry { +import static org.quiltmc.enigma_plugin.util.StringUtil.isValidJavaIdentifier; + +public final class SimpleTypeFieldNamesRegistry { + private static final String INVALID_LOCAL_NAME_FOR_TYPE_TEMPLATE = "Invalid local name \"%s\" for type \"%s\""; + + // this would ideally have weak value references, but we don't have guava's MapMaker/Cache, + // and in practice it only stores one path and references its registry are never released + private static final Map READ_CACHE = new HashMap<>(1); + + public static SimpleTypeFieldNamesRegistry readFrom(Path path) { + return READ_CACHE.computeIfAbsent(path, newPath -> { + final SimpleTypeFieldNamesRegistry registry = new SimpleTypeFieldNamesRegistry(newPath); + registry.read(); + return registry; + }); + } + + private static void skipToObjectEnd(JsonReader reader) throws IOException { + while (reader.hasNext()) { + reader.skipValue(); + } + + reader.endObject(); + } + private final Path path; /** * Using a {@link LinkedHashMap} to ensure we keep the read order. */ private final Map entries = new LinkedHashMap<>(); - public SimpleTypeFieldNamesRegistry(Path path) { + private SimpleTypeFieldNamesRegistry(Path path) { this.path = path; } @@ -51,7 +80,7 @@ public Stream streamTypes() { return this.entries.keySet().stream(); } - public void read() { + private void read() { try (var reader = JsonReader.json5(this.path)) { if (reader.peek() != JsonToken.BEGIN_OBJECT) { return; @@ -74,13 +103,19 @@ public void read() { switch (reader.peek()) { case STRING -> { String localName = reader.nextString(); + + if (!isValidJavaIdentifier(localName)) { + Logger.error(INVALID_LOCAL_NAME_FOR_TYPE_TEMPLATE.formatted(localName, type)); + break; + } + this.entries.put(type, new Entry(type, localName, CasingUtil.toScreamingSnakeCase(localName))); } case BEGIN_OBJECT -> { String localName = null; String staticName = null; boolean exclusive = false; - boolean inherit = false; + Result inherit = Result.ok(Inherit.DEFAULT); List fallback = Collections.emptyList(); reader.beginObject(); @@ -92,7 +127,7 @@ public void read() { case "local_name" -> localName = reader.nextString(); case "static_name" -> staticName = reader.nextString(); case "exclusive" -> exclusive = reader.nextBoolean(); - case "inherit" -> inherit = reader.nextBoolean(); + case "inherit" -> inherit = Inherit.read(reader); case "fallback" -> { reader.beginArray(); @@ -111,9 +146,24 @@ public void read() { break; } - if (staticName == null) staticName = CasingUtil.toScreamingSnakeCase(localName); + if (!isValidJavaIdentifier(localName)) { + Logger.error(INVALID_LOCAL_NAME_FOR_TYPE_TEMPLATE.formatted(localName, type)); + break; + } + + if (staticName == null) { + staticName = CasingUtil.toScreamingSnakeCase(localName); + } else if (!isValidJavaIdentifier(staticName)) { + Logger.error("Invalid static name \"%s\" for type \"%s\"".formatted(staticName, type)); + break; + } + + if (inherit.isErr()) { + Logger.error("Invalid inherit value: " + inherit.unwrapErr()); + break; + } - this.entries.put(type, new Entry(type, new Name(localName, staticName), exclusive, inherit, fallback)); + this.entries.put(type, new Entry(type, new Name(localName, staticName), exclusive, inherit.unwrap(), fallback)); } default -> reader.skipValue(); } @@ -166,9 +216,9 @@ private List collectFallbacks(JsonReader reader, String type) throws IOExc return list; } - public record Entry(String type, Name name, boolean exclusive, boolean inherit, List fallback) { + public record Entry(String type, Name name, boolean exclusive, Inherit inherit, List fallback) { public Entry(String type, String localName, String staticName) { - this(type, new Name(localName, staticName), false, false, Collections.emptyList()); + this(type, new Name(localName, staticName), false, Inherit.None.INSTANCE, Collections.emptyList()); } public @Nullable Name findFallback(Predicate predicate) { @@ -182,6 +232,135 @@ public Entry(String type, String localName, String staticName) { } } - public record Name(String local, String staticName) { + public record Name(String local, String constant) { + } + + public sealed interface Inherit { + Inherit DEFAULT = None.INSTANCE; + + String KEY = "inherit"; + String TYPE_KEY = "type"; + + static Result read(JsonReader reader) throws IOException { + if (reader.peek() == JsonToken.BEGIN_OBJECT) { + reader.beginObject(); + + if (reader.hasNext() && reader.nextName().equals(TYPE_KEY)) { + String typeName = reader.nextString(); + final Type type; + try { + type = Type.valueOf(typeName); + } catch (IllegalArgumentException e) { + skipToObjectEnd(reader); + + return Result.err( + "Invalid \"%s\" object \"%s\"; must be one of: %s".formatted( + KEY, TYPE_KEY, + Arrays.stream(Type.values()) + .map(Object::toString) + .map(name -> "\"" + name + "\"") + .collect(Collectors.joining(", ")) + ) + ); + } + + Result inherit = type.read(reader); + + skipToObjectEnd(reader); + + return inherit; + } else { + reader.skipValue(); + skipToObjectEnd(reader); + + return Result.err("\"%s\" must be the first property of an \"%s\" object".formatted(TYPE_KEY, KEY)); + } + } else { + reader.skipValue(); + return Result.err("must be OBJECT"); + } + } + + private static Result missingRequirementErrOf(String requirer, String required) { + return Result.err("%s requires a %s".formatted(requirer, required)); + } + + enum Type { + NONE, + DIRECT, + TRUNCATED_SUBTYPE_NAME, + TRANSFORMED_SUBTYPE_NAME; + + Result read(JsonReader reader) throws IOException { + return switch (this) { + case NONE -> Result.ok(None.INSTANCE); + case DIRECT -> Result.ok(Direct.INSTANCE); + case TRUNCATED_SUBTYPE_NAME -> TruncatedSubtypeName.readValue(reader); + case TRANSFORMED_SUBTYPE_NAME -> TransformedSubtypeName.readValue(reader); + }; + } + } + + final class None implements Inherit { + public static final None INSTANCE = new None(); + + private None() { } + } + + final class Direct implements Inherit { + public static final Direct INSTANCE = new Direct(); + + private Direct() { } + } + + record TruncatedSubtypeName(String suffix) implements Inherit { + private static final String SUFFIX_KEY = "suffix"; + + private static Result readValue(JsonReader reader) throws IOException { + while (reader.hasNext()) { + String key = reader.nextName(); + if (key.equals(SUFFIX_KEY)) { + final String suffix = reader.nextString(); + if (suffix.isEmpty() || !suffix.chars().allMatch(Character::isJavaIdentifierPart)) { + return Result.err("invalid suffix: " + suffix); + } + + return Result.ok(new TruncatedSubtypeName(suffix)); + } else { + reader.skipValue(); + } + } + + return missingRequirementErrOf(Type.TRUNCATED_SUBTYPE_NAME.toString(), SUFFIX_KEY); + } + } + + record TransformedSubtypeName(Pattern pattern, String replacement) implements Inherit { + private static final String PATTERN_KEY = "pattern"; + private static final String REPLACEMENT_KEY = "replacement"; + + private static Result readValue(JsonReader reader) throws IOException { + Pattern pattern = null; + String replacement = null; + while (reader.hasNext()) { + String key = reader.nextName(); + switch (key) { + case PATTERN_KEY -> pattern = Pattern.compile(reader.nextString()); + case REPLACEMENT_KEY -> replacement = reader.nextString(); + default -> reader.skipValue(); + } + } + + if (pattern == null) { + return missingRequirementErrOf(Type.TRANSFORMED_SUBTYPE_NAME.toString(), PATTERN_KEY); + } + + if (replacement == null) { + return missingRequirementErrOf(Type.TRANSFORMED_SUBTYPE_NAME.toString(), REPLACEMENT_KEY); + } + + return Result.ok(new TransformedSubtypeName(pattern, replacement)); + } + } } } diff --git a/src/main/java/org/quiltmc/enigma_plugin/index/simple_type_single/SimpleTypeSingleIndex.java b/src/main/java/org/quiltmc/enigma_plugin/index/simple_type_single/SimpleTypeSingleIndex.java index aa0719fc..c9755467 100644 --- a/src/main/java/org/quiltmc/enigma_plugin/index/simple_type_single/SimpleTypeSingleIndex.java +++ b/src/main/java/org/quiltmc/enigma_plugin/index/simple_type_single/SimpleTypeSingleIndex.java @@ -28,7 +28,6 @@ import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; import org.quiltmc.enigma.api.translation.representation.entry.LocalVariableEntry; import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; -import org.jetbrains.annotations.TestOnly; import org.objectweb.asm.Type; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.FieldNode; @@ -36,21 +35,25 @@ import org.objectweb.asm.tree.ParameterNode; import org.quiltmc.enigma_plugin.Arguments; import org.quiltmc.enigma_plugin.index.Index; +import org.quiltmc.enigma_plugin.index.simple_type_single.SimpleTypeFieldNamesRegistry.Inherit; import org.quiltmc.enigma_plugin.index.simple_type_single.SimpleTypeFieldNamesRegistry.Name; import org.quiltmc.enigma_plugin.util.AsmUtil; import org.quiltmc.enigma_plugin.util.Descriptors; import org.tinylog.Logger; import java.nio.file.Path; -import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; +import static org.quiltmc.enigma_plugin.util.StringUtil.getObjectTypeOrNull; + /** * Index of fields/local variables that are of a rather simple type (as-in easy to guess the variable name) and which * they are entirely unique within their context (no other fields/local vars in the same scope have the same type). @@ -103,14 +106,13 @@ public void setIndexingContext(Set classes, JarIndex jarIndex) { this.inheritance = jarIndex.getIndex(InheritanceIndex.class); } - public void loadRegistry(Path path) { + private void loadRegistry(Path path) { if (path == null) { this.registry = null; return; } - this.registry = new SimpleTypeFieldNamesRegistry(path); - this.registry.read(); + this.registry = SimpleTypeFieldNamesRegistry.readFrom(path); this.unverifiedTypes.clear(); if (this.verificationLevel != VerificationLevel.NONE) { @@ -123,10 +125,6 @@ public boolean isEnabled() { return this.registry != null; } - public void dropCache() { - this.fieldCache.clear(); - } - public @Nullable String getField(FieldEntry fieldEntry) { return this.fields.get(fieldEntry); } @@ -170,22 +168,9 @@ public void verifyTypes() { } } - @TestOnly - public List getParamsOf(MethodEntry methodEntry) { - var params = new ArrayList(); - - this.parameters.forEach((param, name) -> { - if (param.getParent() != null && param.getParent().equals(methodEntry)) { - params.add(param); - } - }); - - return params; - } - @Override public void onIndexingEnded() { - this.dropCache(); + this.fieldCache.clear(); } @Override @@ -196,14 +181,10 @@ public void visitClassNode(ClassProvider provider, ClassNode node) { this.unverifiedTypes.remove(node.name); - this.collectMatchingFields(provider, node).forEach((name, entry) -> { + this.collectMatchingFields(provider, node, parentEntry).forEach((name, entry) -> { if (!entry.isNull()) { - var fieldEntry = new FieldEntry(parentEntry, entry.node().name, new TypeDescriptor(entry.node().desc)); - this.fields.put(fieldEntry, - AsmUtil.matchAccess(entry.node(), ACC_STATIC, ACC_FINAL) - ? entry.name().staticName() - : entry.name().local() - ); + var fieldEntry = new FieldEntry(entry.parent, entry.node().name, new TypeDescriptor(entry.node().desc)); + this.fields.put(fieldEntry, name); } }); @@ -235,7 +216,7 @@ public void visitClassNode(ClassProvider provider, ClassNode node) { this.collectMatchingParameters(method, bannedTypes, parameters).forEach((name, param) -> { if (!param.isNull()) { - boolean isStatic = AsmUtil.maskMatch(method.access, ACC_STATIC); + boolean isStatic = AsmUtil.matchAccess(method, ACC_STATIC); int index = param.index() + (isStatic ? 0 : 1); var paramEntry = new LocalVariableEntry(methodEntry, index); this.parameters.put(paramEntry, name); @@ -245,8 +226,7 @@ public void visitClassNode(ClassProvider provider, ClassNode node) { } } - private Map collectMatchingFields(ClassProvider classProvider, - ClassNode classNode) { + private Map collectMatchingFields(ClassProvider classProvider, ClassNode classNode, ClassEntry parent) { var existing = this.fieldCache.get(classNode); if (existing != null) return existing; @@ -258,7 +238,7 @@ private Map collectMatchingFields(ClassProvider clas ClassNode outerClass = classProvider.get(classNode.outerClass); if (outerClass != null) { - knownFields.putAll(this.collectMatchingFields(classProvider, outerClass)); + knownFields.putAll(this.collectMatchingFields(classProvider, outerClass, new ClassEntry(outerClass.name))); } } @@ -267,43 +247,45 @@ private Map collectMatchingFields(ClassProvider clas var entry = this.getEntry(type); if (entry != null) { + Function nameGetter = AsmUtil.matchAccess(field, ACC_STATIC, ACC_FINAL) ? Name::constant : Name::local; + // Check if there's a field by the default name - var existingEntry = knownFields.get(entry.name().local()); + var existingEntry = knownFields.get(nameGetter.apply(entry.name())); if (existingEntry != null) { // If the existing field is of the same type, remove it and skip this one if (existingEntry.entry() == entry) { - knownFields.put(entry.name().local(), FieldBuildingEntry.createNull(entry)); + knownFields.put(nameGetter.apply(entry.name()), FieldBuildingEntry.createNull(entry)); continue; } // If there's already a field by the default name, find a fallback name - Name foundFallback = entry.findFallback(fallback -> !knownFields.containsKey(fallback.local())); + Name foundFallback = entry.findFallback(fallback -> !knownFields.containsKey(nameGetter.apply(fallback))); if (foundFallback != null) { - knownFields.put(foundFallback.local(), new FieldBuildingEntry(field, foundFallback, entry)); + knownFields.put(nameGetter.apply(foundFallback), new FieldBuildingEntry(parent, field, foundFallback, entry)); // If the existing entry is exclusive, remove it and if possible replace it with one of its fallbacks if (!existingEntry.isNull() && existingEntry.entry().exclusive()) { Name replacement = existingEntry.entry().findFallback( - fallback -> !knownFields.containsKey(fallback.local()) + fallback -> !knownFields.containsKey(nameGetter.apply(fallback)) ); - knownFields.put(entry.name().local(), FieldBuildingEntry.createNull(entry)); + knownFields.put(nameGetter.apply(entry.name()), FieldBuildingEntry.createNull(entry)); if (replacement != null) { - knownFields.put(replacement.local(), - new FieldBuildingEntry(existingEntry.node(), replacement, existingEntry.entry()) + knownFields.put(nameGetter.apply(replacement), + new FieldBuildingEntry(parent, existingEntry.node(), replacement, existingEntry.entry()) ); } } } else { // If a fallback name couldn't be found, remove the name for the existing field - knownFields.put(entry.name().local(), FieldBuildingEntry.createNull(entry)); + knownFields.put(nameGetter.apply(entry.name()), FieldBuildingEntry.createNull(entry)); } } else { // Another field with the name doesn't exist, proceed as usual - knownFields.put(entry.name().local(), new FieldBuildingEntry(field, entry.name(), entry)); + knownFields.put(nameGetter.apply(entry.name()), new FieldBuildingEntry(parent, field, entry.name(), entry)); } } } @@ -375,47 +357,46 @@ private Map collectMatchingParameters(MethodNode } // Check all parent classes for an entry. This goes in order of super/interface, supersuper/interfacesuper, etc - for (ClassEntry ancestor : this.inheritance.getAncestors(new ClassEntry(type))) { - entry = this.registry.getEntry(ancestor.getFullName()); - - // Only return if the entry allows inheritance - if (entry != null && entry.inherit()) { - return entry; - } - } - - return null; + return this.inheritance + .streamAncestors(new ClassEntry(type)) + .flatMap(ancestor -> Optional + .ofNullable(this.registry.getEntry(ancestor.getFullName())) + // Only return if the entry allows inheritance + .filter(ancestorEntry -> ancestorEntry.inherit() == Inherit.Direct.INSTANCE) + .stream() + ) + .findFirst() + .orElse(null); } @Nullable private String verifyTypeOrNull(String descriptor) { - if (descriptor.charAt(0) != 'L') { + String type = getObjectTypeOrNull(descriptor); + if (type == null) { return null; } - String type = descriptor.substring(1, descriptor.length() - 1); - this.unverifiedTypes.remove(type); return type; } - private record FieldBuildingEntry(FieldNode node, Name name, SimpleTypeFieldNamesRegistry.Entry entry) { - public static FieldBuildingEntry createNull(SimpleTypeFieldNamesRegistry.Entry entry) { - return new FieldBuildingEntry(null, null, entry); + private record FieldBuildingEntry(ClassEntry parent, FieldNode node, Name name, SimpleTypeFieldNamesRegistry.Entry entry) { + static FieldBuildingEntry createNull(SimpleTypeFieldNamesRegistry.Entry entry) { + return new FieldBuildingEntry(null, null, null, entry); } - public boolean isNull() { + boolean isNull() { return this.node == null; } } private record ParameterBuildingEntry(ParameterNode node, int index, SimpleTypeFieldNamesRegistry.Entry entry) { - public static ParameterBuildingEntry createNull(SimpleTypeFieldNamesRegistry.Entry entry) { + static ParameterBuildingEntry createNull(SimpleTypeFieldNamesRegistry.Entry entry) { return new ParameterBuildingEntry(null, -1, entry); } - public boolean isNull() { + boolean isNull() { return this.node == null; } } diff --git a/src/main/java/org/quiltmc/enigma_plugin/proposal/ConflictFixProposer.java b/src/main/java/org/quiltmc/enigma_plugin/proposal/ConflictFixProposer.java index 84e3e694..2b20fa92 100644 --- a/src/main/java/org/quiltmc/enigma_plugin/proposal/ConflictFixProposer.java +++ b/src/main/java/org/quiltmc/enigma_plugin/proposal/ConflictFixProposer.java @@ -28,6 +28,7 @@ import org.quiltmc.enigma_plugin.index.JarIndexer; import org.quiltmc.enigma_plugin.index.simple_type_single.SimpleTypeSingleIndex; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -86,14 +87,14 @@ private void fixParamConflicts(Map, EntryMapping> mappings, EntryRemapp private Optional getConflictingParam(Map, EntryMapping> mappings, EntryRemapper remapper, LocalVariableEntry entry, @Nullable String name) { MethodEntry method = entry.getParent(); if (method != null) { - var args = method.getParameterIterator(remapper.getJarIndex().getIndex(EntryIndex.class), remapper.getDeobfuscator()); + List args = method.getParameters(remapper.getJarIndex().getIndex(EntryIndex.class)); - while (args.hasNext()) { - LocalVariableEntry arg = args.next(); + for (final LocalVariableEntry arg : args) { // check newly proposed mappings for a name - String remappedName = arg.getName(); + String remappedName = remapper.deobfuscate(arg).getName(); if (mappings.containsKey(arg)) { - remappedName = mappings.get(arg) == null ? null : mappings.get(arg).targetName(); + final EntryMapping mapping = mappings.get(arg); + remappedName = mapping == null ? null : mapping.targetName(); } if (arg.getIndex() != entry.getIndex() && name != null && name.equals(remappedName)) { @@ -106,6 +107,5 @@ private Optional getConflictingParam(Map, EntryMapp } @Override - public void insertProposedNames(Enigma enigma, JarIndex index, Map, EntryMapping> mappings) { - } + public void insertProposedNames(Enigma enigma, JarIndex index, Map, EntryMapping> mappings) { } } diff --git a/src/main/java/org/quiltmc/enigma_plugin/proposal/DefaultProposalService.java b/src/main/java/org/quiltmc/enigma_plugin/proposal/DefaultProposalService.java index 46ccc99a..31c177c4 100644 --- a/src/main/java/org/quiltmc/enigma_plugin/proposal/DefaultProposalService.java +++ b/src/main/java/org/quiltmc/enigma_plugin/proposal/DefaultProposalService.java @@ -21,6 +21,7 @@ import org.quiltmc.enigma_plugin.Arguments; import org.quiltmc.enigma_plugin.QuiltEnigmaPlugin; import org.quiltmc.enigma_plugin.index.JarIndexer; +import org.quiltmc.enigma_plugin.index.simple_type_single.SimpleSubtypeSingleIndex; import org.quiltmc.enigma_plugin.index.simple_type_single.SimpleTypeSingleIndex; public class DefaultProposalService extends NameProposerService { @@ -36,6 +37,10 @@ public DefaultProposalService(JarIndexer indexer, EnigmaServiceContext, EntryMapping> mappings) { } + + @Override + public void proposeDynamicNames( + EntryRemapper remapper, Entry obfEntry, + EntryMapping oldMapping, EntryMapping newMapping, Map, EntryMapping> mappings + ) { + if (obfEntry == null) { + this.index.forEachField((type, field, info) -> this.proposeField(remapper, mappings, type, field, info)); + this.index.forEachParam((type, param, entry) -> this.proposeParam(remapper, mappings, type, param, entry)); + } else if (obfEntry instanceof ClassEntry type) { + this.index.forEachFieldOfType(type, (field, info) -> this.proposeField(remapper, mappings, type, field, info)); + this.index.forEachParamOfType(type, (param, entry) -> this.proposeParam(remapper, mappings, type, param, entry)); + } + } + + private void proposeField( + EntryRemapper remapper, Map, EntryMapping> mappings, + ClassEntry type, FieldEntry field, FieldInfo info + ) { + if (!this.hasJarProposal(remapper, field)) { + if (remapper.getMapping(type).targetName() != null) { + getSimpleTargetName(remapper, type) + .flatMap(info.entry().renamer()::rename) + .map(name -> info.isConstant() ? toScreamingSnakeCase(name) : StringUtil.unCapitalize(name)) + .filter(StringUtil::isValidJavaIdentifier) + .ifPresent(name -> this.insertDynamicProposal(mappings, field, name)); + } + } + } + + private void proposeParam( + EntryRemapper remapper, Map, EntryMapping> mappings, + ClassEntry type, LocalVariableEntry param, SubtypeEntry entry + ) { + if (!this.hasJarProposal(remapper, param)) { + getSimpleTargetName(remapper, type) + .flatMap(entry.renamer()::rename) + .map(StringUtil::unCapitalize) + .filter(StringUtil::isValidJavaIdentifier) + .ifPresent(name -> this.insertDynamicProposal(mappings, param, name)); + } + } + + private static Optional getSimpleTargetName(EntryRemapper remapper, ClassEntry type) { + if (remapper.getMapping(type).targetName() != null) { + return Optional.of(remapper.deobfuscate(type).getSimpleName()); + } else { + return Optional.empty(); + } + } +} diff --git a/src/main/java/org/quiltmc/enigma_plugin/util/AsmUtil.java b/src/main/java/org/quiltmc/enigma_plugin/util/AsmUtil.java index 031a61d0..b6c2b845 100644 --- a/src/main/java/org/quiltmc/enigma_plugin/util/AsmUtil.java +++ b/src/main/java/org/quiltmc/enigma_plugin/util/AsmUtil.java @@ -56,26 +56,30 @@ public static int getLocalIndex(boolean isStatic, String desc, int local) { return -1; } - public static boolean maskMatch(int value, int... masks) { - boolean matched = true; - + public static boolean masksMatch(int value, int... masks) { for (int mask : masks) { - matched &= (value & mask) != 0; + if ((value & mask) == 0) { + return false; + } } - return matched; + return true; } public static boolean matchAccess(FieldNode node, int... masks) { - return maskMatch(node.access, masks); + return masksMatch(node.access, masks); } public static boolean matchAccess(MethodNode node, int... masks) { - return maskMatch(node.access, masks); + return masksMatch(node.access, masks); } public static boolean matchAccess(ParameterNode node, int... masks) { - return maskMatch(node.access, masks); + return masksMatch(node.access, masks); + } + + public static boolean matchAccess(ClassNode node, int... masks) { + return masksMatch(node.access, masks); } public static Optional getField(ClassNode node, String name, String desc) { diff --git a/src/main/java/org/quiltmc/enigma_plugin/util/StringUtil.java b/src/main/java/org/quiltmc/enigma_plugin/util/StringUtil.java new file mode 100644 index 00000000..040d456b --- /dev/null +++ b/src/main/java/org/quiltmc/enigma_plugin/util/StringUtil.java @@ -0,0 +1,84 @@ +/* + * Copyright 2025 QuiltMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.enigma_plugin.util; + +import org.jetbrains.annotations.Nullable; + +import java.util.Set; + +public final class StringUtil { + private StringUtil() { + throw new UnsupportedOperationException(); + } + + private static final Set ILLEGAL_IDENTIFIERS = Set.of( + "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", + "continue", "default", "do", "double", "else", "enum", "extends", "false", "final", "finally", + "float", "for", "goto", "if", "implements", "import", "instanceof", "int", "interface", "long", + "native", "new", "null", "package", "private", "protected", "public", "return", "short", "static", + "strictfp", "super", "switch", "synchronized", "this", "throw", "throws", "transient", "true", "try", + "void", "volatile", "while", "_" + ); + + @Nullable + public static String getObjectTypeOrNull(String descriptor) { + if (descriptor.length() <= 3 || descriptor.charAt(0) != 'L') { + return null; + } else { + return descriptor.substring(1, descriptor.length() - 1); + } + } + + public static boolean isValidJavaIdentifier(String id) { + if (id.isEmpty() || ILLEGAL_IDENTIFIERS.contains(id) || !Character.isJavaIdentifierStart(id.charAt(0))) { + return false; + } + + for (int i = 1; i < id.length(); i++) { + if (!Character.isJavaIdentifierPart(id.charAt(i))) { + return false; + } + } + + return true; + } + + /** + * Un-capitalized the first character of the passed {@code string}. + */ + public static String unCapitalize(String string) { + if (string.isEmpty()) { + return string; + } + + final char first = string.charAt(0); + final char firstLower = Character.toLowerCase(first); + + if (first == firstLower) { + return string; + } else { + final var builder = new StringBuilder(); + builder.append(firstLower); + + for (int i = 1; i < string.length(); i++) { + builder.append(string.charAt(i)); + } + + return builder.toString(); + } + } +} diff --git a/src/main/resources/simple_type_field_names_schema.json b/src/main/resources/simple_type_field_names_schema.json index 9cab6caa..20b2bfbc 100644 --- a/src/main/resources/simple_type_field_names_schema.json +++ b/src/main/resources/simple_type_field_names_schema.json @@ -39,6 +39,45 @@ "type": "boolean", "description": "Whether the default name should be used if there's only a single entry using that name. Only applies if there is any fallback name" }, + "inherit": { + "type": "object", + "description": "How to handle subtype naming.", + "properties": { + "type": { + "type": "string", + "description": "The kind of subtype name handling to perform. Must be the first property.", + "anyOf": [ + { "const": "NONE", "description": "Do not name subtypes." }, + { "const": "DIRECT", "description": "Subtypes inherit this simple type name." }, + { "const": "TRUNCATED_SUBTYPE_NAME", "description": "Use a suffix to identify and truncate the names of subtypes." }, + { "const": "TRANSFORMED_SUBTYPE_NAME", "description": "Use a regex pattern and a replacement string to identify and transform the names of subtypes." } + ] + }, + "suffix": { + "type": "string", + "description": "Used by TRUNCATED_SUBTYPE_NAME to recognize and truncate subtypes.", + "minLength": 1, + "pattern": "[_$a-zA-Z0-9]+" + }, + "pattern": { + "type": "string", + "description": "Regex used by TRANSFORMED_SUBTYPE_NAME to match subtypes.", + "minLength": 1 + }, + "replacement": { + "type": "string", + "description": "Replacement string used by TRANSFORMED_SUBTYPE_NAME to generate a name when \"pattern\" matches a subtype. May reference capturing groups defined in \"pattern\".", + "minLength": 1 + } + }, + "required": [ "type" ], + "if": { "properties": { "type": { "const": "TRUNCATED_SUBTYPE_NAME" } } }, + "then": { "required": [ "suffix" ] }, + "else": { + "if": { "properties": { "type": { "const": "TRANSFORMED_SUBTYPE_NAME" } } }, + "then": { "required": [ "pattern", "replacement" ] } + } + }, "fallback": { "type": "array", "description": "Fallback names to use in case an entry with the default name already exists", diff --git a/src/test/java/org/quiltmc/enigma_plugin/proposal/ConflictFixProposerTest.java b/src/test/java/org/quiltmc/enigma_plugin/proposal/ConflictFixProposerTest.java index 02a49c29..638be66b 100644 --- a/src/test/java/org/quiltmc/enigma_plugin/proposal/ConflictFixProposerTest.java +++ b/src/test/java/org/quiltmc/enigma_plugin/proposal/ConflictFixProposerTest.java @@ -22,6 +22,7 @@ import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; import org.quiltmc.enigma.api.translation.representation.entry.LocalVariableEntry; import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; +import org.quiltmc.enigma.util.validation.ValidationContext; import org.quiltmc.enigma_plugin.test.util.CommonDescriptors; import org.quiltmc.enigma_plugin.test.util.ProposalAsserter; @@ -46,6 +47,7 @@ public String getTargetId() { return ConflictFixProposer.ID; } + // tests fixing conflict between ConstructorParamsNameProposer and SimpleTypeFieldNameProposer with simple type fallback @Test public void testSimpleTypeNameConflictFix() { final var asserter = this.createAsserter(); @@ -55,9 +57,9 @@ public void testSimpleTypeNameConflictFix() { final var conflictTest = new ClassEntry(CONFLICT_TEST_NAME); final MethodEntry constructor = methodOf(conflictTest, "", V, I, IDENTIFIABLE); - final String simpleName = "idAble"; final LocalVariableEntry param2 = localOf(constructor, 2); + final String simpleName = "idAble"; // param 2 is initially 'idAble' asserter.assertProposal(simpleName, param2); @@ -73,6 +75,7 @@ public void testSimpleTypeNameConflictFix() { asserter.assertDynamicProposal("identifiable", param2); } + // tests fixing conflict between ConstructorParamsNameProposer and SimpleTypeFieldNameProposer with no simple type fallback @Test public void testSimpleTypeNameConflictFixNoFallback() { final ProposalAsserter asserter = this.createAsserter(); @@ -80,9 +83,9 @@ public void testSimpleTypeNameConflictFixNoFallback() { final var conflictTest = new ClassEntry(CONFLICT_TEST_NAME); final MethodEntry constructor = methodOf(conflictTest, "", V, I, IDENTIFIED); - final String simpleName = "identified"; final LocalVariableEntry param2 = localOf(constructor, 2); + final String simpleName = "identified"; // param 2 is initially 'identified' asserter.assertProposal(simpleName, param2); @@ -95,6 +98,38 @@ public void testSimpleTypeNameConflictFixNoFallback() { asserter.remapper().insertDynamicallyProposedMappings(backingField, EntryMapping.OBFUSCATED, mapping); asserter.assertDynamicProposal(simpleName, localOf(constructor, 1)); + // conflicting name removed by conflict fix asserter.assertNotProposed(param2); } + + // tests fixing conflict between DelegateParametersProposer and SimpleTypeFieldNameProposer (with no simple type fallback) + // conflict is amongst params of a named method + // this tests deobf entries are not used for mapping lookups + @Test + public void testSimpleTypeNameConflictFixDeobfMethod() { + final var asserter = this.createAsserter(); + + final var conflictTest = new ClassEntry(CONFLICT_TEST_NAME); + final MethodEntry delegateParamsSource = methodOf(conflictTest, "a", V, I, IDENTIFIED); + final MethodEntry delegateParams = methodOf(conflictTest, "b", V, I, IDENTIFIED); + + final LocalVariableEntry delegateParam2 = localOf(delegateParams, 1); + + final String simpleName = "identified"; + // param 2 is initially 'identified' + asserter.assertProposal(simpleName, delegateParam2); + + final var context = new ValidationContext(null); + // name the method with delegate params to make sure conflict fixing works for deobf methods + asserter.remapper().putMapping(context, delegateParams, new EntryMapping("delegateParams")); + // override simple type name for the second param source so the first param can receive the simple type name + asserter.remapper().putMapping(context, localOf(delegateParamsSource, 1), new EntryMapping("noConflict")); + // name the first param source the simple type name, creating a conflict in the delegate params + asserter.remapper().putMapping(context, localOf(delegateParamsSource, 0), new EntryMapping(simpleName)); + + // first delegate param proposed by DelegateParametersNameProposer + asserter.assertDynamicProposal(simpleName, localOf(delegateParams, 0)); + // conflicting name removed by conflict fix + asserter.assertNotProposed(delegateParam2); + } } diff --git a/src/test/java/org/quiltmc/enigma_plugin/proposal/ConventionalNameProposerTest.java b/src/test/java/org/quiltmc/enigma_plugin/proposal/ConventionalNameProposerTest.java index dc403b57..6458b215 100644 --- a/src/test/java/org/quiltmc/enigma_plugin/proposal/ConventionalNameProposerTest.java +++ b/src/test/java/org/quiltmc/enigma_plugin/proposal/ConventionalNameProposerTest.java @@ -18,6 +18,7 @@ import org.quiltmc.enigma_plugin.test.util.ProposalAsserter; import org.quiltmc.enigma_plugin.test.util.TestUtil; +import org.quiltmc.enigma_plugin.util.StringUtil; import java.nio.file.Files; import java.nio.file.Path; @@ -73,6 +74,6 @@ default ProposalAsserter createAsserter() { } private String getUnCapitalizedTarget() { - return TestUtil.unCapitalize(this.getTarget().getSimpleName()); + return StringUtil.unCapitalize(this.getTarget().getSimpleName()); } } diff --git a/src/test/java/org/quiltmc/enigma_plugin/proposal/SimpleSubtypeFieldNameProposerTest.java b/src/test/java/org/quiltmc/enigma_plugin/proposal/SimpleSubtypeFieldNameProposerTest.java new file mode 100644 index 00000000..f81d97cd --- /dev/null +++ b/src/test/java/org/quiltmc/enigma_plugin/proposal/SimpleSubtypeFieldNameProposerTest.java @@ -0,0 +1,273 @@ +/* + * Copyright 2025 QuiltMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.enigma_plugin.proposal; + +import org.junit.jupiter.api.Test; +import org.quiltmc.enigma.api.translation.mapping.EntryMapping; +import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; +import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; +import org.quiltmc.enigma.util.validation.ValidationContext; +import org.quiltmc.enigma_plugin.test.util.CommonDescriptors; +import org.quiltmc.enigma_plugin.test.util.ProposalAsserter; + +import static org.quiltmc.enigma_plugin.test.util.TestUtil.fieldOf; +import static org.quiltmc.enigma_plugin.test.util.TestUtil.localOf; +import static org.quiltmc.enigma_plugin.test.util.TestUtil.methodOf; +import static org.quiltmc.enigma_plugin.test.util.TestUtil.typeDescOf; + +public class SimpleSubtypeFieldNameProposerTest implements ConventionalNameProposerTest, CommonDescriptors { + private static final String SIMPLE_SUBTYPE_NAMES_TEST_NAME = "a/a/a"; + + private static final String ENTITY_NAME = "a/a/b/b"; + private static final String ENTITY_NON_SUFFIXED_NAME = "a/a/b/c"; + private static final String LIVING_ENTITY_NAME = "a/a/b/d"; + private static final String BIRD_ENTITY_NAME = "a/a/b/a"; + private static final String WHILE_ENTITY_NAME = "a/a/b/e"; + private static final String SPECIFIC_ENTITY_NAME = "a/a/b/a/c"; + private static final String ENTITY_MARKER_NAME = "a/a/b/a/a"; + private static final String ENTITY_SPECIFIC_ENTITY_NAME = "a/a/b/a/b"; + + private static final String BLOCK_ENTITY_RENDERER_NAME = "a/a/a/a"; + private static final String DISPLAY_BLOCK_ENTITY_RENDERER_NAME = "a/a/a/b"; + private static final String PAINTING_BLOCK_ENTITY_RENDERER_NAME = "a/a/a/d"; + private static final String DUPLICATE_BLOCK_ENTITY_RENDERER_NAME = "a/a/a/c"; + + private static final String ENTITY = typeDescOf(ENTITY_NAME); + private static final String ENTITY_NON_SUFFIXED = typeDescOf(ENTITY_NON_SUFFIXED_NAME); + private static final String LIVING_ENTITY = typeDescOf(LIVING_ENTITY_NAME); + private static final String BIRD_ENTITY = typeDescOf(BIRD_ENTITY_NAME); + private static final String WHILE_ENTITY = typeDescOf(WHILE_ENTITY_NAME); + private static final String SPECIFIC_ENTITY = typeDescOf(SPECIFIC_ENTITY_NAME); + private static final String ENTITY_MARKER = typeDescOf(ENTITY_MARKER_NAME); + private static final String ENTITY_SPECIFIC_ENTITY = typeDescOf(ENTITY_SPECIFIC_ENTITY_NAME); + + private static final String BLOCK_ENTITY_RENDERER = typeDescOf(BLOCK_ENTITY_RENDERER_NAME); + private static final String DISPLAY_BLOCK_ENTITY_RENDERER = typeDescOf(DISPLAY_BLOCK_ENTITY_RENDERER_NAME); + private static final String PAINTING_BLOCK_ENTITY_RENDERER = typeDescOf(PAINTING_BLOCK_ENTITY_RENDERER_NAME); + private static final String DUPLICATE_BLOCK_ENTITY_RENDERER = typeDescOf(DUPLICATE_BLOCK_ENTITY_RENDERER_NAME); + + @Override + public Class getTarget() { + return SimpleSubtypeFieldNameProposer.class; + } + + @Override + public String getTargetId() { + return SimpleSubtypeFieldNameProposer.ID; + } + + // make sure simple type name is set up for Entity + @Test + void truncatedSubtypeSupertype() { + final ProposalAsserter asserter = this.createAsserter(); + + final var testClass = new ClassEntry(SIMPLE_SUBTYPE_NAMES_TEST_NAME); + + // simple type names, not dynamic subtype names + asserter.assertProposal("ENTITY", fieldOf(testClass, "a", ENTITY)); + asserter.assertProposal("entity", fieldOf(testClass, "k", ENTITY)); + // eatStatic + asserter.assertProposal("entity", localOf(methodOf(testClass, "a", V, ENTITY), 0)); + // eat + asserter.assertProposal("entity", localOf(methodOf(testClass, "b", V, ENTITY), 1)); + } + + @Test + void testTruncatedSubtypeNames() { + final ProposalAsserter asserter = this.createAsserter(); + + final var testClass = new ClassEntry(SIMPLE_SUBTYPE_NAMES_TEST_NAME); + + final var context = new ValidationContext(null); + + asserter.remapper().putMapping( + context, new ClassEntry(ENTITY_NON_SUFFIXED_NAME), + new EntryMapping("EntityNonSuffixed") + ); + + // NON_SUFFIXED + asserter.assertNotProposed(fieldOf(testClass, "d", ENTITY_NON_SUFFIXED)); + // nonSuffixed + asserter.assertNotProposed(fieldOf(testClass, "n", ENTITY_NON_SUFFIXED)); + // eatNonSuffixedStatic + asserter.assertNotProposed(localOf(methodOf(testClass, "a", OBJ, ENTITY_NON_SUFFIXED, OBJ), 0)); + // eatNonSuffixed + asserter.assertNotProposed(localOf(methodOf(testClass, "b", OBJ, ENTITY_NON_SUFFIXED, OBJ), 1)); + + asserter.remapper().putMapping(context, new ClassEntry(LIVING_ENTITY_NAME), new EntryMapping("LivingEntity")); + // don't propose names for non-concrete classes + // LIVING_ENTITY + asserter.assertNotProposed(fieldOf(testClass, "b", LIVING_ENTITY)); + // livingEntity + asserter.assertNotProposed(fieldOf(testClass, "l", LIVING_ENTITY)); + // eatLivingStatic + asserter.assertNotProposed(localOf(methodOf(testClass, "a", Z, LIVING_ENTITY), 0)); + // eatLiving + asserter.assertNotProposed(localOf(methodOf(testClass, "b", Z, LIVING_ENTITY), 1)); + + asserter.remapper().putMapping( + context, new ClassEntry(BIRD_ENTITY_NAME), + new EntryMapping("com/example/BirdEntity") + ); + + asserter.assertDynamicProposal("BIRD", fieldOf(testClass, "c", BIRD_ENTITY)); + asserter.assertDynamicProposal("bird", fieldOf(testClass, "m", BIRD_ENTITY)); + // eatBirdStatic + asserter.assertDynamicProposal("bird", localOf(methodOf(testClass, "a", I, I, BIRD_ENTITY), 1)); + // eatBird + asserter.assertDynamicProposal("bird", localOf(methodOf(testClass, "b", I, I, BIRD_ENTITY), 2)); + } + + @Test + void testNoInvalidSubtypeNames() { + final ProposalAsserter asserter = this.createAsserter(); + + final var testClass = new ClassEntry(SIMPLE_SUBTYPE_NAMES_TEST_NAME); + + final var context = new ValidationContext(null); + + asserter.remapper().putMapping( + context, new ClassEntry(WHILE_ENTITY_NAME), + new EntryMapping("com/example/WhileEntity") + ); + + // "WHILE" proposed despite its lowercase form being invalid + asserter.assertDynamicProposal("WHILE", fieldOf(testClass, "j", WHILE_ENTITY)); + // "while" not proposed for notWhile field or param because it's a keyword + asserter.assertNotProposed(fieldOf(testClass, "t", WHILE_ENTITY)); + asserter.assertNotProposed(methodOf(testClass, "a", V, WHILE_ENTITY)); + } + + // make sure simple type name is set up for BlockEntityRenderer + @Test + void transformedSubtypeSupertype() { + final ProposalAsserter asserter = this.createAsserter(); + + final var testClass = new ClassEntry(SIMPLE_SUBTYPE_NAMES_TEST_NAME); + + // simple type names, not dynamic subtype names + asserter.assertProposal("BLOCK_ENTITY_RENDERER", fieldOf(testClass, "e", BLOCK_ENTITY_RENDERER)); + asserter.assertProposal("blockEntityRenderer", fieldOf(testClass, "o", BLOCK_ENTITY_RENDERER)); + // renderStatic + asserter.assertProposal("blockEntityRenderer", localOf(methodOf(testClass, "a", V, BLOCK_ENTITY_RENDERER), 0)); + // render + asserter.assertProposal("blockEntityRenderer", localOf(methodOf(testClass, "b", V, BLOCK_ENTITY_RENDERER), 1)); + } + + @Test + void testTransformedSubtypeNames() { + final ProposalAsserter asserter = this.createAsserter(); + + final var testClass = new ClassEntry(SIMPLE_SUBTYPE_NAMES_TEST_NAME); + + final var context = new ValidationContext(null); + + asserter.remapper().putMapping( + context, new ClassEntry(DISPLAY_BLOCK_ENTITY_RENDERER_NAME), + new EntryMapping("DisplayBlockEntityRenderer") + ); + + asserter.assertDynamicProposal("DISPLAY_RENDERER", fieldOf(testClass, "f", DISPLAY_BLOCK_ENTITY_RENDERER)); + asserter.assertDynamicProposal("displayRenderer", fieldOf(testClass, "p", DISPLAY_BLOCK_ENTITY_RENDERER)); + // renderStatic + asserter.assertDynamicProposal("displayRenderer", localOf(methodOf(testClass, "a", V, DISPLAY_BLOCK_ENTITY_RENDERER), 0)); + // render + asserter.assertDynamicProposal("displayRenderer", localOf(methodOf(testClass, "b", V, DISPLAY_BLOCK_ENTITY_RENDERER), 1)); + + asserter.remapper().putMapping( + context, new ClassEntry(PAINTING_BLOCK_ENTITY_RENDERER_NAME), + new EntryMapping("PaintingBlockEntityRenderer") + ); + + asserter.assertDynamicProposal("PAINTING_RENDERER", fieldOf(testClass, "g", PAINTING_BLOCK_ENTITY_RENDERER)); + asserter.assertDynamicProposal("paintingRenderer", fieldOf(testClass, "q", PAINTING_BLOCK_ENTITY_RENDERER)); + // renderStatic + asserter.assertDynamicProposal("paintingRenderer", localOf(methodOf(testClass, "a", V, PAINTING_BLOCK_ENTITY_RENDERER), 0)); + // render + asserter.assertDynamicProposal("paintingRenderer", localOf(methodOf(testClass, "b", V, PAINTING_BLOCK_ENTITY_RENDERER), 1)); + } + + @Test + void testNoDuplicates() { + final ProposalAsserter asserter = this.createAsserter(); + + final var testClass = new ClassEntry(SIMPLE_SUBTYPE_NAMES_TEST_NAME); + + final var context = new ValidationContext(null); + + asserter.remapper().putMapping( + context, new ClassEntry(DUPLICATE_BLOCK_ENTITY_RENDERER_NAME), + new EntryMapping("DuplicateBlockEntityRenderer") + ); + + // DUPLICATE_1 + asserter.assertNotProposed(fieldOf(testClass, "h", DUPLICATE_BLOCK_ENTITY_RENDERER)); + // DUPLICATE_2 + asserter.assertNotProposed(fieldOf(testClass, "i", DUPLICATE_BLOCK_ENTITY_RENDERER)); + // duplicate1 + asserter.assertNotProposed(fieldOf(testClass, "r", DUPLICATE_BLOCK_ENTITY_RENDERER)); + // duplicate2 + asserter.assertNotProposed(fieldOf(testClass, "s", DUPLICATE_BLOCK_ENTITY_RENDERER)); + + final MethodEntry renderStaticDuplicateMethod = methodOf( + testClass, "a", V, + DUPLICATE_BLOCK_ENTITY_RENDERER, DUPLICATE_BLOCK_ENTITY_RENDERER + ); + asserter.assertNotProposed(localOf(renderStaticDuplicateMethod, 0)); + asserter.assertNotProposed(localOf(renderStaticDuplicateMethod, 1)); + + final MethodEntry renderDuplicateMethod = methodOf( + testClass, "b", V, + DUPLICATE_BLOCK_ENTITY_RENDERER, DUPLICATE_BLOCK_ENTITY_RENDERER + ); + asserter.assertNotProposed(localOf(renderDuplicateMethod, 1)); + asserter.assertNotProposed(localOf(renderDuplicateMethod, 2)); + } + + @Test + void testSubSubtypeNames() { + final ProposalAsserter asserter = this.createAsserter(); + + final var testClass = new ClassEntry(SIMPLE_SUBTYPE_NAMES_TEST_NAME); + + final var context = new ValidationContext(null); + asserter.remapper().putMapping( + context, new ClassEntry(SPECIFIC_ENTITY_NAME), + new EntryMapping("SpecificEntity") + ); + + asserter.remapper().putMapping( + context, new ClassEntry(ENTITY_MARKER_NAME), + new EntryMapping("EntityMarker") + ); + + asserter.remapper().putMapping( + context, new ClassEntry(ENTITY_SPECIFIC_ENTITY_NAME), + new EntryMapping("EntitySpecificEntity") + ); + + // method: eatSpecificEntity + // named simple type name despite being a matching subtype of Entity + // non-sub-types take priority + asserter.assertProposal("specificEntity", localOf(methodOf(testClass, "a", V, SPECIFIC_ENTITY), 1)); + // method: eatMarker + asserter.assertDynamicProposal("marker", localOf(methodOf(testClass, "a", V, ENTITY_MARKER), 1)); + // method: eatEntitySpecificEntity + // named by SpecificEntity's subtype pattern, not Entity's, because SpecificEntity is a closer supertype + asserter.assertDynamicProposal("specificEntity", localOf(methodOf(testClass, "a", V, ENTITY_SPECIFIC_ENTITY), 1)); + } +} diff --git a/src/test/java/org/quiltmc/enigma_plugin/proposal/SimpleTypeFieldNameProposerTest.java b/src/test/java/org/quiltmc/enigma_plugin/proposal/SimpleTypeFieldNameProposerTest.java index 7900575c..5224e57d 100644 --- a/src/test/java/org/quiltmc/enigma_plugin/proposal/SimpleTypeFieldNameProposerTest.java +++ b/src/test/java/org/quiltmc/enigma_plugin/proposal/SimpleTypeFieldNameProposerTest.java @@ -16,16 +16,12 @@ package org.quiltmc.enigma_plugin.proposal; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; import org.quiltmc.enigma_plugin.test.util.CommonDescriptors; import org.quiltmc.enigma_plugin.test.util.ProposalAsserter; -import java.io.ByteArrayOutputStream; -import java.io.PrintStream; - import static org.quiltmc.enigma_plugin.test.util.TestUtil.fieldOf; import static org.quiltmc.enigma_plugin.test.util.TestUtil.localOf; import static org.quiltmc.enigma_plugin.test.util.TestUtil.methodOf; @@ -46,8 +42,10 @@ public class SimpleTypeFieldNameProposerTest implements ConventionalNameProposer private static final String VALUE_C = typeDescOf("a/a/a/i"); private static final String VALUE_D = typeDescOf(VALUE_D_NAME); private static final String VALUE_DD = typeDescOf("a/a/a/k"); - private static final String VALUE_E = typeDescOf("a/a/a/l"); - private static final String VALUE_EE = typeDescOf("a/a/a/m"); + private static final String VALUE_E = typeDescOf("a/a/a/n"); + private static final String VALUE_EE = typeDescOf("a/a/a/o"); + private static final String VALUE_D_SPECIFIC = typeDescOf("a/a/a/l"); + private static final String VALUE_D_SPECIFIC_INHERITOR = typeDescOf("a/a/a/m"); @Override public Class getTarget() { @@ -74,12 +72,12 @@ public void testSimpleTypeSingleNames() { final var fallbacks = new ClassEntry(fields, "b"); asserter.assertProposal("POS", fieldOf(fallbacks, "a", POS)); - asserter.assertProposal("position", fieldOf(fallbacks, "b", POSITION)); - asserter.assertProposal("randomPosition", fieldOf(fallbacks, "c", RANDOM_POSITION)); + asserter.assertProposal("POSITION", fieldOf(fallbacks, "b", POSITION)); + asserter.assertProposal("RANDOM_POSITION", fieldOf(fallbacks, "c", RANDOM_POSITION)); asserter.assertProposal("STATIC_STATE_A", fieldOf(fallbacks, "d", STATE_A)); asserter.assertProposal("STATIC_STATE_B", fieldOf(fallbacks, "e", STATE_B)); - asserter.assertProposal("VALUE_A", fieldOf(fallbacks, "f", VALUE_A)); - asserter.assertProposal("VALUE_B", fieldOf(fallbacks, "g", VALUE_B)); + asserter.assertProposal("valueA", fieldOf(fallbacks, "f", VALUE_A)); + asserter.assertProposal("valueB", fieldOf(fallbacks, "g", VALUE_B)); asserter.assertProposal("valueC", fieldOf(fallbacks, "h", VALUE_C)); final var inheritance = new ClassEntry(fields, "c"); @@ -155,19 +153,17 @@ public void testSimpleTypeSingleNames() { } @Test - void testTypeVerification() { - PrintStream originalErr = System.err; + void testInheritanceOverride() { + final ProposalAsserter asserter = this.createAsserter(); - try { - ByteArrayOutputStream testErr = new ByteArrayOutputStream(); - System.setErr(new PrintStream(testErr)); + final var testClass = new ClassEntry(SIMPLE_TYPES_NAMES_TEST_NAME); - this.createAsserter(); + final var parameters = new ClassEntry(testClass, "b"); - // simple_type_field_names_type_verification warning - Assertions.assertTrue(testErr.toString().contains("[WARN]: The following simple type field name type is missing: not/present")); - } finally { - System.setErr(originalErr); - } + final String specific = "specific"; + // should have ValueDSpecific's type name, not inherited ValueD type name + // valueSpecific + asserter.assertProposal(specific, localOf(methodOf(parameters, "a", V, VALUE_D_SPECIFIC), 0)); + asserter.assertProposal(specific, localOf(methodOf(parameters, "a", V, VALUE_D_SPECIFIC_INHERITOR), 0)); } } diff --git a/src/test/java/org/quiltmc/enigma_plugin/proposal/SimpleTypeVerificationTest.java b/src/test/java/org/quiltmc/enigma_plugin/proposal/SimpleTypeVerificationTest.java new file mode 100644 index 00000000..1387b168 --- /dev/null +++ b/src/test/java/org/quiltmc/enigma_plugin/proposal/SimpleTypeVerificationTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2025 QuiltMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.enigma_plugin.proposal; + +import org.junit.jupiter.api.Test; +import org.quiltmc.enigma_plugin.test.util.TestUtil; + +import java.nio.file.Path; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; +import static org.quiltmc.enigma_plugin.proposal.ConventionalNameProposerTest.PROFILE_JSON; + +public class SimpleTypeVerificationTest { + private static final String SOURCE_SET_NAME = "simpleTypeVerification"; + private static final Path OBF_JAR = TestUtil.obfJarPathOf(SOURCE_SET_NAME); + private static final Path PROFILE = TestUtil.BUILD_RESOURCES.resolve(SOURCE_SET_NAME + "/" + PROFILE_JSON); + + @Test + void test() { + final IllegalStateException thrown = assertThrowsExactly( + IllegalStateException.class, + () -> TestUtil.setupEnigma(OBF_JAR, PROFILE) + ); + + assertThat(thrown.getMessage(), is("The following simple type field name type is missing: not/present")); + } +} diff --git a/src/test/java/org/quiltmc/enigma_plugin/test/util/TestUtil.java b/src/test/java/org/quiltmc/enigma_plugin/test/util/TestUtil.java index 73ea37a3..94f842f3 100644 --- a/src/test/java/org/quiltmc/enigma_plugin/test/util/TestUtil.java +++ b/src/test/java/org/quiltmc/enigma_plugin/test/util/TestUtil.java @@ -17,6 +17,7 @@ package org.quiltmc.enigma_plugin.test.util; import org.junit.jupiter.api.Assertions; +import org.opentest4j.AssertionFailedError; import org.quiltmc.enigma.api.Enigma; import org.quiltmc.enigma.api.EnigmaProfile; import org.quiltmc.enigma.api.EnigmaProject; @@ -38,6 +39,7 @@ import java.io.IOException; import java.nio.file.Path; +import java.util.Objects; public final class TestUtil { private static final String TEST_OBF_SUFFIX = "-test-obf"; @@ -62,28 +64,6 @@ public static Path obfJarPathOf(String namePrefix) { return BUILD.resolve(("test-obf/%s" + TEST_OBF_SUFFIX + ".jar").formatted(namePrefix)); } - public static String unCapitalize(String string) { - if (string.isEmpty()) { - return string; - } - - final char first = string.charAt(0); - final char firstLower = Character.toLowerCase(first); - - if (first == firstLower) { - return string; - } else { - final var builder = new StringBuilder(); - builder.append(firstLower); - - for (int i = 1; i < string.length(); i++) { - builder.append(string.charAt(i)); - } - - return builder.toString(); - } - } - public static EntryRemapper setupEnigma(Path jar, Path profile) { final EnigmaProfile enigmaProfile; try { @@ -168,4 +148,17 @@ public static String typeDescOf(String typeName) { public static String javaLangDescOf(String simpleName) { return typeDescOf("java/lang/" + simpleName); } + + public static T requireNonNull(T t, String name) { + return Objects.requireNonNull(t, name + " must not be null!"); + } + + public static void assertContains(String string, String substring) { + if (!requireNonNull(string, "string").contains(requireNonNull(substring, "substring"))) { + throw new AssertionFailedError( + "String does not contain substring \"%s\":%n\t%s" + .formatted(substring, string) + ); + } + } } diff --git a/src/testInputs/conflictFixProposer/java/com/example/ConflictTest.java b/src/testInputs/conflictFixProposer/java/com/example/ConflictTest.java index 90837b7a..290dc8d3 100644 --- a/src/testInputs/conflictFixProposer/java/com/example/ConflictTest.java +++ b/src/testInputs/conflictFixProposer/java/com/example/ConflictTest.java @@ -1,6 +1,12 @@ package com.example; public class ConflictTest { + public static void delegateParamsSource(int integer, Identified identified) { } + + public static void delegateParams(int integer, Identified identified) { + delegateParamsSource(integer, identified); + } + public final int integer; public final Identifiable idAble; diff --git a/src/testInputs/conflictFixProposer/resources/simple_type_field_names.json5 b/src/testInputs/conflictFixProposer/resources/simple_type_field_names.json5 index d5cd39c8..e5a84944 100644 --- a/src/testInputs/conflictFixProposer/resources/simple_type_field_names.json5 +++ b/src/testInputs/conflictFixProposer/resources/simple_type_field_names.json5 @@ -1,5 +1,5 @@ { - $schema: "../../src/main/resources/simple_type_field_names_schema.json", + $schema: "../../../../src/main/resources/simple_type_field_names_schema.json", // for conflict fix test "a/a/b": { // Identifiable diff --git a/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/SimpleSubtypeNamesTest.java b/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/SimpleSubtypeNamesTest.java new file mode 100644 index 00000000..709803aa --- /dev/null +++ b/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/SimpleSubtypeNamesTest.java @@ -0,0 +1,102 @@ +package com.example; + +import com.example.block_entity_renderer.BlockEntityRenderer; +import com.example.block_entity_renderer.DisplayBlockEntityRenderer; +import com.example.block_entity_renderer.DuplicateBlockEntityRenderer; +import com.example.block_entity_renderer.PaintingBlockEntityRenderer; +import com.example.entity.BirdEntity; +import com.example.entity.Entity; +import com.example.entity.EntityNonSuffixed; +import com.example.entity.LivingEntity; +import com.example.entity.WhileEntity; +import com.example.entity.specific.EntityMarker; +import com.example.entity.specific.EntitySpecificEntity; +import com.example.entity.specific.SpecificEntity; + +public class SimpleSubtypeNamesTest { + static final Entity ENTITY = new Entity(); + static final LivingEntity LIVING_ENTITY = null; + static final BirdEntity BIRD = new BirdEntity(); + static final EntityNonSuffixed NON_SUFFIXED = new EntityNonSuffixed(); + + static final BlockEntityRenderer BLOCK_ENTITY_RENDERER = new BlockEntityRenderer(); + static final DisplayBlockEntityRenderer DISPLAY_RENDERER = new DisplayBlockEntityRenderer(); + static final PaintingBlockEntityRenderer PAINTING_RENDERER = new PaintingBlockEntityRenderer(); + + static final DuplicateBlockEntityRenderer DUPLICATE_1 = new DuplicateBlockEntityRenderer(); + static final DuplicateBlockEntityRenderer DUPLICATE_2 = new DuplicateBlockEntityRenderer(); + + // validly named despite its lowercase name being invalid + static final WhileEntity WHILE = new WhileEntity(); + + static void eatStatic(Entity entity) { } + + static boolean eatLivingStatic(LivingEntity living) { + return true; + } + + static int eatBirdStatic(int count, BirdEntity bird) { + return count; + } + + static Object eatNonSuffixedStatic(EntityNonSuffixed nonSuffixed, Object arg) { + return arg; + } + + static void renderStatic(BlockEntityRenderer blockEntityRenderer) { } + + static void renderStatic(DisplayBlockEntityRenderer displayRenderer) { } + + static void renderStatic(PaintingBlockEntityRenderer paintingRenderer) { } + + static void renderStatic(DuplicateBlockEntityRenderer duplicate1, DuplicateBlockEntityRenderer duplicate2) { } + + Entity entity = new Entity(); + LivingEntity livingEntity = null; + BirdEntity bird = new BirdEntity(); + EntityNonSuffixed nonSuffixed = new EntityNonSuffixed(); + + BlockEntityRenderer blockEntityRenderer = new BlockEntityRenderer(); + DisplayBlockEntityRenderer displayRenderer = new DisplayBlockEntityRenderer(); + PaintingBlockEntityRenderer paintingRenderer = new PaintingBlockEntityRenderer(); + + DuplicateBlockEntityRenderer duplicate1 = new DuplicateBlockEntityRenderer(); + DuplicateBlockEntityRenderer duplicate2 = new DuplicateBlockEntityRenderer(); + + // not invalidly named while keyword + WhileEntity notWhile = new WhileEntity(); + + void eat(Entity entity) { } + + boolean eatLiving(LivingEntity living) { + return true; + } + + int eatBird(int count, BirdEntity bird) { + return count; + } + + Object eatNonSuffixed(EntityNonSuffixed nonSuffixed, Object arg) { + return arg; + } + + // get simple non-sub-type name + void eatSpecificEntity(SpecificEntity specificEntity) { } + + // named by SpecificEntity subtype, not from Entity subtype + void eatMarker(EntityMarker marker) { } + + // named by SpecificEntity subtype despite have Entity subtype's suffix + void eatEntitySpecificEntity(EntitySpecificEntity specificEntity) { } + + // not invalidly named while keyword + void eatWhile(WhileEntity notWhile) { } + + void render(BlockEntityRenderer blockEntityRenderer) { } + + void render(DisplayBlockEntityRenderer displayRenderer) { } + + void render(PaintingBlockEntityRenderer paintingRenderer) { } + + void render(DuplicateBlockEntityRenderer duplicate1, DuplicateBlockEntityRenderer duplicate2) { } +} diff --git a/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/block_entity_renderer/BlockEntityRenderer.java b/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/block_entity_renderer/BlockEntityRenderer.java new file mode 100644 index 00000000..cc99177e --- /dev/null +++ b/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/block_entity_renderer/BlockEntityRenderer.java @@ -0,0 +1,3 @@ +package com.example.block_entity_renderer; + +public class BlockEntityRenderer { } diff --git a/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/block_entity_renderer/DisplayBlockEntityRenderer.java b/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/block_entity_renderer/DisplayBlockEntityRenderer.java new file mode 100644 index 00000000..66df6947 --- /dev/null +++ b/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/block_entity_renderer/DisplayBlockEntityRenderer.java @@ -0,0 +1,3 @@ +package com.example.block_entity_renderer; + +public class DisplayBlockEntityRenderer extends BlockEntityRenderer { } diff --git a/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/block_entity_renderer/DuplicateBlockEntityRenderer.java b/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/block_entity_renderer/DuplicateBlockEntityRenderer.java new file mode 100644 index 00000000..bfa65585 --- /dev/null +++ b/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/block_entity_renderer/DuplicateBlockEntityRenderer.java @@ -0,0 +1,3 @@ +package com.example.block_entity_renderer; + +public class DuplicateBlockEntityRenderer extends BlockEntityRenderer { } diff --git a/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/block_entity_renderer/PaintingBlockEntityRenderer.java b/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/block_entity_renderer/PaintingBlockEntityRenderer.java new file mode 100644 index 00000000..d630865f --- /dev/null +++ b/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/block_entity_renderer/PaintingBlockEntityRenderer.java @@ -0,0 +1,3 @@ +package com.example.block_entity_renderer; + +public class PaintingBlockEntityRenderer extends DisplayBlockEntityRenderer { } diff --git a/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/entity/BirdEntity.java b/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/entity/BirdEntity.java new file mode 100644 index 00000000..26e7bcda --- /dev/null +++ b/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/entity/BirdEntity.java @@ -0,0 +1,3 @@ +package com.example.entity; + +public class BirdEntity extends LivingEntity { } diff --git a/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/entity/Entity.java b/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/entity/Entity.java new file mode 100644 index 00000000..83ccb24a --- /dev/null +++ b/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/entity/Entity.java @@ -0,0 +1,3 @@ +package com.example.entity; + +public class Entity { } diff --git a/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/entity/EntityNonSuffixed.java b/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/entity/EntityNonSuffixed.java new file mode 100644 index 00000000..0aac6127 --- /dev/null +++ b/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/entity/EntityNonSuffixed.java @@ -0,0 +1,3 @@ +package com.example.entity; + +public class EntityNonSuffixed extends Entity { } diff --git a/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/entity/LivingEntity.java b/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/entity/LivingEntity.java new file mode 100644 index 00000000..dfe11486 --- /dev/null +++ b/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/entity/LivingEntity.java @@ -0,0 +1,3 @@ +package com.example.entity; + +public abstract class LivingEntity extends Entity { } diff --git a/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/entity/WhileEntity.java b/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/entity/WhileEntity.java new file mode 100644 index 00000000..1ad8499d --- /dev/null +++ b/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/entity/WhileEntity.java @@ -0,0 +1,3 @@ +package com.example.entity; + +public class WhileEntity extends Entity { } diff --git a/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/entity/specific/EntityMarker.java b/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/entity/specific/EntityMarker.java new file mode 100644 index 00000000..a7677c63 --- /dev/null +++ b/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/entity/specific/EntityMarker.java @@ -0,0 +1,3 @@ +package com.example.entity.specific; + +public class EntityMarker extends SpecificEntity { } diff --git a/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/entity/specific/EntitySpecificEntity.java b/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/entity/specific/EntitySpecificEntity.java new file mode 100644 index 00000000..ef842b8f --- /dev/null +++ b/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/entity/specific/EntitySpecificEntity.java @@ -0,0 +1,3 @@ +package com.example.entity.specific; + +public class EntitySpecificEntity extends SpecificEntity { } diff --git a/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/entity/specific/SpecificEntity.java b/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/entity/specific/SpecificEntity.java new file mode 100644 index 00000000..50014e64 --- /dev/null +++ b/src/testInputs/simpleSubtypeFieldNameProposer/java/com/example/entity/specific/SpecificEntity.java @@ -0,0 +1,5 @@ +package com.example.entity.specific; + +import com.example.entity.Entity; + +public class SpecificEntity extends Entity { } diff --git a/src/testInputs/simpleSubtypeFieldNameProposer/resources/profile.json b/src/testInputs/simpleSubtypeFieldNameProposer/resources/profile.json new file mode 100644 index 00000000..698a2990 --- /dev/null +++ b/src/testInputs/simpleSubtypeFieldNameProposer/resources/profile.json @@ -0,0 +1,29 @@ +{ + "services" : { + "name_proposal": [ + { + "id": "enigma:specialized_method_name_proposer" + }, + { + "id": "quiltmc:name_proposal", + "args": { + "disable_constant_fields": "false", + "disable_records": "false", + "disable_codecs": "false" + } + } + ], + "jar_indexer": [ + { + "id": "quiltmc:jar_index", + "args": { + "disable_constant_fields": "false", + "disable_records": "false", + "disable_codecs": "false", + "simple_type_field_names_path": "./simple_type_field_names.json5", + "simple_type_verification_error_level": "THROW" + } + } + ] + } +} diff --git a/src/testInputs/simpleSubtypeFieldNameProposer/resources/simple_type_field_names.json5 b/src/testInputs/simpleSubtypeFieldNameProposer/resources/simple_type_field_names.json5 new file mode 100644 index 00000000..4f82e3fb --- /dev/null +++ b/src/testInputs/simpleSubtypeFieldNameProposer/resources/simple_type_field_names.json5 @@ -0,0 +1,27 @@ +{ + $schema: "../../../../src/main/resources/simple_type_field_names_schema.json", + + "a/a/b/b": { // Entity + local_name: "entity", + inherit: { + type: "TRUNCATED_SUBTYPE_NAME", + suffix: "Entity" + } + }, + "a/a/a/a": { // BlockEntityRenderer + local_name: "blockEntityRenderer", + inherit: { + type: "TRANSFORMED_SUBTYPE_NAME", + "pattern": "(.+)BlockEntityRenderer", + "replacement": "$1Renderer" + } + }, + "a/a/b/a/c": { // SpecificEntity + local_name: "specificEntity", + inherit: { + type: "TRANSFORMED_SUBTYPE_NAME", + "pattern": "Entity(.+)", + "replacement": "$1" + } + } +} \ No newline at end of file diff --git a/src/testInputs/simpleTypeFieldNameProposer/java/com/example/SimpleTypeNamesTest.java b/src/testInputs/simpleTypeFieldNameProposer/java/com/example/SimpleTypeNamesTest.java index 32f3e590..7ec9912a 100644 --- a/src/testInputs/simpleTypeFieldNameProposer/java/com/example/SimpleTypeNamesTest.java +++ b/src/testInputs/simpleTypeFieldNameProposer/java/com/example/SimpleTypeNamesTest.java @@ -11,6 +11,8 @@ import com.example.named.ValueC; import com.example.named.ValueD; import com.example.named.ValueDD; +import com.example.named.ValueDSpecific; +import com.example.named.ValueDSpecificInheritor; import com.example.named.ValueE; import com.example.named.ValueEE; @@ -23,14 +25,14 @@ public static class Duplicate { public static class Fallbacks { public static final Pos POS = new Pos(); - public final Position position = new Position(); - public final RandomPosition randomPosition = new RandomPosition(); + public static final Position POSITION = new Position(); + public static final RandomPosition RANDOM_POSITION = new RandomPosition(); public static final StateA STATIC_STATE_A = new StateA(); public static final StateB STATIC_STATE_B = new StateB(); - public static final ValueA VALUE_A = new ValueA(); - public static final ValueB VALUE_B = new ValueB(); + public final ValueA valueA = new ValueA(); + public final ValueB valueB = new ValueB(); public final ValueC valueC = new ValueC(); } @@ -85,5 +87,9 @@ public static void value(ValueE valueD) { public static void value(ValueEE valueD) { } + + public static void valueSpecific(ValueDSpecific specific) { } + + public static void valueSpecificInheritor(ValueDSpecificInheritor specific) { } } } diff --git a/src/testInputs/simpleTypeFieldNameProposer/java/com/example/named/ValueDSpecific.java b/src/testInputs/simpleTypeFieldNameProposer/java/com/example/named/ValueDSpecific.java new file mode 100644 index 00000000..346c681d --- /dev/null +++ b/src/testInputs/simpleTypeFieldNameProposer/java/com/example/named/ValueDSpecific.java @@ -0,0 +1,3 @@ +package com.example.named; + +public class ValueDSpecific extends ValueD { } diff --git a/src/testInputs/simpleTypeFieldNameProposer/java/com/example/named/ValueDSpecificInheritor.java b/src/testInputs/simpleTypeFieldNameProposer/java/com/example/named/ValueDSpecificInheritor.java new file mode 100644 index 00000000..aa241638 --- /dev/null +++ b/src/testInputs/simpleTypeFieldNameProposer/java/com/example/named/ValueDSpecificInheritor.java @@ -0,0 +1,3 @@ +package com.example.named; + +public class ValueDSpecificInheritor extends ValueDSpecific { } diff --git a/src/testInputs/simpleTypeFieldNameProposer/resources/profile.json b/src/testInputs/simpleTypeFieldNameProposer/resources/profile.json index 02c2f50d..698a2990 100644 --- a/src/testInputs/simpleTypeFieldNameProposer/resources/profile.json +++ b/src/testInputs/simpleTypeFieldNameProposer/resources/profile.json @@ -21,7 +21,7 @@ "disable_records": "false", "disable_codecs": "false", "simple_type_field_names_path": "./simple_type_field_names.json5", - "simple_type_verification_error_level": "WARN" + "simple_type_verification_error_level": "THROW" } } ] diff --git a/src/testInputs/simpleTypeFieldNameProposer/resources/simple_type_field_names.json5 b/src/testInputs/simpleTypeFieldNameProposer/resources/simple_type_field_names.json5 index 06c3171c..8c4454c2 100644 --- a/src/testInputs/simpleTypeFieldNameProposer/resources/simple_type_field_names.json5 +++ b/src/testInputs/simpleTypeFieldNameProposer/resources/simple_type_field_names.json5 @@ -1,5 +1,5 @@ { - $schema: "../../src/main/resources/simple_type_field_names_schema.json", + $schema: "../../../../src/main/resources/simple_type_field_names_schema.json", "a/a/a/a": "config", @@ -70,11 +70,19 @@ "a/a/a/j": { // ValueD local_name: "valueD", exclusive: true, - inherit: true + inherit: { + "type": "DIRECT" + } }, - "a/a/a/l": { // ValueE + "a/a/a/n": { // ValueE local_name: "valueE", exclusive: true }, - "not/present": "missing" + "a/a/a/l": { // ValueDSpecific + local_name: "specific", + exclusive: true, + inherit: { + "type": "DIRECT" + } + } } \ No newline at end of file diff --git a/src/testInputs/simpleTypeVerification/java/com/example/Present.java b/src/testInputs/simpleTypeVerification/java/com/example/Present.java new file mode 100644 index 00000000..667624a2 --- /dev/null +++ b/src/testInputs/simpleTypeVerification/java/com/example/Present.java @@ -0,0 +1,3 @@ +package com.example; + +public class Present { } diff --git a/src/testInputs/simpleTypeVerification/resources/profile.json b/src/testInputs/simpleTypeVerification/resources/profile.json new file mode 100644 index 00000000..698a2990 --- /dev/null +++ b/src/testInputs/simpleTypeVerification/resources/profile.json @@ -0,0 +1,29 @@ +{ + "services" : { + "name_proposal": [ + { + "id": "enigma:specialized_method_name_proposer" + }, + { + "id": "quiltmc:name_proposal", + "args": { + "disable_constant_fields": "false", + "disable_records": "false", + "disable_codecs": "false" + } + } + ], + "jar_indexer": [ + { + "id": "quiltmc:jar_index", + "args": { + "disable_constant_fields": "false", + "disable_records": "false", + "disable_codecs": "false", + "simple_type_field_names_path": "./simple_type_field_names.json5", + "simple_type_verification_error_level": "THROW" + } + } + ] + } +} diff --git a/src/testInputs/simpleTypeVerification/resources/simple_type_field_names.json5 b/src/testInputs/simpleTypeVerification/resources/simple_type_field_names.json5 new file mode 100644 index 00000000..509a6bb7 --- /dev/null +++ b/src/testInputs/simpleTypeVerification/resources/simple_type_field_names.json5 @@ -0,0 +1,7 @@ +{ + $schema: "../../../../src/main/resources/simple_type_field_names_schema.json", + + "a/a/a": "present", + + "not/present": "missing" +} \ No newline at end of file