kopia lustrzana https://github.com/onthegomap/planetiler
500 wiersze
17 KiB
Java
500 wiersze
17 KiB
Java
package com.onthegomap.planetiler.experimental.lua;
|
|
|
|
import static com.onthegomap.planetiler.experimental.lua.JavaToLuaCase.transformMemberName;
|
|
import static java.util.Map.entry;
|
|
|
|
import com.google.common.base.CaseFormat;
|
|
import com.google.common.reflect.Invokable;
|
|
import com.google.common.reflect.TypeToken;
|
|
import com.onthegomap.planetiler.util.Format;
|
|
import java.lang.reflect.Executable;
|
|
import java.lang.reflect.Field;
|
|
import java.lang.reflect.Member;
|
|
import java.lang.reflect.Method;
|
|
import java.lang.reflect.Modifier;
|
|
import java.lang.reflect.RecordComponent;
|
|
import java.lang.reflect.Type;
|
|
import java.lang.reflect.TypeVariable;
|
|
import java.nio.file.Path;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Comparator;
|
|
import java.util.Deque;
|
|
import java.util.HashSet;
|
|
import java.util.LinkedHashSet;
|
|
import java.util.LinkedList;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.TreeMap;
|
|
import java.util.TreeSet;
|
|
import java.util.stream.Collectors;
|
|
import org.luaj.vm2.LuaDouble;
|
|
import org.luaj.vm2.LuaInteger;
|
|
import org.luaj.vm2.LuaNumber;
|
|
import org.luaj.vm2.LuaString;
|
|
import org.luaj.vm2.LuaTable;
|
|
import org.luaj.vm2.LuaValue;
|
|
import org.luaj.vm2.lib.jse.LuaBindMethods;
|
|
import org.luaj.vm2.lib.jse.LuaFunctionType;
|
|
import org.luaj.vm2.lib.jse.LuaGetter;
|
|
import org.luaj.vm2.lib.jse.LuaSetter;
|
|
import org.luaj.vm2.lib.jse.LuaType;
|
|
|
|
/**
|
|
* Generates a lua file with type definitions for the lua environment exposed by planetiler.
|
|
*
|
|
* <pre>
|
|
* java -jar planetiler.jar lua-types > types.lua
|
|
* </pre>
|
|
*
|
|
* @see <a href="https://luals.github.io/wiki/annotations/">Lua Language Server type annotations</a>
|
|
*/
|
|
public class GenerateLuaTypes {
|
|
private static final Map<Class<?>, String> TYPE_NAMES = Map.ofEntries(
|
|
entry(Object.class, "any"),
|
|
entry(LuaInteger.class, "integer"),
|
|
entry(LuaDouble.class, "number"),
|
|
entry(LuaNumber.class, "number"),
|
|
entry(LuaString.class, "string"),
|
|
entry(LuaTable.class, "table"),
|
|
entry(Class.class, "userdata"),
|
|
entry(String.class, "string"),
|
|
entry(Number.class, "number"),
|
|
entry(byte[].class, "string"),
|
|
entry(Integer.class, "integer"),
|
|
entry(int.class, "integer"),
|
|
entry(Long.class, "integer"),
|
|
entry(long.class, "integer"),
|
|
entry(Short.class, "integer"),
|
|
entry(short.class, "integer"),
|
|
entry(Byte.class, "integer"),
|
|
entry(byte.class, "integer"),
|
|
entry(Double.class, "number"),
|
|
entry(double.class, "number"),
|
|
entry(Float.class, "number"),
|
|
entry(float.class, "number"),
|
|
entry(boolean.class, "boolean"),
|
|
entry(Boolean.class, "boolean"),
|
|
entry(Void.class, "nil"),
|
|
entry(void.class, "nil")
|
|
);
|
|
private static final TypeToken<List<?>> LIST_TYPE = new TypeToken<>() {};
|
|
private static final TypeToken<Map<?, ?>> MAP_TYPE = new TypeToken<>() {};
|
|
private final Deque<String> debugStack = new LinkedList<>();
|
|
private final Set<String> handled = new HashSet<>();
|
|
private final StringBuilder builder = new StringBuilder();
|
|
private static final String NEWLINE = System.lineSeparator();
|
|
|
|
GenerateLuaTypes() {
|
|
write("""
|
|
---@meta
|
|
local types = {}
|
|
""");
|
|
}
|
|
|
|
public static void main(String[] args) {
|
|
var generator = new GenerateLuaTypes().generatePlanetiler();
|
|
System.out.println(generator.toString().replaceAll("[\r\n]+", NEWLINE));
|
|
}
|
|
|
|
private static String luaClassName(Class<?> clazz) {
|
|
return clazz.getName().replaceAll("[\\$\\.]", "_");
|
|
}
|
|
|
|
private static boolean differentFromParents2(Invokable<?, ?> invokable, TypeToken<?> superType) {
|
|
if (!invokable.getReturnType().equals(superType.resolveType(invokable.getReturnType().getType()))) {
|
|
return true;
|
|
}
|
|
var orig =
|
|
invokable.getParameters().stream()
|
|
.map(t -> invokable.getOwnerType().resolveType(t.getType().getType()))
|
|
.toList();
|
|
var resolved = invokable.getParameters().stream().map(t -> superType.resolveType(t.getType().getType())).toList();
|
|
return !orig.equals(resolved);
|
|
}
|
|
|
|
private static boolean hasMethod(Class<?> clazz, Method method) {
|
|
try {
|
|
clazz.getMethod(method.getName(), method.getParameterTypes());
|
|
return true;
|
|
} catch (NoSuchMethodException e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private void write(String line) {
|
|
builder.append(line).append(NEWLINE);
|
|
}
|
|
|
|
GenerateLuaTypes generatePlanetiler() {
|
|
exportGlobalInstance("planetiler", LuaEnvironment.PlanetilerNamespace.class);
|
|
for (var clazz : LuaEnvironment.CLASSES_TO_EXPOSE) {
|
|
exportGlobalType(clazz);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
void exportGlobalInstance(String name, Class<?> clazz) {
|
|
write("---@class (exact) " + getInstanceTypeName(clazz));
|
|
write(name + " = {}");
|
|
}
|
|
|
|
void exportGlobalType(Class<?> clazz) {
|
|
write("---@class (exact) " + getStaticTypeName(clazz));
|
|
write(clazz.getSimpleName() + " = {}");
|
|
}
|
|
|
|
private String getStaticTypeName(Class<?> clazz) {
|
|
String name = luaClassName(clazz) + "__class";
|
|
debugStack.push(" -> " + clazz.getSimpleName());
|
|
try {
|
|
if (handled.add(name)) {
|
|
write(getStaticTypeDefinition(clazz));
|
|
}
|
|
return name;
|
|
} finally {
|
|
debugStack.pop();
|
|
}
|
|
}
|
|
|
|
private String getInstanceTypeName(TypeToken<?> type) {
|
|
if (LIST_TYPE.isSupertypeOf(type)) {
|
|
return getInstanceTypeName(type.resolveType(LIST_TYPE.getRawType().getTypeParameters()[0])) + "[]";
|
|
} else if (MAP_TYPE.isSupertypeOf(type)) {
|
|
return "{[%s]: %s}".formatted(
|
|
getInstanceTypeName(type.resolveType(MAP_TYPE.getRawType().getTypeParameters()[0])),
|
|
getInstanceTypeName(type.resolveType(MAP_TYPE.getRawType().getTypeParameters()[1]))
|
|
);
|
|
}
|
|
return getInstanceTypeName(type.getRawType());
|
|
}
|
|
|
|
private String getInstanceTypeName(Class<?> clazz) {
|
|
if (clazz.getPackageName().startsWith("com.google.protobuf")) {
|
|
return "any";
|
|
}
|
|
if (LuaValue.class.equals(clazz)) {
|
|
throw new IllegalArgumentException("Unhandled LuaValue: " + String.join("", debugStack));
|
|
}
|
|
debugStack.push(" -> " + clazz.getSimpleName());
|
|
try {
|
|
if (TYPE_NAMES.containsKey(clazz)) {
|
|
return TYPE_NAMES.get(clazz);
|
|
}
|
|
if (clazz.isArray()) {
|
|
return getInstanceTypeName(clazz.getComponentType()) + "[]";
|
|
}
|
|
if (LuaValue.class.isAssignableFrom(clazz)) {
|
|
return "any";
|
|
}
|
|
|
|
String name = luaClassName(clazz);
|
|
if (handled.add(name)) {
|
|
write(getTypeDefinition(clazz));
|
|
}
|
|
return name;
|
|
} finally {
|
|
debugStack.pop();
|
|
}
|
|
}
|
|
|
|
String getTypeDefinition(Class<?> clazz) {
|
|
return generateLuaInstanceTypeDefinition(clazz).generate();
|
|
}
|
|
|
|
String getStaticTypeDefinition(Class<?> clazz) {
|
|
return generateLuaTypeDefinition(clazz, "__class", true).generate();
|
|
}
|
|
|
|
private LuaTypeDefinition generateLuaInstanceTypeDefinition(Class<?> clazz) {
|
|
return generateLuaTypeDefinition(clazz, "", false);
|
|
}
|
|
|
|
private LuaTypeDefinition generateLuaTypeDefinition(Class<?> clazz, String suffix, boolean isStatic) {
|
|
TypeToken<?> type = TypeToken.of(clazz);
|
|
var definition = new LuaTypeDefinition(type, suffix, isStatic);
|
|
|
|
if (!isStatic) {
|
|
Type superclass = clazz.getGenericSuperclass();
|
|
if (superclass != null && superclass != Object.class) {
|
|
definition.addParent(type.resolveType(superclass));
|
|
}
|
|
for (var iface : clazz.getGenericInterfaces()) {
|
|
definition.addParent(type.resolveType(iface));
|
|
}
|
|
}
|
|
|
|
for (var field : clazz.getFields()) {
|
|
TypeToken<?> rawType = TypeToken.of(field.getDeclaringClass()).resolveType(field.getGenericType());
|
|
TypeToken<?> typeOnThisClass = type.resolveType(field.getGenericType());
|
|
if (Modifier.isPublic(field.getModifiers()) && isStatic == Modifier.isStatic(field.getModifiers()) &&
|
|
(isStatic || field.getDeclaringClass() == clazz || !rawType.equals(typeOnThisClass))) {
|
|
definition.addField(field);
|
|
}
|
|
}
|
|
|
|
Set<Method> recordFields = clazz.isRecord() ? Arrays.stream(clazz.getRecordComponents())
|
|
.map(RecordComponent::getAccessor)
|
|
.collect(Collectors.toSet()) : Set.of();
|
|
|
|
// - declare public (static and nonstatic) methods
|
|
for (var method : clazz.getMethods()) {
|
|
if (Modifier.isPublic(method.getModifiers()) && isStatic == Modifier.isStatic(method.getModifiers())) {
|
|
Invokable<?, ?> invokable = type.method(method);
|
|
if (!isStatic && !invokable.getOwnerType().equals(type) && !differentFromParents(invokable, type)) {
|
|
continue;
|
|
}
|
|
if (hasMethod(Object.class, method)) {
|
|
// skip object methods
|
|
} else if (method.isAnnotationPresent(LuaGetter.class) ||
|
|
(method.getParameterCount() == 0 && recordFields.contains(method))) {
|
|
definition.addField(method, method.getGenericReturnType());
|
|
} else if (method.isAnnotationPresent(LuaSetter.class)) {
|
|
definition.addField(method, method.getParameterTypes()[0]);
|
|
} else {
|
|
definition.addMethod(method);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isStatic) {
|
|
for (var constructor : clazz.getConstructors()) {
|
|
if (Modifier.isPublic(constructor.getModifiers())) {
|
|
definition.addMethod("new", constructor, clazz);
|
|
}
|
|
}
|
|
}
|
|
return definition;
|
|
}
|
|
|
|
private boolean differentFromParents(Invokable<?, ?> invokable, TypeToken<?> type) {
|
|
Class<?> superclass = type.getRawType().getSuperclass();
|
|
if (superclass != null) {
|
|
var superType = TypeToken.of(superclass);
|
|
if (differentFromParents2(invokable, superType)) {
|
|
return true;
|
|
}
|
|
}
|
|
for (var iface : type.getTypes().interfaces()) {
|
|
if (differentFromParents2(invokable, iface)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return builder.toString();
|
|
}
|
|
|
|
private record LuaFieldDefinition(String name, String type) {
|
|
|
|
void write(StringBuilder builder) {
|
|
builder.append("---@field %s %s%n".formatted(name, type));
|
|
}
|
|
}
|
|
|
|
private record LuaParameterDefinition(String name, String type) {}
|
|
|
|
private record LuaTypeParameter(String name, String superType) {
|
|
|
|
@Override
|
|
public String toString() {
|
|
return name +
|
|
((superType.equals(luaClassName(Object.class)) || "any".equals(superType)) ? "" : (" : " + superType));
|
|
}
|
|
}
|
|
|
|
private record LuaMethodDefinitions(String name, List<LuaTypeParameter> typeParameters,
|
|
List<LuaParameterDefinition> params, String returnType) {
|
|
|
|
void write(String typeName, StringBuilder builder) {
|
|
for (var typeParam : typeParameters) {
|
|
builder.append("---@generic %s%n".formatted(typeParam));
|
|
}
|
|
for (var param : params) {
|
|
builder.append("---@param %s %s%n".formatted(param.name, param.type));
|
|
}
|
|
builder.append("---@return %s%n".formatted(returnType));
|
|
builder.append("function types.%s:%s(%s) end%n".formatted(
|
|
typeName,
|
|
name,
|
|
params.stream().map(p -> p.name).collect(Collectors.joining(", "))
|
|
));
|
|
}
|
|
|
|
public String functionTypeString() {
|
|
return "fun(%s): %s".formatted(
|
|
params.stream().map(p -> p.name + ": " + p.type).collect(Collectors.joining(", ")),
|
|
returnType
|
|
);
|
|
}
|
|
|
|
public void writeAsField(StringBuilder builder) {
|
|
builder.append("---@field %s %s%n".formatted(
|
|
name,
|
|
functionTypeString()
|
|
));
|
|
}
|
|
}
|
|
|
|
private class LuaTypeDefinition {
|
|
|
|
private final TypeToken<?> type;
|
|
private final boolean isStatic;
|
|
String name;
|
|
Set<String> parents = new LinkedHashSet<>();
|
|
Map<String, LuaFieldDefinition> fields = new TreeMap<>();
|
|
Set<LuaMethodDefinitions> methods = new TreeSet<>(Comparator.comparing(Record::toString));
|
|
|
|
LuaTypeDefinition(TypeToken<?> type, String suffix, boolean isStatic) {
|
|
this.type = type;
|
|
this.name = luaClassName(type.getRawType()) + suffix;
|
|
this.isStatic = isStatic;
|
|
}
|
|
|
|
LuaTypeDefinition(TypeToken<?> type) {
|
|
this(type, "", false);
|
|
}
|
|
|
|
void addParent(TypeToken<?> type) {
|
|
parents.add(getInstanceTypeName(type));
|
|
}
|
|
|
|
LuaFieldDefinition addField(Member field, Type fieldType) {
|
|
try {
|
|
debugStack.push("." + field.getName());
|
|
String fieldName = transformMemberName(field.getName());
|
|
var fieldDefinition =
|
|
new LuaFieldDefinition(fieldName, getInstanceTypeName(type.resolveType(fieldType)));
|
|
fields.put(fieldName, fieldDefinition);
|
|
return fieldDefinition;
|
|
} finally {
|
|
debugStack.pop();
|
|
}
|
|
}
|
|
|
|
LuaFieldDefinition addField(Field field) {
|
|
if (field.getType().equals(LuaValue.class) && field.isAnnotationPresent(LuaFunctionType.class)) {
|
|
var functionDetails = field.getAnnotation(LuaFunctionType.class);
|
|
var target = functionDetails.target();
|
|
var targetMethod = functionDetails.method().isBlank() ?
|
|
CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, field.getName()) :
|
|
functionDetails.method();
|
|
var matchingMethods = Arrays.stream(target.getDeclaredMethods())
|
|
.filter(m -> m.getName().equals(targetMethod))
|
|
.toList();
|
|
if (matchingMethods.size() != 1) {
|
|
throw new IllegalArgumentException("Expected exactly 1 method named " + targetMethod +
|
|
" on " + target.getSimpleName() + ", found " + matchingMethods.size() + " " + String.join("", debugStack));
|
|
}
|
|
var definition = new LuaTypeDefinition(type);
|
|
var method = definition.createMethod(matchingMethods.get(0));
|
|
var fieldName = transformMemberName(field.getName());
|
|
var fieldDefinition = new LuaFieldDefinition(
|
|
fieldName,
|
|
method.functionTypeString()
|
|
);
|
|
fields.put(fieldName, fieldDefinition);
|
|
return fieldDefinition;
|
|
}
|
|
return addField(field, field.getGenericType());
|
|
}
|
|
|
|
void addMethod(Method method) {
|
|
methods.add(createMethod(method));
|
|
}
|
|
|
|
void addMethod(String methodName, Executable method, Type returnType) {
|
|
methods.add(createMethod(methodName, method, returnType));
|
|
}
|
|
|
|
private LuaMethodDefinitions createMethod(Method method) {
|
|
return createMethod(method.getName(), method, method.getGenericReturnType());
|
|
}
|
|
|
|
private LuaMethodDefinitions createMethod(String methodName, Executable method, Type returnType) {
|
|
methodName = transformMemberName(methodName);
|
|
List<LuaTypeParameter> typeParameters = new ArrayList<>();
|
|
for (var param : method.getTypeParameters()) {
|
|
typeParameters.add(new LuaTypeParameter(
|
|
param.getName(),
|
|
getInstanceTypeName(type.resolveType(param.getBounds()[0]))
|
|
));
|
|
}
|
|
List<LuaParameterDefinition> parameters = new ArrayList<>();
|
|
for (var param : method.getParameters()) {
|
|
parameters.add(new LuaParameterDefinition(
|
|
transformMemberName(param.getName()),
|
|
param.isAnnotationPresent(LuaType.class) ? param.getAnnotation(LuaType.class).value() :
|
|
param.getType() == Path.class ? "%s|string|string[]".formatted(getInstanceTypeName(Path.class)) :
|
|
resolveType(param.getParameterizedType(), method.getTypeParameters())
|
|
));
|
|
}
|
|
return new LuaMethodDefinitions(
|
|
methodName,
|
|
typeParameters,
|
|
parameters,
|
|
resolveType(returnType, method.getTypeParameters())
|
|
);
|
|
}
|
|
|
|
private String resolveType(Type elementType, TypeVariable<?>[] typeParameters) {
|
|
var resolvedType = type.resolveType(elementType);
|
|
// only return type parameter name when it is parameterized at the method level, see:
|
|
// https://github.com/LuaLS/lua-language-server/issues/734
|
|
// https://github.com/LuaLS/lua-language-server/issues/1861
|
|
if (resolvedType.getType() instanceof TypeVariable<?> variable) {
|
|
for (var typeParam : typeParameters) {
|
|
if (typeParam.getName().equals(variable.getName())) {
|
|
return typeParam.getName();
|
|
}
|
|
}
|
|
}
|
|
return getInstanceTypeName(resolvedType);
|
|
}
|
|
|
|
void write(StringBuilder builder) {
|
|
String nameToUse = this.name;
|
|
if (type.getRawType().isEnum() && !isStatic) {
|
|
nameToUse += "__enum";
|
|
builder.append("---@alias %s%n".formatted(name));
|
|
builder.append("---|%s%n".formatted(nameToUse));
|
|
builder.append("---|integer").append(NEWLINE);
|
|
for (var constant : type.getRawType().getEnumConstants()) {
|
|
builder.append("---|%s%n".formatted(Format.quote(constant.toString())));
|
|
}
|
|
}
|
|
builder.append("---@class (exact) %s".formatted(nameToUse));
|
|
if (!parents.isEmpty()) {
|
|
builder.append(" : ").append(String.join(", ", parents));
|
|
}
|
|
builder.append(NEWLINE);
|
|
for (var field : fields.values()) {
|
|
field.write(builder);
|
|
}
|
|
boolean bindMethods = type.getRawType().isAnnotationPresent(LuaBindMethods.class);
|
|
if (bindMethods) {
|
|
for (var method : methods) {
|
|
method.writeAsField(builder);
|
|
}
|
|
}
|
|
builder.append("types.%s = {}%n".formatted(nameToUse));
|
|
if (!bindMethods) {
|
|
for (var method : methods) {
|
|
method.write(nameToUse, builder);
|
|
}
|
|
}
|
|
}
|
|
|
|
public String generate() {
|
|
StringBuilder tmp = new StringBuilder();
|
|
write(tmp);
|
|
return tmp.toString();
|
|
}
|
|
|
|
}
|
|
}
|