protected void appendMethods(@Nonnull StringBuilder code) throws ExpressionCompileException {
		boolean isEnum = AccessFlag.isEnum(classAccess);
		for (MethodMember method : methods) {
			// Skip stubbing compiler-generated methods.
			if (method.hasBridgeModifier() || method.hasSyntheticModifier())
				continue;

			// Skip stubbing of illegally named methods.
			String name = method.getName();
			boolean isCtor = false;
			if (name.equals("<init>")) {
				// Skip constructors for enum classes since we always drop enum const parameters.
				if (isEnum)
					continue;
				isCtor = true;
			} else if (!isSafeName(name))
				continue;

			// Skip stubbing the method if it is the one we're assembling the expression within.
			String descriptor = method.getDescriptor();
			MethodType localMethodType = Types.methodType(descriptor);
			if (doSkipMethod(name, localMethodType))
				continue;

			// Skip enum's 'valueOf'
			if (isEnum &&
					name.equals("valueOf") &&
					descriptor.equals("(Ljava/lang/String;)L" + className + ";"))
				continue;

			// Skip stubbing of methods with bad return types / bad parameter types.
			NameType returnInfo = getInfo(name, localMethodType.returnType().descriptor());
			if (!isSafeClassName(returnInfo.className))
				continue;
			List<ClassType> parameterTypes = localMethodType.parameterTypes();
			if (!parameterTypes.stream().map(p -> {
				try {
					return getInfo("p", p.descriptor()).className();
				} catch (Throwable t) {
					return "\0"; // Bogus which will throw off the safe name check.
				}
			}).allMatch(ClassStubGenerator::isSafeClassName))
				continue;

			// Skip methods with return/parameter types that aren't accessible in the workspace.
			boolean hasMissingType = false;
			Type[] types = new Type[parameterTypes.size() + 1];
			for (int i = 0; i < types.length - 1; i++)
				types[i] = parameterTypes.get(i);
			types[parameterTypes.size()] = localMethodType.returnType();
			for (Type type : types) {
				hasMissingType = isMissingType(type);
				if (hasMissingType)
					break;
			}
			if (hasMissingType) continue;

			// Stub the method. Start with the access modifiers.
			if (method.hasPublicModifier())
				code.append("public ");
			else if (method.hasProtectedModifier())
				code.append("protected ");
			else if (method.hasPrivateModifier())
				code.append("private ");
			if (method.hasStaticModifier())
				code.append("static ");

			// Method name. Consider edge case for constructors.
			if (isCtor)
				code.append(getLocalName()).append('(');
			else
				code.append(returnInfo.className()).append(' ').append(returnInfo.name).append('(');

			// Add the parameters. We only care about the types, names don't really matter.
			List<ClassType> methodParameterTypes = parameterTypes;
			int parameterCount = methodParameterTypes.size();
			for (int i = 0; i < parameterCount; i++) {
				ClassType paramType = methodParameterTypes.get(i);

				// Skip this parameter if it is an inner class's outer "this" reference
				if (isCtor
						&& paramType instanceof ObjectType paramObjectType
						&& className.startsWith(paramObjectType.internalName() + '$'))
					continue;

				NameType paramInfo = getInfo("p" + i, paramType.descriptor());
				code.append(paramInfo.className).append(' ').append(paramInfo.name);
				if (i < parameterCount - 1)
					code.append(", ");
			}
			code.append(") { ");
			if (isCtor) {
				// If we know the parent type, we need to properly implement the constructor.
				// If we don't know the parent type, we cannot generate a valid constructor.
				ClassPathNode superPath = superName == null ? null : workspace.findJvmClass(superName);
				if (superPath == null && superName != null)
					// Generally this shouldn't happen since we filter the super-name in the constructor.
					// But just in case we'll keep this error handling here.
					throw new ExpressionCompileException("Cannot generate 'super(...)' for constructor, " +
							"missing type information for: " + superName);
				if (superPath != null) {
					// To make it easy, we'll find the simplest constructor in the parent class and pass dummy values.
					// Unlike regular methods we cannot just say 'throw new RuntimeException();' since calling
					// the 'super(...)' is required.
					MethodType parentConstructor = superPath.getValue().methodStream()
							.filter(m -> m.getName().equals("<init>"))
							.map(m -> Types.methodType(m.getDescriptor()))
							.min(Comparator.comparingInt(a -> a.parameterTypes().size()))
							.orElse(null);
					if (parentConstructor != null) {
						code.append("super(");
						parameterCount = parentConstructor.parameterTypes().size();
						for (int i = 0; i < parameterCount; i++) {
							ClassType type = parentConstructor.parameterTypes().get(i);
							if (type instanceof ObjectType) {
								code.append("null");
							} else {
								char prim = type.descriptor().charAt(0);
								if (prim == 'Z')
									code.append("false");
								else
									code.append('0');
							}
							if (i < parameterCount - 1) code.append(", ");
						}
						code.append(");");
					}
				}
			} else {
				code.append("throw new RuntimeException();");
			}
			code.append(" }\n");
		}
	}
Appends all method stubs to the class.
Some methods can be skipped by implementing {@link #doSkipMethod(String, MethodType)}.

@param code
		Class code to append the methods to.

@throws ExpressionCompileException
		When the methods could not be stubbed out.
recaf-core/src/main/java/software/coley/recaf/services/compile/stub/ClassStubGenerator.java