org.teavm.jso.impl.JSAliasRenderer Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2015 Alexey Andreev.
*
* 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.teavm.jso.impl;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Predicate;
import org.teavm.backend.javascript.codegen.SourceWriter;
import org.teavm.backend.javascript.rendering.RenderingManager;
import org.teavm.backend.javascript.spi.MethodContributor;
import org.teavm.backend.javascript.spi.MethodContributorContext;
import org.teavm.jso.JSClass;
import org.teavm.model.AnnotationReader;
import org.teavm.model.ClassReader;
import org.teavm.model.ElementModifier;
import org.teavm.model.FieldReader;
import org.teavm.model.FieldReference;
import org.teavm.model.ListableClassReaderSource;
import org.teavm.model.MethodDescriptor;
import org.teavm.model.MethodReader;
import org.teavm.model.MethodReference;
import org.teavm.vm.BuildTarget;
import org.teavm.vm.spi.RendererListener;
class JSAliasRenderer implements RendererListener, MethodContributor {
private static String variableChars = "abcdefghijklmnopqrstuvwxyz";
private SourceWriter writer;
private ListableClassReaderSource classSource;
private JSTypeHelper typeHelper;
private RenderingManager context;
private int lastExportIndex;
@Override
public void begin(RenderingManager context, BuildTarget buildTarget) {
writer = context.getWriter();
classSource = context.getClassSource();
typeHelper = new JSTypeHelper(context.getClassSource());
this.context = context;
}
@Override
public void complete() {
exportClasses();
exportModule();
}
private void exportClasses() {
if (!hasClassesToExpose()) {
return;
}
writer.startVariableDeclaration().appendFunction("$rt_jso_marker")
.appendGlobal("Symbol").append("('jsoClass')").endDeclaration();
writer.append("(()").sameLineWs().append("=>").ws().append("{").softNewLine().indent();
writer.append("let c;").softNewLine();
var exportedNamesByClass = new HashMap();
for (var className : classSource.getClassNames()) {
var classReader = classSource.get(className);
var hasExportedMembers = false;
hasExportedMembers |= exportClassInstanceMembers(classReader);
if (!className.equals(context.getEntryPoint())) {
var name = "$rt_export_class_ " + getClassAliasName(classReader) + "_" + lastExportIndex++;
hasExportedMembers |= exportClassStaticMembers(classReader, name);
if (hasExportedMembers) {
exportedNamesByClass.put(className, name);
}
}
}
writer.outdent().append("})();").newLine();
for (var className : classSource.getClassNames()) {
var classReader = classSource.get(className);
var name = exportedNamesByClass.get(className);
if (name != null && !typeHelper.isJavaScriptClass(className)
&& !typeHelper.isJavaScriptImplementation(className)) {
exportClassFromModule(classReader, name);
}
}
}
private boolean exportClassInstanceMembers(ClassReader classReader) {
var members = collectMembers(classReader, method -> !method.hasModifier(ElementModifier.STATIC));
var isJsClassImpl = typeHelper.isJavaScriptImplementation(classReader.getName());
if (members.methods.isEmpty() && members.properties.isEmpty() && !isJsClassImpl) {
return false;
}
writer.append("c").ws().append("=").ws().appendClass(classReader.getName()).append(".prototype;")
.softNewLine();
if (isJsClassImpl) {
writer.append("c[").appendFunction("$rt_jso_marker").append("]").ws().append("=").ws().append("true;")
.softNewLine();
}
for (var aliasEntry : members.methods.entrySet()) {
if (classReader.getMethod(aliasEntry.getValue()) == null) {
continue;
}
appendMethodAlias(aliasEntry.getKey());
writer.ws().append("=").ws().append("c.").appendVirtualMethod(aliasEntry.getValue())
.append(";").softNewLine();
}
for (var aliasEntry : members.properties.entrySet()) {
var propInfo = aliasEntry.getValue();
if (propInfo.getter == null || classReader.getMethod(propInfo.getter) == null) {
continue;
}
appendPropertyAlias(aliasEntry.getKey());
writer.append("get:").ws().append("c.").appendVirtualMethod(propInfo.getter);
if (propInfo.setter != null && classReader.getMethod(propInfo.setter) != null) {
writer.append(",").softNewLine();
writer.append("set:").ws().append("c.").appendVirtualMethod(propInfo.setter);
}
writer.softNewLine().outdent().append("});").softNewLine();
}
var functorField = getFunctorField(classReader);
if (functorField != null) {
writeFunctor(classReader, functorField.getReference());
}
return true;
}
private boolean exportClassStaticMembers(ClassReader classReader, String name) {
var members = collectMembers(classReader, c -> c.hasModifier(ElementModifier.STATIC));
if (members.methods.isEmpty() && members.properties.isEmpty()) {
return false;
}
writer.append("c").ws().append("=").ws().appendFunction(name).append(";").softNewLine();
for (var aliasEntry : members.methods.entrySet()) {
appendMethodAlias(aliasEntry.getKey());
var fullRef = new MethodReference(classReader.getName(), aliasEntry.getValue());
writer.ws().append("=").ws().appendMethod(fullRef).append(";").softNewLine();
}
for (var aliasEntry : members.properties.entrySet()) {
var propInfo = aliasEntry.getValue();
if (propInfo.getter == null) {
continue;
}
appendPropertyAlias(aliasEntry.getKey());
var fullGetter = new MethodReference(classReader.getName(), propInfo.getter);
writer.append("get:").ws().appendMethod(fullGetter);
if (propInfo.setter != null) {
writer.append(",").softNewLine();
var fullSetter = new MethodReference(classReader.getName(), propInfo.setter);
writer.append("set:").ws().appendMethod(fullSetter);
}
writer.softNewLine().outdent().append("});").softNewLine();
}
return true;
}
private void appendMethodAlias(String name) {
if (isKeyword(name)) {
writer.append("c[\"").append(name).append("\"]");
} else {
writer.append("c.").append(name);
}
}
private void appendPropertyAlias(String name) {
writer.append("Object.defineProperty(c,")
.ws().append("\"").append(name).append("\",")
.ws().append("{").indent().softNewLine();
}
private Members collectMembers(ClassReader classReader, Predicate filter) {
var methods = new HashMap();
var properties = new HashMap();
MethodDescriptor constructor = null;
for (var method : classReader.getMethods()) {
if (!filter.test(method)) {
continue;
}
var methodAlias = getPublicAlias(method);
if (methodAlias != null) {
switch (methodAlias.kind) {
case METHOD:
methods.put(methodAlias.name, method.getDescriptor());
break;
case GETTER: {
var propInfo = properties.computeIfAbsent(methodAlias.name, k -> new PropertyInfo());
propInfo.getter = method.getDescriptor();
break;
}
case SETTER: {
var propInfo = properties.computeIfAbsent(methodAlias.name, k -> new PropertyInfo());
propInfo.setter = method.getDescriptor();
break;
}
case CONSTRUCTOR:
constructor = method.getDescriptor();
break;
}
}
}
return new Members(methods, properties, constructor);
}
private void exportModule() {
var cls = classSource.get(context.getEntryPoint());
for (var method : cls.getMethods()) {
if (!method.hasModifier(ElementModifier.STATIC)) {
continue;
}
var methodAlias = getPublicAlias(method);
if (methodAlias != null && methodAlias.kind == AliasKind.METHOD) {
context.exportMethod(method.getReference(), methodAlias.name);
}
}
}
private void exportClassFromModule(ClassReader cls, String functionName) {
var name = getClassAliasName(cls);
var constructors = collectMembers(cls, method -> !method.hasModifier(ElementModifier.STATIC));
var method = constructors.constructor;
writer.append("function ").appendFunction(functionName).append("(");
if (method != null) {
for (var i = 0; i < method.parameterCount(); ++i) {
if (i > 0) {
writer.append(",").ws();
}
writer.append("p" + i);
}
}
writer.append(")").ws().appendBlockStart();
if (method != null) {
writer.appendClass(cls.getName()).append(".call(this);").softNewLine();
writer.appendMethod(new MethodReference(cls.getName(), method)).append("(this");
for (var i = 0; i < method.parameterCount(); ++i) {
writer.append(",").ws().append("p" + i);
}
writer.append(");").softNewLine();
} else {
writer.append("throw new Error(\"Can't instantiate this class directly\");").softNewLine();
}
writer.outdent().append("}").append(";").softNewLine();
writer.appendFunction(functionName).append(".prototype").ws().append("=").ws()
.appendClass(cls.getName()).append(".prototype;").softNewLine();
context.exportFunction(functionName, name);
}
private String getClassAliasName(ClassReader cls) {
var name = cls.getSimpleName();
if (name == null) {
name = cls.getName().substring(cls.getName().lastIndexOf('.') + 1);
}
var jsExport = cls.getAnnotations().get(JSClass.class.getName());
if (jsExport != null) {
var nameValue = jsExport.getValue("name");
if (nameValue != null) {
var nameValueString = nameValue.getString();
if (!nameValueString.isEmpty()) {
name = nameValueString;
}
}
}
return name;
}
private boolean hasClassesToExpose() {
for (String className : classSource.getClassNames()) {
ClassReader cls = classSource.get(className);
if (typeHelper.isJavaScriptImplementation(className)) {
return true;
}
for (var method : cls.getMethods()) {
if (getPublicAlias(method) != null) {
return true;
}
}
}
return false;
}
private Alias getPublicAlias(MethodReader method) {
var annot = method.getAnnotations().get(JSMethodToExpose.class.getName());
if (annot != null) {
return new Alias(annot.getValue("name").getString(), AliasKind.METHOD);
}
annot = method.getAnnotations().get(JSGetterToExpose.class.getName());
if (annot != null) {
return new Alias(annot.getValue("name").getString(), AliasKind.GETTER);
}
annot = method.getAnnotations().get(JSSetterToExpose.class.getName());
if (annot != null) {
return new Alias(annot.getValue("name").getString(), AliasKind.SETTER);
}
annot = method.getAnnotations().get(JSConstructorToExpose.class.getName());
if (annot != null) {
return new Alias(null, AliasKind.CONSTRUCTOR);
}
return null;
}
private FieldReader getFunctorField(ClassReader cls) {
return cls.getField("$$jso_functor$$");
}
private boolean isKeyword(String id) {
switch (id) {
case "with":
case "delete":
case "in":
case "undefined":
case "debugger":
case "export":
case "function":
case "let":
case "var":
case "typeof":
case "yield":
return true;
default:
return false;
}
}
private void writeFunctor(ClassReader cls, FieldReference functorField) {
AnnotationReader implAnnot = cls.getAnnotations().get(FunctorImpl.class.getName());
MethodDescriptor functorMethod = MethodDescriptor.parse(implAnnot.getValue("value").getString());
String alias = cls.getMethod(functorMethod).getAnnotations()
.get(JSMethodToExpose.class.getName()).getValue("name").getString();
if (alias == null) {
return;
}
writer.append("c.jso$functor$").append(alias).ws().append("=").ws().append("function()").ws().append("{")
.indent().softNewLine();
writer.append("if").ws().append("(!this.").appendField(functorField).append(")").ws().append("{")
.indent().softNewLine();
writer.append("var self").ws().append('=').ws().append("this;").softNewLine();
writer.append("this.").appendField(functorField).ws().append('=').ws().append("function(");
appendArguments(functorMethod.parameterCount());
writer.append(")").ws().append('{').indent().softNewLine();
writer.append("return self.").appendVirtualMethod(functorMethod).append('(');
appendArguments(functorMethod.parameterCount());
writer.append(");").softNewLine();
writer.outdent().append("};").softNewLine();
writer.outdent().append("}").softNewLine();
writer.append("return this.").appendField(functorField).append(';').softNewLine();
writer.outdent().append("};").softNewLine();
}
private void appendArguments(int count) {
for (int i = 0; i < count; ++i) {
if (i > 0) {
writer.append(',').ws();
}
writer.append(variableName(i));
}
}
private String variableName(int index) {
StringBuilder sb = new StringBuilder();
sb.append(variableChars.charAt(index % variableChars.length()));
index /= variableChars.length();
if (index > 0) {
sb.append(index);
}
return sb.toString();
}
@Override
public boolean isContributing(MethodContributorContext context, MethodReference methodRef) {
ClassReader classReader = context.getClassSource().get(methodRef.getClassName());
if (classReader == null) {
return false;
}
if (getFunctorField(classReader) != null) {
return true;
}
MethodReader methodReader = classReader.getMethod(methodRef.getDescriptor());
return methodReader != null && getPublicAlias(methodReader) != null;
}
private static class Members {
final Map methods;
final Map properties;
final MethodDescriptor constructor;
Members(Map methods, Map properties,
MethodDescriptor constructor) {
this.methods = methods;
this.properties = properties;
this.constructor = constructor;
}
}
private static class PropertyInfo {
MethodDescriptor getter;
MethodDescriptor setter;
}
private static class Alias {
final String name;
final AliasKind kind;
Alias(String name, AliasKind kind) {
this.name = name;
this.kind = kind;
}
}
private enum AliasKind {
METHOD,
GETTER,
SETTER,
CONSTRUCTOR
}
}