Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ plugins {
}

group = "org.densy.scriptify.declaration"
version = "1.0.1-SNAPSHOT"
version = "1.1.0-SNAPSHOT"

java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
languageVersion = JavaLanguageVersion.of(21)
}
}

Expand All @@ -19,7 +19,8 @@ repositories {
}

dependencies {
api("org.densy.scriptify:api:1.5.0-SNAPSHOT")
api("org.densy.scriptify:api:1.6.0-beta")
api("org.densy.scriptify:core:1.6.0-beta")
compileOnlyApi("org.projectlombok:lombok:1.18.36")
annotationProcessor("org.projectlombok:lombok:1.18.36")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.densy.scriptify.declaration;

import lombok.Builder;
import lombok.Getter;

import java.util.HashSet;
import java.util.Set;

/**
* Options for the declaration generator.
*/
@Builder
@Getter
public class DeclarationGeneratorOptions {

@Builder.Default
private boolean generateClassHierarchy = false;

@Builder.Default
private Set<String> hierarchyExcludedPackages = new HashSet<>(Set.of(
"java.lang",
"java.io",
"java.nio"
));

@Builder.Default
private int hierarchyMaxDepth = 2;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package org.densy.scriptify.declaration;

import lombok.Setter;
import org.densy.scriptify.declaration.util.JavaToTypeScriptConverter;

import java.lang.reflect.*;
import java.util.*;

@Setter
public class ScriptTsClassDeclarationGenerator {

private Map<Class<?>, String> knownClasses = new HashMap<>();

private String resolveType(Type type) {
if (type instanceof Class<?> clazz) {
if (knownClasses.containsKey(clazz)) return knownClasses.get(clazz);
} else if (type instanceof ParameterizedType pt) {
if (pt.getRawType() instanceof Class<?> raw && knownClasses.containsKey(raw)) {
String rawName = knownClasses.get(raw);
String[] args = Arrays.stream(pt.getActualTypeArguments())
.map(this::resolveType)
.toArray(String[]::new);
return rawName + "<" + String.join(", ", args) + ">";
}
}
return JavaToTypeScriptConverter.convert(type);
}

public String generate(Class<?> clazz, String indent, String prefix) {
if (clazz.isEnum()) {
return generateEnum(clazz, indent, prefix);
}
if (clazz.isInterface()) {
return generateInterface(clazz, indent, prefix);
}
return generateClass(clazz, indent, prefix);
}

private String generateClass(Class<?> clazz, String indent, String prefix) {
StringBuilder builder = new StringBuilder();
builder.append(indent).append(prefix).append("class ")
.append(clazz.getSimpleName()).append(" {\n");

// constructors
for (Constructor<?> constructor : clazz.getConstructors()) {
builder.append(indent).append(" constructor(");
builder.append(generateParams(constructor.getGenericParameterTypes(),
constructor.getParameters()));
builder.append(");\n");
}

// public fields
for (Field field : clazz.getFields()) {
if (Modifier.isStatic(field.getModifiers())) continue;
builder.append(indent).append(" ");
if (Modifier.isFinal(field.getModifiers())) builder.append("readonly ");
builder.append(field.getName()).append(": ")
.append(resolveType(field.getGenericType()))
.append(";\n");
}

// public methods
for (Method method : clazz.getMethods()) {
if (method.getDeclaringClass() == Object.class) continue;
if (Modifier.isStatic(method.getModifiers())) continue;

builder.append(indent).append(" ")
.append(method.getName()).append("(")
.append(generateParams(method.getGenericParameterTypes(), method.getParameters()))
.append("): ")
.append(resolveType(method.getGenericReturnType()))
.append(";\n");
}

builder.append(indent).append("}");
return builder.toString();
}

private String generateInterface(Class<?> clazz, String indent, String prefix) {
StringBuilder builder = new StringBuilder();
builder.append(indent).append(prefix).append("interface ")
.append(clazz.getSimpleName()).append(" {\n");

// interface constants and static fields
for (Field field : clazz.getFields()) {
builder.append(indent).append(" readonly ")
.append(field.getName()).append(": ")
.append(resolveType(field.getGenericType()))
.append(";\n");
}

// interface methods
for (Method method : clazz.getMethods()) {
if (method.getDeclaringClass() == Object.class) continue;

builder.append(indent).append(" ")
.append(method.getName()).append("(")
.append(generateParams(method.getGenericParameterTypes(), method.getParameters()))
.append("): ")
.append(resolveType(method.getGenericReturnType()))
.append(";\n");
}

builder.append(indent).append("}");
return builder.toString();
}

private String generateEnum(Class<?> clazz, String indent, String prefix) {
StringBuilder builder = new StringBuilder();
builder.append(indent).append(prefix).append("enum ")
.append(clazz.getSimpleName()).append(" {\n");

Object[] constants = clazz.getEnumConstants();
for (int i = 0; i < constants.length; i++) {
builder.append(indent).append(" ").append(((Enum<?>) constants[i]).name());
if (i < constants.length - 1) builder.append(",");
builder.append("\n");
}

builder.append(indent).append("}");
return builder.toString();
}

private String generateParams(Type[] types, Parameter[] parameters) {
List<String> params = new ArrayList<>();
for (int i = 0; i < types.length; i++) {
String name = parameters[i].isNamePresent()
? parameters[i].getName()
: "arg" + i;
params.add(name + ": " + resolveType(types[i]));
}
return String.join(", ", params);
}
}
Original file line number Diff line number Diff line change
@@ -1,37 +1,43 @@
package org.densy.scriptify.declaration;

import org.densy.scriptify.api.script.Script;
import org.densy.scriptify.api.script.constant.ScriptConstant;
import org.densy.scriptify.api.script.constant.ScriptConstantManager;
import org.densy.scriptify.api.script.function.ScriptFunctionManager;
import org.densy.scriptify.api.script.function.definition.ScriptFunctionDefinition;
import org.densy.scriptify.api.script.function.definition.ScriptFunctionExecutor;
import org.densy.scriptify.declaration.util.JavaToTypeScriptConverter;
import org.densy.scriptify.api.script.module.ScriptInternalModule;
import org.densy.scriptify.api.script.module.ScriptModule;
import org.densy.scriptify.api.script.module.ScriptModuleManager;
import org.densy.scriptify.api.script.module.export.ScriptExport;
import org.densy.scriptify.api.script.module.export.ScriptValueExport;
import org.densy.scriptify.declaration.util.ClassHierarchyCollector;
import org.densy.scriptify.declaration.writer.ScriptTsExportDeclarationWriter;
import org.densy.scriptify.declaration.writer.ScriptTsModuleDeclarationWriter;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

/**
* Generator for Scriptify functions declarations.
*/
import java.nio.file.*;
import java.util.*;

public class ScriptTsDeclarationGenerator {

private final Script<?> script;
private final DeclarationGeneratorOptions options;
private final ScriptTsModuleDeclarationWriter moduleWriter;
private final ScriptTsClassDeclarationGenerator classGenerator;
private final ClassHierarchyCollector hierarchyCollector;

public ScriptTsDeclarationGenerator(Script<?> script) {
this(script, DeclarationGeneratorOptions.builder().build());
}

public ScriptTsDeclarationGenerator(Script<?> script, DeclarationGeneratorOptions options) {
this.script = script;
this.options = options;
this.classGenerator = new ScriptTsClassDeclarationGenerator();
ScriptTsExportDeclarationWriter exportWriter =
new ScriptTsExportDeclarationWriter(script, classGenerator);
this.moduleWriter = new ScriptTsModuleDeclarationWriter(exportWriter);
this.hierarchyCollector = new ClassHierarchyCollector(options, script);
}

/**
* Returns the main header of the declaration.
*
* @return String header
*/
protected String getHeader() {
try (InputStream is = getClass().getClassLoader().getResourceAsStream("header.txt")) {
if (is == null) {
Expand All @@ -43,75 +49,82 @@ protected String getHeader() {
}
}


/**
* Generates a declaration of constants and functions.
*
* @return Generated declaration
*/
public String generate() {
StringBuilder sb = new StringBuilder(this.getHeader());

// Generate constant declarations
ScriptConstantManager constantManager = script.getConstantManager();
if (constantManager != null) {
for (ScriptConstant constant : constantManager.getConstants().values()) {
Object value = constant.getValue();
sb.append("declare const ")
.append(constant.getName())
.append(": ")
.append(JavaToTypeScriptConverter.convert(value != null ? value.getClass() : null))
.append(";\n\n");
}
StringBuilder builder = new StringBuilder(getHeader());
ScriptModuleManager moduleManager = script.getModuleManager();

// collect all modules to process
List<ScriptInternalModule> allModules = new ArrayList<>();
allModules.add(moduleManager.getGlobalModule());
for (ScriptModule module : moduleManager.getModules().values()) {
if (module instanceof ScriptInternalModule im) allModules.add(im);
}

// Generate function declarations
ScriptFunctionManager functionManager = script.getFunctionManager();
if (functionManager != null) {
for (ScriptFunctionDefinition def : functionManager.getFunctions().values()) {
for (ScriptFunctionExecutor executor : def.getExecutors()) {
sb.append("/**\n")
.append(" * Script ").append(def.getFunction().getName()).append(" function").append("\n");
for (var arg : executor.getArguments()) {
sb.append(" * @param ").append(arg.getName())
.append(" ").append(arg.isRequired() ? "(required)" : "(optional)")
.append("\n");
}
sb.append(" */\n");

sb.append("declare function ")
.append(def.getFunction().getName())
.append("(");

List<String> params = new ArrayList<>();
for (var arg : executor.getArguments()) {
params.add(
arg.getName() +
(arg.isRequired() ? "" : "?") +
": " + JavaToTypeScriptConverter.convert(arg.getType())
);
}
sb.append(String.join(", ", params))
.append("): ")
.append(JavaToTypeScriptConverter.convert(executor.getMethod().getReturnType()))
.append(";\n\n");
Map<Class<?>, String> knownClasses = new LinkedHashMap<>();

Set<Class<?>> registered = collectRegisteredClasses(allModules);
for (Class<?> clazz : registered) {
knownClasses.put(clazz, clazz.getSimpleName());
}

if (options.isGenerateClassHierarchy()) {
Set<Class<?>> hierarchy = new LinkedHashSet<>();
for (ScriptInternalModule module : allModules) {
hierarchy.addAll(hierarchyCollector.collect(module, registered));
}
for (Class<?> clazz : hierarchy) {
knownClasses.put(clazz, clazz.getSimpleName());
}

classGenerator.setKnownClasses(knownClasses);
moduleWriter.getExportWriter().setHierarchyClasses(knownClasses);

if (!hierarchy.isEmpty()) {
builder.append("// --- Generated class hierarchy ---\n\n");
for (Class<?> clazz : hierarchy) {
builder.append(classGenerator.generate(clazz, "", "declare ")).append("\n\n");
}
}
} else {
classGenerator.setKnownClasses(knownClasses);
moduleWriter.getExportWriter().setHierarchyClasses(knownClasses);
}

// generate global and internal modules
if (!moduleManager.getGlobalModule().getExports().isEmpty()) {
builder.append("// --- Generated global internal module ---\n\n");
}
moduleWriter.writeGlobal(builder, moduleManager.getGlobalModule());

for (ScriptModule module : moduleManager.getModules().values()) {
if (module instanceof ScriptInternalModule internalModule) {
builder.append("// --- Generated internal module \"")
.append(internalModule.getName())
.append("\" ---\n\n");
moduleWriter.writeNamed(builder, internalModule);
}
}

return sb.toString();
return builder.toString();
}

private Set<Class<?>> collectRegisteredClasses(List<ScriptInternalModule> modules) {
Set<Class<?>> registered = new HashSet<>();
for (ScriptInternalModule module : modules) {
for (ScriptExport export : module.getExports()) {
if (export instanceof ScriptValueExport ve && ve.isClass()) {
registered.add((Class<?>) ve.getValue());
}
}
}
return registered;
}

/**
* Generates and saves the declaration as a file.
*
* @param path Save path
*/
public void save(Path path) {
try {
Files.writeString(path, this.generate(), StandardCharsets.UTF_8);
Files.writeString(path, generate(), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
Loading
Loading