diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSAliasRenderer.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSAliasRenderer.java index 591505801..1824fa399 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSAliasRenderer.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSAliasRenderer.java @@ -17,7 +17,6 @@ package org.teavm.jso.impl; import java.io.IOException; import java.util.HashMap; -import java.util.Map; import org.teavm.backend.javascript.codegen.SourceWriter; import org.teavm.backend.javascript.rendering.RenderingManager; import org.teavm.backend.javascript.spi.VirtualMethodContributor; @@ -54,20 +53,35 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor { writer.append("var c;").softNewLine(); for (String className : classSource.getClassNames()) { ClassReader classReader = classSource.get(className); - Map methods = new HashMap<>(); - for (MethodReader method : classReader.getMethods()) { - String methodAlias = getPublicAlias(method); + var methods = new HashMap(); + var properties = new HashMap(); + for (var method : classReader.getMethods()) { + var methodAlias = getPublicAlias(method); if (methodAlias != null) { - methods.put(method.getDescriptor(), methodAlias); + 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; + } + } } } - if (methods.isEmpty()) { + if (methods.isEmpty() && properties.isEmpty()) { continue; } boolean first = true; - for (Map.Entry aliasEntry : methods.entrySet()) { - if (classReader.getMethod(aliasEntry.getKey()) == null) { + for (var aliasEntry : methods.entrySet()) { + if (classReader.getMethod(aliasEntry.getValue()) == null) { continue; } if (first) { @@ -75,12 +89,33 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor { .softNewLine(); first = false; } - if (isKeyword(aliasEntry.getValue())) { - writer.append("c[\"").append(aliasEntry.getValue()).append("\"]"); + if (isKeyword(aliasEntry.getKey())) { + writer.append("c[\"").append(aliasEntry.getKey()).append("\"]"); } else { - writer.append("c.").append(aliasEntry.getValue()); + writer.append("c.").append(aliasEntry.getKey()); } - writer.ws().append("=").ws().append("c.").appendMethod(aliasEntry.getKey()).append(";").softNewLine(); + writer.ws().append("=").ws().append("c.").appendMethod(aliasEntry.getValue()) + .append(";").softNewLine(); + } + for (var aliasEntry : properties.entrySet()) { + var propInfo = aliasEntry.getValue(); + if (propInfo.getter == null || classReader.getMethod(propInfo.getter) == null) { + continue; + } + if (first) { + writer.append("c").ws().append("=").ws().appendClass(className).append(".prototype;") + .softNewLine(); + first = false; + } + writer.append("Object.defineProperty(c,") + .ws().append("\"").append(aliasEntry.getKey()).append("\",") + .ws().append("{").indent().softNewLine(); + writer.append("get:").ws().append("c.").appendMethod(propInfo.getter); + if (propInfo.setter != null && classReader.getMethod(propInfo.setter) != null) { + writer.append(",").softNewLine(); + writer.append("set:").ws().append("c.").appendMethod(propInfo.setter); + } + writer.softNewLine().outdent().append("});").softNewLine(); } FieldReader functorField = getFunctorField(classReader); @@ -101,9 +136,23 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor { return false; } - private String getPublicAlias(MethodReader method) { - AnnotationReader annot = method.getAnnotations().get(JSMethodToExpose.class.getName()); - return annot != null ? annot.getValue("name").getString() : null; + 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); + } + + return null; } private FieldReader getFunctorField(ClassReader cls) { @@ -190,4 +239,25 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor { MethodReader methodReader = classReader.getMethod(methodRef.getDescriptor()); return methodReader != null && getPublicAlias(methodReader) != null; } + + static class PropertyInfo { + MethodDescriptor getter; + MethodDescriptor setter; + } + + static class Alias { + final String name; + final AliasKind kind; + + Alias(String name, AliasKind kind) { + this.name = name; + this.kind = kind; + } + } + + enum AliasKind { + METHOD, + GETTER, + SETTER + } } diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSDependencyListener.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSDependencyListener.java index b8874ccba..90f6c79c9 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSDependencyListener.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSDependencyListener.java @@ -48,6 +48,12 @@ class JSDependencyListener extends AbstractDependencyListener { ClassReader cls = agent.getClassSource().get(className); for (MethodReader method : cls.getMethods()) { AnnotationReader exposeAnnot = method.getAnnotations().get(JSMethodToExpose.class.getName()); + if (exposeAnnot == null) { + exposeAnnot = method.getAnnotations().get(JSGetterToExpose.class.getName()); + } + if (exposeAnnot == null) { + exposeAnnot = method.getAnnotations().get(JSSetterToExpose.class.getName()); + } if (exposeAnnot != null) { MethodDependency methodDep = agent.linkMethod(method.getReference()); methodDep.getVariable(0).propagate(agent.getType(className)); diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSGetterToExpose.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSGetterToExpose.java new file mode 100644 index 000000000..65c3adf27 --- /dev/null +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSGetterToExpose.java @@ -0,0 +1,27 @@ +/* + * Copyright 2017 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.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.METHOD) +@interface JSGetterToExpose { + String name(); +} diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSObjectClassTransformer.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSObjectClassTransformer.java index 53de5b5c2..c8e45f009 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSObjectClassTransformer.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSObjectClassTransformer.java @@ -25,6 +25,7 @@ import java.util.Set; import org.teavm.diagnostics.Diagnostics; import org.teavm.jso.JSMethod; import org.teavm.jso.JSObject; +import org.teavm.jso.JSProperty; import org.teavm.model.AccessLevel; import org.teavm.model.AnnotationHolder; import org.teavm.model.AnnotationReader; @@ -154,9 +155,21 @@ class JSObjectClassTransformer implements ClassHolderTransformer { classHolder.addMethod(exportedMethod); - String publicAlias = classToExpose.methods.get(method); - AnnotationHolder annot = new AnnotationHolder(JSMethodToExpose.class.getName()); - annot.getValues().put("name", new AnnotationValue(publicAlias)); + var export = classToExpose.methods.get(method); + String annotationName; + switch (export.kind) { + case GETTER: + annotationName = JSGetterToExpose.class.getName(); + break; + case SETTER: + annotationName = JSSetterToExpose.class.getName(); + break; + default: + annotationName = JSMethodToExpose.class.getName(); + break; + } + AnnotationHolder annot = new AnnotationHolder(annotationName); + annot.getValues().put("name", new AnnotationValue(export.alias)); exportedMethod.getAnnotations().add(annot); if (methodRef.equals(functorMethod)) { @@ -189,8 +202,8 @@ class JSObjectClassTransformer implements ClassHolderTransformer { } if (cls.getParent() != null) { ExposedClass parent = getExposedClass(cls.getParent()); - exposedCls.inheritedMethods.putAll(parent.inheritedMethods); - exposedCls.inheritedMethods.putAll(parent.methods); + exposedCls.inheritedMethods.addAll(parent.inheritedMethods); + exposedCls.inheritedMethods.addAll(parent.methods.keySet()); exposedCls.implementedInterfaces.addAll(parent.implementedInterfaces); } addInterfaces(exposedCls, cls); @@ -213,10 +226,12 @@ class JSObjectClassTransformer implements ClassHolderTransformer { || (method.getProgram() != null && method.getProgram().basicBlockCount() > 0)) { continue; } - if (!exposedCls.inheritedMethods.containsKey(method.getDescriptor())) { - String name = method.getName(); + if (!exposedCls.inheritedMethods.contains(method.getDescriptor())) { + String name = null; + MethodKind kind = MethodKind.METHOD; AnnotationReader methodAnnot = method.getAnnotations().get(JSMethod.class.getName()); if (methodAnnot != null) { + name = method.getName(); AnnotationValue nameVal = methodAnnot.getValue("value"); if (nameVal != null) { String nameStr = nameVal.getString(); @@ -224,8 +239,43 @@ class JSObjectClassTransformer implements ClassHolderTransformer { name = nameStr; } } + } else { + var propertyAnnot = method.getAnnotations().get(JSProperty.class.getName()); + if (propertyAnnot != null) { + AnnotationValue nameVal = propertyAnnot.getValue("value"); + if (nameVal != null) { + String nameStr = nameVal.getString(); + if (!nameStr.isEmpty()) { + name = nameStr; + } + } + String expectedPrefix; + if (method.parameterCount() == 0) { + if (method.getResultType() == ValueType.BOOLEAN) { + expectedPrefix = "is"; + } else { + expectedPrefix = "get"; + } + kind = MethodKind.GETTER; + } else { + expectedPrefix = "set"; + kind = MethodKind.SETTER; + } + + if (name == null) { + name = method.getName(); + if (name.startsWith(expectedPrefix) && name.length() > expectedPrefix.length() + && Character.isUpperCase(name.charAt(expectedPrefix.length()))) { + name = Character.toLowerCase(name.charAt(expectedPrefix.length())) + + name.substring(expectedPrefix.length() + 1); + } + } + } } - exposedCls.methods.put(method.getDescriptor(), name); + if (name == null) { + name = method.getName(); + } + exposedCls.methods.put(method.getDescriptor(), new MethodExport(name, kind)); } } } @@ -256,8 +306,24 @@ class JSObjectClassTransformer implements ClassHolderTransformer { } static class ExposedClass { - Map inheritedMethods = new HashMap<>(); - Map methods = new HashMap<>(); + Set inheritedMethods = new HashSet<>(); + Map methods = new HashMap<>(); Set implementedInterfaces = new HashSet<>(); } + + enum MethodKind { + METHOD, + GETTER, + SETTER + } + + static class MethodExport { + final String alias; + final MethodKind kind; + + MethodExport(String alias, MethodKind kind) { + this.alias = alias; + this.kind = kind; + } + } } diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSSetterToExpose.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSSetterToExpose.java new file mode 100644 index 000000000..f64c71833 --- /dev/null +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSSetterToExpose.java @@ -0,0 +1,27 @@ +/* + * Copyright 2017 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.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.METHOD) +@interface JSSetterToExpose { + String name(); +} diff --git a/tests/src/test/java/org/teavm/jso/test/ExportClass.java b/tests/src/test/java/org/teavm/jso/test/ExportClass.java index 697fb328b..d44f87367 100644 --- a/tests/src/test/java/org/teavm/jso/test/ExportClass.java +++ b/tests/src/test/java/org/teavm/jso/test/ExportClass.java @@ -20,10 +20,13 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.teavm.jso.JSBody; import org.teavm.jso.JSObject; +import org.teavm.jso.JSProperty; import org.teavm.junit.SkipJVM; import org.teavm.junit.TeaVMTestRunner; +import org.teavm.junit.WholeClassCompilation; @RunWith(TeaVMTestRunner.class) +@WholeClassCompilation @SkipJVM public class ExportClass { @Test @@ -32,9 +35,24 @@ public class ExportClass { assertEquals("[OK]", callIFromJs(new DerivedSimpleClass())); } + @Test + public void classWithPropertiesExported() { + var o = new ClassWithProperty("q"); + assertEquals("q", extractFoo(o)); + + setFoo(o); + assertEquals("w", o.fooValue); + } + @JSBody(params = "a", script = "return a.foo('OK');") private static native String callIFromJs(I a); + @JSBody(params = "a", script = "return a.foo;") + private static native String extractFoo(J a); + + @JSBody(params = "a", script = "a.foo = 'w';") + private static native String setFoo(J a); + interface I extends JSObject { String foo(String a); } @@ -53,4 +71,29 @@ public class ExportClass { } } + interface J extends JSObject { + @JSProperty + String getFoo(); + + @JSProperty + void setFoo(String value); + } + + static class ClassWithProperty implements J { + String fooValue; + + ClassWithProperty(String fooValue) { + this.fooValue = fooValue; + } + + @Override + public String getFoo() { + return fooValue; + } + + @Override + public void setFoo(String value) { + fooValue = value; + } + } }