From 9abdbd5352115191d8ad5bb7018a5497430aa8db Mon Sep 17 00:00:00 2001 From: Joseph Burton Date: Wed, 20 Aug 2025 18:15:59 +0100 Subject: Services rework (#554) * Refactor services * Split out built-in name proposal into a separate plugin so that it can be disabled separately * Fix checkstyle--- enigma/src/main/java/cuchaz/enigma/Enigma.java | 66 +++++---- .../src/main/java/cuchaz/enigma/EnigmaProfile.java | 100 ++------------ .../enigma/analysis/BuiltinNameProposalPlugin.java | 143 ++++++++++++++++++++ .../java/cuchaz/enigma/analysis/BuiltinPlugin.java | 148 +-------------------- .../cuchaz/enigma/api/EnigmaPluginContext.java | 6 +- .../src/main/java/cuchaz/enigma/api/Ordering.java | 24 ++++ .../enigma/api/service/EnigmaServiceContext.java | 11 -- .../enigma/api/service/EnigmaServiceFactory.java | 2 +- .../java/cuchaz/enigma/utils/OrderingImpl.java | 129 ++++++++++++++++++ .../services/cuchaz.enigma.api.EnigmaPlugin | 1 + enigma/src/main/resources/profile.json | 20 --- 11 files changed, 358 insertions(+), 292 deletions(-) create mode 100644 enigma/src/main/java/cuchaz/enigma/analysis/BuiltinNameProposalPlugin.java create mode 100644 enigma/src/main/java/cuchaz/enigma/api/Ordering.java delete mode 100644 enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceContext.java create mode 100644 enigma/src/main/java/cuchaz/enigma/utils/OrderingImpl.java delete mode 100644 enigma/src/main/resources/profile.json diff --git a/enigma/src/main/java/cuchaz/enigma/Enigma.java b/enigma/src/main/java/cuchaz/enigma/Enigma.java index 315b5f6..01967bb 100644 --- a/enigma/src/main/java/cuchaz/enigma/Enigma.java +++ b/enigma/src/main/java/cuchaz/enigma/Enigma.java @@ -13,7 +13,12 @@ package cuchaz.enigma; import java.io.IOException; import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.ServiceLoader; import java.util.Set; @@ -24,6 +29,7 @@ import org.objectweb.asm.Opcodes; import cuchaz.enigma.analysis.index.JarIndex; import cuchaz.enigma.api.EnigmaPlugin; import cuchaz.enigma.api.EnigmaPluginContext; +import cuchaz.enigma.api.Ordering; import cuchaz.enigma.api.service.EnigmaService; import cuchaz.enigma.api.service.EnigmaServiceFactory; import cuchaz.enigma.api.service.EnigmaServiceType; @@ -32,6 +38,7 @@ import cuchaz.enigma.classprovider.CachingClassProvider; import cuchaz.enigma.classprovider.ClassProvider; import cuchaz.enigma.classprovider.CombiningClassProvider; import cuchaz.enigma.classprovider.JarClassProvider; +import cuchaz.enigma.utils.OrderingImpl; import cuchaz.enigma.utils.Utils; public class Enigma { @@ -96,7 +103,6 @@ public class Enigma { public static class Builder { private EnigmaProfile profile = EnigmaProfile.EMPTY; - private Iterable plugins = ServiceLoader.load(EnigmaPlugin.class); private Builder() { } @@ -107,18 +113,12 @@ public class Enigma { return this; } - public Builder setPlugins(Iterable plugins) { - Preconditions.checkNotNull(plugins, "plugins cannot be null"); - this.plugins = plugins; - return this; - } - public Enigma build() { - PluginContext pluginContext = new PluginContext(profile); + PluginContext pluginContext = new PluginContext(); - for (EnigmaPlugin plugin : plugins) { - plugin.init(pluginContext); - } + ServiceLoader.load(EnigmaPlugin.class).stream() + .filter(plugin -> !profile.getDisabledPlugins().contains(plugin.type().getName())) + .forEach(plugin -> plugin.get().init(pluginContext)); EnigmaServices services = pluginContext.buildServices(); return new Enigma(profile, services); @@ -126,30 +126,44 @@ public class Enigma { } private static class PluginContext implements EnigmaPluginContext { - private final EnigmaProfile profile; + private final Map, PendingServices> pendingServices = new HashMap<>(); - private final ImmutableListMultimap.Builder, EnigmaService> services = ImmutableListMultimap.builder(); - - PluginContext(EnigmaProfile profile) { - this.profile = profile; + @Override + public void registerService(String id, EnigmaServiceType serviceType, EnigmaServiceFactory factory, Ordering... ordering) { + @SuppressWarnings("unchecked") + PendingServices pending = (PendingServices) pendingServices.computeIfAbsent(serviceType, k -> new PendingServices<>()); + pending.factories.put(id, factory); + pending.orderings.put(id, Arrays.asList(ordering)); } @Override - public void registerService(String id, EnigmaServiceType serviceType, EnigmaServiceFactory factory) { - List serviceProfiles = profile.getServiceProfiles(serviceType); - - for (EnigmaProfile.Service serviceProfile : serviceProfiles) { - if (serviceProfile.matches(id)) { - T service = factory.create(serviceProfile::getArgument); - services.put(serviceType, service); - break; - } - } + public void disableService(String id, EnigmaServiceType serviceType) { + pendingServices.computeIfAbsent(serviceType, k -> new PendingServices<>()).disabled.add(id); } EnigmaServices buildServices() { + ImmutableListMultimap.Builder, EnigmaService> services = ImmutableListMultimap.builder(); + + pendingServices.forEach((serviceType, pending) -> { + pending.orderings.keySet().removeAll(pending.disabled); + List orderedServices = OrderingImpl.sort(serviceType.key, pending.orderings); + orderedServices.forEach(serviceId -> { + services.put(serviceType, pending.factories.get(serviceId).create()); + }); + }); + return new EnigmaServices(services.build()); } + + private record PendingServices( + Map> factories, + Map> orderings, + Set disabled + ) { + PendingServices() { + this(new HashMap<>(), new LinkedHashMap<>(), new HashSet<>()); + } + } } static { diff --git a/enigma/src/main/java/cuchaz/enigma/EnigmaProfile.java b/enigma/src/main/java/cuchaz/enigma/EnigmaProfile.java index f95bf1e..1bcbaa9 100644 --- a/enigma/src/main/java/cuchaz/enigma/EnigmaProfile.java +++ b/enigma/src/main/java/cuchaz/enigma/EnigmaProfile.java @@ -2,50 +2,32 @@ package cuchaz.enigma; import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStreamReader; import java.io.Reader; -import java.lang.reflect.Type; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.Set; import javax.annotation.Nullable; -import com.google.common.collect.ImmutableMap; import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; import com.google.gson.annotations.SerializedName; -import com.google.gson.reflect.TypeToken; -import cuchaz.enigma.api.service.EnigmaServiceType; import cuchaz.enigma.translation.mapping.serde.MappingFileNameFormat; import cuchaz.enigma.translation.mapping.serde.MappingSaveParameters; public final class EnigmaProfile { - public static final EnigmaProfile EMPTY = new EnigmaProfile(new ServiceContainer(ImmutableMap.of())); + public static final EnigmaProfile EMPTY = new EnigmaProfile(); private static final MappingSaveParameters DEFAULT_MAPPING_SAVE_PARAMETERS = new MappingSaveParameters(MappingFileNameFormat.BY_DEOBF); - private static final Gson GSON = new GsonBuilder().registerTypeAdapter(ServiceContainer.class, (JsonDeserializer) EnigmaProfile::loadServiceContainer).create(); - private static final Type SERVICE_LIST_TYPE = new TypeToken>() { - }.getType(); + private static final Gson GSON = new Gson(); - @SerializedName("services") - private final ServiceContainer serviceProfiles; + @SerializedName("disabled_plugins") + private final Set disabledPlugins = Set.of(); @SerializedName("mapping_save_parameters") - private final MappingSaveParameters mappingSaveParameters = null; + private final MappingSaveParameters mappingSaveParameters = DEFAULT_MAPPING_SAVE_PARAMETERS; - private EnigmaProfile(ServiceContainer serviceProfiles) { - this.serviceProfiles = serviceProfiles; + private EnigmaProfile() { } public static EnigmaProfile read(@Nullable Path file) throws IOException { @@ -54,12 +36,7 @@ public final class EnigmaProfile { return EnigmaProfile.parse(reader); } } else { - try (BufferedReader reader = new BufferedReader(new InputStreamReader(EnigmaProfile.class.getResourceAsStream("/profile.json"), StandardCharsets.UTF_8))) { - return EnigmaProfile.parse(reader); - } catch (IOException ex) { - System.err.println("Failed to load default profile, will use empty profile: " + ex.getMessage()); - return EnigmaProfile.EMPTY; - } + return EMPTY; } } @@ -67,66 +44,11 @@ public final class EnigmaProfile { return GSON.fromJson(reader, EnigmaProfile.class); } - private static ServiceContainer loadServiceContainer(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - if (!json.isJsonObject()) { - throw new JsonParseException("services must be an Object!"); - } - - JsonObject object = json.getAsJsonObject(); - - ImmutableMap.Builder> builder = ImmutableMap.builder(); - - for (Map.Entry entry : object.entrySet()) { - JsonElement value = entry.getValue(); - - if (value.isJsonObject()) { - builder.put(entry.getKey(), Collections.singletonList(GSON.fromJson(value, Service.class))); - } else if (value.isJsonArray()) { - builder.put(entry.getKey(), GSON.fromJson(value, SERVICE_LIST_TYPE)); - } else { - throw new JsonParseException(String.format("Don't know how to convert %s to a list of service!", value)); - } - } - - return new ServiceContainer(builder.build()); - } - - public List getServiceProfiles(EnigmaServiceType serviceType) { - return serviceProfiles.get(serviceType.key); + public Set getDisabledPlugins() { + return disabledPlugins; } public MappingSaveParameters getMappingSaveParameters() { - //noinspection ConstantConditions - return mappingSaveParameters == null ? EnigmaProfile.DEFAULT_MAPPING_SAVE_PARAMETERS : mappingSaveParameters; - } - - public static class Service { - private final String id; - private final Map args; - - Service(String id, Map args) { - this.id = id; - this.args = args; - } - - public boolean matches(String id) { - return this.id.equals(id); - } - - public Optional getArgument(String key) { - return args != null ? Optional.ofNullable(args.get(key)) : Optional.empty(); - } - } - - static final class ServiceContainer { - private final Map> services; - - ServiceContainer(Map> services) { - this.services = services; - } - - List get(String key) { - return services.getOrDefault(key, Collections.emptyList()); - } + return mappingSaveParameters; } } diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinNameProposalPlugin.java b/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinNameProposalPlugin.java new file mode 100644 index 0000000..90029d1 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinNameProposalPlugin.java @@ -0,0 +1,143 @@ +package cuchaz.enigma.analysis; + +import java.util.ArrayList; +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 org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.analysis.Analyzer; +import org.objectweb.asm.tree.analysis.Frame; +import org.objectweb.asm.tree.analysis.SourceInterpreter; +import org.objectweb.asm.tree.analysis.SourceValue; + +import cuchaz.enigma.Enigma; +import cuchaz.enigma.api.EnigmaPlugin; +import cuchaz.enigma.api.EnigmaPluginContext; +import cuchaz.enigma.api.service.JarIndexerService; +import cuchaz.enigma.api.service.NameProposalService; +import cuchaz.enigma.translation.representation.TypeDescriptor; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.FieldEntry; +import cuchaz.enigma.utils.Pair; + +public class BuiltinNameProposalPlugin implements EnigmaPlugin { + @Override + public void init(EnigmaPluginContext ctx) { + final Map, String> names = new HashMap<>(); + JarIndexerService indexerService = JarIndexerService.fromVisitorsInParallel(EnumFieldNameFindingVisitor::new, visitors -> visitors.forEach(visitor -> names.putAll(visitor.mappings))); + + ctx.registerService("enigma:enum_initializer_indexer", JarIndexerService.TYPE, () -> indexerService); + ctx.registerService("enigma:enum_name_proposer", NameProposalService.TYPE, () -> (obfEntry, remapper) -> Optional.ofNullable(names.get(obfEntry))); + } + + private static final class EnumFieldNameFindingVisitor extends ClassVisitor { + private ClassEntry clazz; + private String className; + private final Map, String> mappings = new HashMap<>(); + private final Set> enumFields = new HashSet<>(); + private final List classInits = new ArrayList<>(); + + EnumFieldNameFindingVisitor() { + super(Enigma.ASM_VERSION); + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + super.visit(version, access, name, signature, superName, interfaces); + this.className = name; + this.clazz = new ClassEntry(name); + this.enumFields.clear(); + this.classInits.clear(); + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + if ((access & Opcodes.ACC_ENUM) != 0) { + if (!enumFields.add(new Pair<>(name, descriptor))) { + throw new IllegalArgumentException("Found two enum fields with the same name \"" + name + "\" and desc \"" + descriptor + "\"!"); + } + } + + return super.visitField(access, name, descriptor, signature, value); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + if ("".equals(name)) { + MethodNode node = new MethodNode(api, access, name, descriptor, signature, exceptions); + classInits.add(node); + return node; + } + + return super.visitMethod(access, name, descriptor, signature, exceptions); + } + + @Override + public void visitEnd() { + super.visitEnd(); + + try { + collectResults(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private void collectResults() throws Exception { + if (enumFields.isEmpty()) { + return; + } + + String owner = className; + Analyzer analyzer = new Analyzer<>(new SourceInterpreter()); + + for (MethodNode mn : classInits) { + Frame[] frames = analyzer.analyze(className, mn); + InsnList instrs = mn.instructions; + + for (int i = 1; i < instrs.size(); i++) { + AbstractInsnNode instr1 = instrs.get(i - 1); + AbstractInsnNode instr2 = instrs.get(i); + String s = null; + + if (instr2.getOpcode() == Opcodes.PUTSTATIC && ((FieldInsnNode) instr2).owner.equals(owner) && enumFields.contains(new Pair<>(((FieldInsnNode) instr2).name, ((FieldInsnNode) instr2).desc)) && instr1.getOpcode() == Opcodes.INVOKESPECIAL && "".equals( + ((MethodInsnNode) instr1).name)) { + for (int j = 0; j < frames[i - 1].getStackSize(); j++) { + SourceValue sv = frames[i - 1].getStack(j); + + for (AbstractInsnNode ci : sv.insns) { + if (ci instanceof LdcInsnNode && ((LdcInsnNode) ci).cst instanceof String) { + //if (s == null || !s.equals(((LdcInsnNode) ci).cst)) { + if (s == null) { + s = (String) (((LdcInsnNode) ci).cst); + // stringsFound++; + } + } + } + } + } + + if (s != null) { + mappings.put(new FieldEntry(clazz, ((FieldInsnNode) instr2).name, new TypeDescriptor(((FieldInsnNode) instr2).desc)), s); + } + + // report otherwise? + } + } + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinPlugin.java b/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinPlugin.java index 0f53d26..aed949a 100644 --- a/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinPlugin.java +++ b/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinPlugin.java @@ -1,160 +1,20 @@ package cuchaz.enigma.analysis; -import java.util.ArrayList; -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 org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.FieldVisitor; -import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.FieldInsnNode; -import org.objectweb.asm.tree.InsnList; -import org.objectweb.asm.tree.LdcInsnNode; -import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.MethodNode; -import org.objectweb.asm.tree.analysis.Analyzer; -import org.objectweb.asm.tree.analysis.Frame; -import org.objectweb.asm.tree.analysis.SourceInterpreter; -import org.objectweb.asm.tree.analysis.SourceValue; - -import cuchaz.enigma.Enigma; import cuchaz.enigma.api.EnigmaPlugin; import cuchaz.enigma.api.EnigmaPluginContext; -import cuchaz.enigma.api.service.JarIndexerService; -import cuchaz.enigma.api.service.NameProposalService; import cuchaz.enigma.source.DecompilerService; import cuchaz.enigma.source.Decompilers; -import cuchaz.enigma.translation.representation.TypeDescriptor; -import cuchaz.enigma.translation.representation.entry.ClassEntry; -import cuchaz.enigma.translation.representation.entry.Entry; -import cuchaz.enigma.translation.representation.entry.FieldEntry; -import cuchaz.enigma.utils.Pair; public final class BuiltinPlugin implements EnigmaPlugin { - public BuiltinPlugin() { - } - @Override public void init(EnigmaPluginContext ctx) { - registerEnumNamingService(ctx); registerDecompilerServices(ctx); } - private void registerEnumNamingService(EnigmaPluginContext ctx) { - final Map, String> names = new HashMap<>(); - JarIndexerService indexerService = JarIndexerService.fromVisitorsInParallel(EnumFieldNameFindingVisitor::new, visitors -> visitors.forEach(visitor -> names.putAll(visitor.mappings))); - - ctx.registerService("enigma:enum_initializer_indexer", JarIndexerService.TYPE, ctx1 -> indexerService); - ctx.registerService("enigma:enum_name_proposer", NameProposalService.TYPE, ctx1 -> (obfEntry, remapper) -> Optional.ofNullable(names.get(obfEntry))); - } - private void registerDecompilerServices(EnigmaPluginContext ctx) { - ctx.registerService("enigma:vineflower", DecompilerService.TYPE, ctx1 -> Decompilers.VINEFLOWER); - ctx.registerService("enigma:cfr", DecompilerService.TYPE, ctx1 -> Decompilers.CFR); - ctx.registerService("enigma:procyon", DecompilerService.TYPE, ctx1 -> Decompilers.PROCYON); - ctx.registerService("enigma:bytecode", DecompilerService.TYPE, ctx1 -> Decompilers.BYTECODE); - } - - private static final class EnumFieldNameFindingVisitor extends ClassVisitor { - private ClassEntry clazz; - private String className; - private final Map, String> mappings = new HashMap<>(); - private final Set> enumFields = new HashSet<>(); - private final List classInits = new ArrayList<>(); - - EnumFieldNameFindingVisitor() { - super(Enigma.ASM_VERSION); - } - - @Override - public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { - super.visit(version, access, name, signature, superName, interfaces); - this.className = name; - this.clazz = new ClassEntry(name); - this.enumFields.clear(); - this.classInits.clear(); - } - - @Override - public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { - if ((access & Opcodes.ACC_ENUM) != 0) { - if (!enumFields.add(new Pair<>(name, descriptor))) { - throw new IllegalArgumentException("Found two enum fields with the same name \"" + name + "\" and desc \"" + descriptor + "\"!"); - } - } - - return super.visitField(access, name, descriptor, signature, value); - } - - @Override - public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { - if ("".equals(name)) { - MethodNode node = new MethodNode(api, access, name, descriptor, signature, exceptions); - classInits.add(node); - return node; - } - - return super.visitMethod(access, name, descriptor, signature, exceptions); - } - - @Override - public void visitEnd() { - super.visitEnd(); - - try { - collectResults(); - } catch (Exception ex) { - throw new RuntimeException(ex); - } - } - - private void collectResults() throws Exception { - if (enumFields.isEmpty()) { - return; - } - - String owner = className; - Analyzer analyzer = new Analyzer<>(new SourceInterpreter()); - - for (MethodNode mn : classInits) { - Frame[] frames = analyzer.analyze(className, mn); - InsnList instrs = mn.instructions; - - for (int i = 1; i < instrs.size(); i++) { - AbstractInsnNode instr1 = instrs.get(i - 1); - AbstractInsnNode instr2 = instrs.get(i); - String s = null; - - if (instr2.getOpcode() == Opcodes.PUTSTATIC && ((FieldInsnNode) instr2).owner.equals(owner) && enumFields.contains(new Pair<>(((FieldInsnNode) instr2).name, ((FieldInsnNode) instr2).desc)) && instr1.getOpcode() == Opcodes.INVOKESPECIAL && "".equals( - ((MethodInsnNode) instr1).name)) { - for (int j = 0; j < frames[i - 1].getStackSize(); j++) { - SourceValue sv = frames[i - 1].getStack(j); - - for (AbstractInsnNode ci : sv.insns) { - if (ci instanceof LdcInsnNode && ((LdcInsnNode) ci).cst instanceof String) { - //if (s == null || !s.equals(((LdcInsnNode) ci).cst)) { - if (s == null) { - s = (String) (((LdcInsnNode) ci).cst); - // stringsFound++; - } - } - } - } - } - - if (s != null) { - mappings.put(new FieldEntry(clazz, ((FieldInsnNode) instr2).name, new TypeDescriptor(((FieldInsnNode) instr2).desc)), s); - } - - // report otherwise? - } - } - } + ctx.registerService("enigma:vineflower", DecompilerService.TYPE, () -> Decompilers.VINEFLOWER); + ctx.registerService("enigma:cfr", DecompilerService.TYPE, () -> Decompilers.CFR); + ctx.registerService("enigma:procyon", DecompilerService.TYPE, () -> Decompilers.PROCYON); + ctx.registerService("enigma:bytecode", DecompilerService.TYPE, () -> Decompilers.BYTECODE); } } diff --git a/enigma/src/main/java/cuchaz/enigma/api/EnigmaPluginContext.java b/enigma/src/main/java/cuchaz/enigma/api/EnigmaPluginContext.java index a59051a..1371378 100644 --- a/enigma/src/main/java/cuchaz/enigma/api/EnigmaPluginContext.java +++ b/enigma/src/main/java/cuchaz/enigma/api/EnigmaPluginContext.java @@ -1,9 +1,13 @@ package cuchaz.enigma.api; +import org.jetbrains.annotations.ApiStatus; + import cuchaz.enigma.api.service.EnigmaService; import cuchaz.enigma.api.service.EnigmaServiceFactory; import cuchaz.enigma.api.service.EnigmaServiceType; +@ApiStatus.NonExtendable public interface EnigmaPluginContext { - void registerService(String id, EnigmaServiceType serviceType, EnigmaServiceFactory factory); + void registerService(String id, EnigmaServiceType serviceType, EnigmaServiceFactory factory, Ordering... ordering); + void disableService(String id, EnigmaServiceType serviceType); } diff --git a/enigma/src/main/java/cuchaz/enigma/api/Ordering.java b/enigma/src/main/java/cuchaz/enigma/api/Ordering.java new file mode 100644 index 0000000..9a384ba --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/api/Ordering.java @@ -0,0 +1,24 @@ +package cuchaz.enigma.api; + +import org.jetbrains.annotations.ApiStatus; + +import cuchaz.enigma.utils.OrderingImpl; + +@ApiStatus.NonExtendable +public interface Ordering { + static Ordering first() { + return OrderingImpl.First.INSTANCE; + } + + static Ordering last() { + return OrderingImpl.Last.INSTANCE; + } + + static Ordering before(String id) { + return new OrderingImpl.Before(id); + } + + static Ordering after(String id) { + return new OrderingImpl.After(id); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceContext.java b/enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceContext.java deleted file mode 100644 index 9e433fb..0000000 --- a/enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceContext.java +++ /dev/null @@ -1,11 +0,0 @@ -package cuchaz.enigma.api.service; - -import java.util.Optional; - -public interface EnigmaServiceContext { - static EnigmaServiceContext empty() { - return key -> Optional.empty(); - } - - Optional getArgument(String key); -} diff --git a/enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceFactory.java b/enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceFactory.java index 7c10ac2..e33ad49 100644 --- a/enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceFactory.java +++ b/enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceFactory.java @@ -1,5 +1,5 @@ package cuchaz.enigma.api.service; public interface EnigmaServiceFactory { - T create(EnigmaServiceContext ctx); + T create(); } diff --git a/enigma/src/main/java/cuchaz/enigma/utils/OrderingImpl.java b/enigma/src/main/java/cuchaz/enigma/utils/OrderingImpl.java new file mode 100644 index 0000000..c68ccb7 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/utils/OrderingImpl.java @@ -0,0 +1,129 @@ +package cuchaz.enigma.utils; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import cuchaz.enigma.api.Ordering; + +public sealed interface OrderingImpl extends Ordering { + static List sort(String serviceName, Map> idToOrderings) { + List first = new ArrayList<>(); + List middle = new ArrayList<>(); + Map inDegree = new HashMap<>(); + Map> graph = new HashMap<>(); + + idToOrderings.forEach((id, orderings) -> { + inDegree.put(id, 0); + graph.put(id, new LinkedHashSet<>()); + + boolean isFirst = false; + boolean isLast = false; + + for (Ordering ordering : orderings) { + if (ordering instanceof First) { + isFirst = true; + } else if (ordering instanceof Last) { + isLast = true; + } + } + + if (isFirst) { + if (isLast) { + throw new IllegalArgumentException("Service " + id + " has both 'first' and 'last' ordering"); + } + + first.add(id); + } else if (!isLast) { + middle.add(id); + } + }); + + idToOrderings.forEach((id, orderings) -> { + boolean isFirst = false; + boolean isLast = false; + + for (Ordering ordering : orderings) { + if (ordering instanceof Before before) { + if (idToOrderings.containsKey(before.id())) { + if (graph.get(id).add(before.id())) { + inDegree.merge(before.id(), 1, Integer::sum); + } + } + } else if (ordering instanceof After after) { + if (idToOrderings.containsKey(after.id())) { + if (graph.get(after.id()).add(id)) { + inDegree.merge(id, 1, Integer::sum); + } + } + } else if (ordering instanceof First) { + isFirst = true; + } else if (ordering instanceof Last) { + isLast = true; + } + } + + if (!isFirst) { + for (String aFirst : first) { + if (graph.get(aFirst).add(id)) { + inDegree.merge(id, 1, Integer::sum); + } + } + + if (isLast) { + for (String aMiddle : middle) { + if (graph.get(aMiddle).add(id)) { + inDegree.merge(id, 1, Integer::sum); + } + } + } + } + }); + + Deque queue = new ArrayDeque<>(); + + for (String id : idToOrderings.keySet()) { + if (inDegree.get(id) == 0) { + queue.add(id); + } + } + + List result = new ArrayList<>(idToOrderings.size()); + + while (!queue.isEmpty()) { + String id = queue.remove(); + result.add(id); + + for (String successor : graph.get(id)) { + if (inDegree.merge(successor, -1, Integer::sum) == 0) { + queue.add(successor); + } + } + } + + if (result.size() != idToOrderings.size()) { + throw new IllegalStateException("Services in " + serviceName + " contain circular dependencies"); + } + + return result; + } + + enum First implements OrderingImpl { + INSTANCE + } + + enum Last implements OrderingImpl { + INSTANCE + } + + record Before(String id) implements OrderingImpl { + } + + record After(String id) implements OrderingImpl { + } +} diff --git a/enigma/src/main/resources/META-INF/services/cuchaz.enigma.api.EnigmaPlugin b/enigma/src/main/resources/META-INF/services/cuchaz.enigma.api.EnigmaPlugin index 136a3e7..ab979a8 100644 --- a/enigma/src/main/resources/META-INF/services/cuchaz.enigma.api.EnigmaPlugin +++ b/enigma/src/main/resources/META-INF/services/cuchaz.enigma.api.EnigmaPlugin @@ -1 +1,2 @@ cuchaz.enigma.analysis.BuiltinPlugin +cuchaz.enigma.analysis.BuiltinNameProposalPlugin diff --git a/enigma/src/main/resources/profile.json b/enigma/src/main/resources/profile.json deleted file mode 100644 index e1af4cd..0000000 --- a/enigma/src/main/resources/profile.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "services": { - "jar_indexer": [ - { - "id": "enigma:enum_initializer_indexer" - }, - { - "id": "enigma:specialized_bridge_method_indexer" - } - ], - "name_proposal": [ - { - "id": "enigma:enum_name_proposer" - }, - { - "id": "enigma:specialized_method_name_proposer" - } - ] - } -} \ No newline at end of file -- cgit v1.2.3