Skip to content

Commit d0013a0

Browse files
committed
feat: enforce property types for DOM elements
1 parent b455f70 commit d0013a0

File tree

10 files changed

+284
-37
lines changed

10 files changed

+284
-37
lines changed

docs/examples/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<properties>
1515
<!-- Convenience property to set the GWT version -->
1616
<gwt.version>2.8.2</gwt.version>
17-
<dagger.version>2.11</dagger.version>
17+
<dagger.version>2.16</dagger.version>
1818

1919
<!-- GWT needs at least java 1.7 -->
2020
<maven.compiler.source>1.8</maven.compiler.source>

processors/src/main/java/com/axellience/vuegwt/processors/component/template/parser/TemplateParser.java

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import static com.axellience.vuegwt.processors.utils.GeneratorsNameUtil.propNameToAttributeName;
44
import static com.axellience.vuegwt.processors.utils.GeneratorsNameUtil.vModelFieldToPlaceHolderField;
5+
import static com.axellience.vuegwt.processors.utils.GeneratorsUtil.boundedAttributeToAttributeName;
6+
import static com.axellience.vuegwt.processors.utils.GeneratorsUtil.isBoundedAttribute;
57
import static com.axellience.vuegwt.processors.utils.GeneratorsUtil.stringTypeToTypeName;
68

79
import com.axellience.vuegwt.core.annotations.component.Prop;
@@ -16,6 +18,7 @@
1618
import com.axellience.vuegwt.processors.component.template.parser.variable.DestructuredPropertyInfo;
1719
import com.axellience.vuegwt.processors.component.template.parser.variable.LocalVariableInfo;
1820
import com.axellience.vuegwt.processors.component.template.parser.variable.VariableInfo;
21+
import com.axellience.vuegwt.processors.dom.DOMElementsUtil;
1922
import com.github.javaparser.JavaParser;
2023
import com.github.javaparser.ParseProblemException;
2124
import com.github.javaparser.ast.Node;
@@ -33,6 +36,7 @@
3336
import io.bit3.jsass.Output;
3437
import io.bit3.jsass.context.StringContext;
3538
import java.net.URI;
39+
import java.util.HashMap;
3640
import java.util.HashSet;
3741
import java.util.LinkedList;
3842
import java.util.List;
@@ -277,6 +281,9 @@ private boolean processElementAttributes(Element element) {
277281
);
278282
}
279283

284+
Map<String, Class<?>> elementPropertiesType = getPropertiesForDOMElement(element)
285+
.orElse(new HashMap<>());
286+
280287
// Iterate on element attributes
281288
Set<LocalComponentProp> foundProps = new HashSet<>();
282289
for (Attribute attribute : element.getAttributes()) {
@@ -303,7 +310,8 @@ private boolean processElementAttributes(Element element) {
303310

304311
currentAttribute = attribute;
305312
currentProp = optionalProp.orElse(null);
306-
currentExpressionReturnType = getExpressionReturnTypeForAttribute(attribute);
313+
currentExpressionReturnType = getExpressionReturnTypeForAttribute(attribute,
314+
elementPropertiesType);
307315
String processedExpression = processExpression(attribute.getValue());
308316

309317
if (attribute.getValueSegment() != null) {
@@ -318,22 +326,23 @@ private boolean processElementAttributes(Element element) {
318326

319327
/**
320328
* Find the corresponding TypeMirror from Elemental2 for a given DOM Element
329+
*
321330
* @param element The element we want the TypeMirror of
322331
* @return The type mirror
323332
*/
324333
private TypeMirror getTypeFromDOMElement(Element element) {
325-
String elementName = element.getStartTag().getName();
326-
elementName = elementName.substring(0, 1).toUpperCase() + elementName.substring(1);
327-
328-
// This might not work if the Tag doesn't map to the constructor
329-
TypeElement typeElement = elements
330-
.getTypeElement("elemental2.dom.HTML" + elementName + "Element");
331-
332-
if (typeElement == null) {
333-
return null;
334-
}
334+
return DOMElementsUtil
335+
.getTypeForElementTag(element.getStartTag().getName())
336+
.map(Class::getCanonicalName)
337+
.map(elements::getTypeElement)
338+
.map(TypeElement::asType)
339+
.orElse(null);
340+
}
335341

336-
return typeElement.asType();
342+
private Optional<Map<String, Class<?>>> getPropertiesForDOMElement(Element element) {
343+
return DOMElementsUtil
344+
.getTypeForElementTag(element.getStartTag().getName())
345+
.map(DOMElementsUtil::getElementProperties);
337346
}
338347

339348
/**
@@ -468,7 +477,8 @@ private void validateRequiredProps(LocalComponent localComponent,
468477
*
469478
* @param attribute The attribute the expression is in
470479
*/
471-
private TypeName getExpressionReturnTypeForAttribute(Attribute attribute) {
480+
private TypeName getExpressionReturnTypeForAttribute(Attribute attribute,
481+
Map<String, Class<?>> propertiesTypes) {
472482
String attributeName = attribute.getKey().toLowerCase();
473483

474484
if (attributeName.indexOf("@") == 0 || attributeName.indexOf("v-on:") == 0) {
@@ -479,6 +489,18 @@ private TypeName getExpressionReturnTypeForAttribute(Attribute attribute) {
479489
return TypeName.BOOLEAN;
480490
}
481491

492+
if (isBoundedAttribute(attributeName)) {
493+
String unboundedAttributeName = boundedAttributeToAttributeName(attributeName);
494+
495+
if (unboundedAttributeName.equals("class") || unboundedAttributeName.equals("style")) {
496+
return TypeName.get(Any.class);
497+
}
498+
499+
if (propertiesTypes.containsKey(unboundedAttributeName)) {
500+
return TypeName.get(propertiesTypes.get(unboundedAttributeName));
501+
}
502+
}
503+
482504
if (currentProp != null) {
483505
return currentProp.getType();
484506
}

processors/src/main/java/com/axellience/vuegwt/processors/component/template/parser/context/localcomponents/LocalComponent.java

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.axellience.vuegwt.processors.component.template.parser.context.localcomponents;
22

33
import static com.axellience.vuegwt.processors.utils.GeneratorsNameUtil.propNameToAttributeName;
4+
import static com.axellience.vuegwt.processors.utils.GeneratorsUtil.boundedAttributeToAttributeName;
45

56
import com.squareup.javapoet.TypeName;
67
import java.util.HashMap;
@@ -50,15 +51,8 @@ private Optional<LocalComponentProp> getProp(String attributeName) {
5051
return Optional.empty();
5152
}
5253

53-
public Optional<LocalComponentProp> getPropForAttribute(String attributeName) {
54-
if (attributeName.toLowerCase().startsWith("v-bind:")) {
55-
return getProp(attributeName.substring("v-bind:".length()));
56-
}
57-
if (attributeName.startsWith(":")) {
58-
return getProp(attributeName.substring(1));
59-
}
60-
61-
return getProp(attributeName);
54+
public Optional<LocalComponentProp> getPropForAttribute(String boundedAttributeName) {
55+
return getProp(boundedAttributeToAttributeName(boundedAttributeName));
6256
}
6357

6458
public Set<LocalComponentProp> getRequiredProps() {
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package com.axellience.vuegwt.processors.dom;
2+
3+
import static com.axellience.vuegwt.processors.utils.GeneratorsNameUtil.propNameToAttributeName;
4+
5+
import elemental2.dom.Element;
6+
import elemental2.dom.HTMLAnchorElement;
7+
import elemental2.dom.HTMLAreaElement;
8+
import elemental2.dom.HTMLAudioElement;
9+
import elemental2.dom.HTMLBRElement;
10+
import elemental2.dom.HTMLButtonElement;
11+
import elemental2.dom.HTMLCanvasElement;
12+
import elemental2.dom.HTMLDataListElement;
13+
import elemental2.dom.HTMLDetailsElement;
14+
import elemental2.dom.HTMLDialogElement;
15+
import elemental2.dom.HTMLDivElement;
16+
import elemental2.dom.HTMLEmbedElement;
17+
import elemental2.dom.HTMLFieldSetElement;
18+
import elemental2.dom.HTMLFormElement;
19+
import elemental2.dom.HTMLHRElement;
20+
import elemental2.dom.HTMLHeadingElement;
21+
import elemental2.dom.HTMLImageElement;
22+
import elemental2.dom.HTMLInputElement;
23+
import elemental2.dom.HTMLLIElement;
24+
import elemental2.dom.HTMLLabelElement;
25+
import elemental2.dom.HTMLLegendElement;
26+
import elemental2.dom.HTMLMapElement;
27+
import elemental2.dom.HTMLMenuElement;
28+
import elemental2.dom.HTMLMenuItemElement;
29+
import elemental2.dom.HTMLMeterElement;
30+
import elemental2.dom.HTMLOListElement;
31+
import elemental2.dom.HTMLObjectElement;
32+
import elemental2.dom.HTMLOptGroupElement;
33+
import elemental2.dom.HTMLOptionElement;
34+
import elemental2.dom.HTMLOutputElement;
35+
import elemental2.dom.HTMLParagraphElement;
36+
import elemental2.dom.HTMLParamElement;
37+
import elemental2.dom.HTMLPreElement;
38+
import elemental2.dom.HTMLProgressElement;
39+
import elemental2.dom.HTMLQuoteElement;
40+
import elemental2.dom.HTMLScriptElement;
41+
import elemental2.dom.HTMLSelectElement;
42+
import elemental2.dom.HTMLSourceElement;
43+
import elemental2.dom.HTMLTableCaptionElement;
44+
import elemental2.dom.HTMLTableCellElement;
45+
import elemental2.dom.HTMLTableColElement;
46+
import elemental2.dom.HTMLTableElement;
47+
import elemental2.dom.HTMLTableRowElement;
48+
import elemental2.dom.HTMLTextAreaElement;
49+
import elemental2.dom.HTMLTrackElement;
50+
import elemental2.dom.HTMLUListElement;
51+
import elemental2.dom.HTMLVideoElement;
52+
import java.lang.reflect.Field;
53+
import java.lang.reflect.Modifier;
54+
import java.util.HashMap;
55+
import java.util.Map;
56+
import java.util.Optional;
57+
58+
public class DOMElementsUtil {
59+
60+
// Taken from Elemento: https://github.com/hal/elemento/blob/develop/template-processor/src/main/java/org/jboss/gwt/elemento/processor/TemplatedProcessor.java#L149
61+
// List of elements from https://developer.mozilla.org/en-US/docs/Web/HTML/Element
62+
// which have a class in elemental2.dom, are standardized and not obsolete or deprecated
63+
private static final Map<String, Class<? extends Element>> HTML_ELEMENTS = new HashMap<>();
64+
65+
private static final Map<Class<? extends Element>, Map<String, Class<?>>> HTML_ELEMENTS_PROPERTIES_CACHE = new HashMap<>();
66+
67+
static {
68+
HTML_ELEMENTS.put("a", HTMLAnchorElement.class);
69+
HTML_ELEMENTS.put("area", HTMLAreaElement.class);
70+
HTML_ELEMENTS.put("audio", HTMLAudioElement.class);
71+
HTML_ELEMENTS.put("blockquote", HTMLQuoteElement.class);
72+
HTML_ELEMENTS.put("br", HTMLBRElement.class);
73+
HTML_ELEMENTS.put("button", HTMLButtonElement.class);
74+
HTML_ELEMENTS.put("canvas", HTMLCanvasElement.class);
75+
HTML_ELEMENTS.put("caption", HTMLTableCaptionElement.class);
76+
HTML_ELEMENTS.put("col", HTMLTableColElement.class);
77+
HTML_ELEMENTS.put("datalist", HTMLDataListElement.class);
78+
HTML_ELEMENTS.put("details", HTMLDetailsElement.class);
79+
HTML_ELEMENTS.put("dialog", HTMLDialogElement.class);
80+
HTML_ELEMENTS.put("div", HTMLDivElement.class);
81+
HTML_ELEMENTS.put("embed", HTMLEmbedElement.class);
82+
HTML_ELEMENTS.put("fieldset", HTMLFieldSetElement.class);
83+
HTML_ELEMENTS.put("form", HTMLFormElement.class);
84+
HTML_ELEMENTS.put("h1", HTMLHeadingElement.class);
85+
HTML_ELEMENTS.put("h2", HTMLHeadingElement.class);
86+
HTML_ELEMENTS.put("h3", HTMLHeadingElement.class);
87+
HTML_ELEMENTS.put("h4", HTMLHeadingElement.class);
88+
HTML_ELEMENTS.put("h5", HTMLHeadingElement.class);
89+
HTML_ELEMENTS.put("h6", HTMLHeadingElement.class);
90+
HTML_ELEMENTS.put("hr", HTMLHRElement.class);
91+
HTML_ELEMENTS.put("img", HTMLImageElement.class);
92+
HTML_ELEMENTS.put("input", HTMLInputElement.class);
93+
HTML_ELEMENTS.put("label", HTMLLabelElement.class);
94+
HTML_ELEMENTS.put("legend", HTMLLegendElement.class);
95+
HTML_ELEMENTS.put("li", HTMLLIElement.class);
96+
HTML_ELEMENTS.put("map", HTMLMapElement.class);
97+
HTML_ELEMENTS.put("menu", HTMLMenuElement.class);
98+
HTML_ELEMENTS.put("menuitem", HTMLMenuItemElement.class);
99+
HTML_ELEMENTS.put("meter", HTMLMeterElement.class);
100+
HTML_ELEMENTS.put("object", HTMLObjectElement.class);
101+
HTML_ELEMENTS.put("ol", HTMLOListElement.class);
102+
HTML_ELEMENTS.put("optgroup", HTMLOptGroupElement.class);
103+
HTML_ELEMENTS.put("option", HTMLOptionElement.class);
104+
HTML_ELEMENTS.put("output", HTMLOutputElement.class);
105+
HTML_ELEMENTS.put("p", HTMLParagraphElement.class);
106+
HTML_ELEMENTS.put("param", HTMLParamElement.class);
107+
HTML_ELEMENTS.put("pre", HTMLPreElement.class);
108+
HTML_ELEMENTS.put("progress", HTMLProgressElement.class);
109+
HTML_ELEMENTS.put("q", HTMLQuoteElement.class);
110+
HTML_ELEMENTS.put("script", HTMLScriptElement.class);
111+
HTML_ELEMENTS.put("select", HTMLSelectElement.class);
112+
HTML_ELEMENTS.put("source", HTMLSourceElement.class);
113+
HTML_ELEMENTS.put("table", HTMLTableElement.class);
114+
HTML_ELEMENTS.put("td", HTMLTableCellElement.class);
115+
HTML_ELEMENTS.put("textarea", HTMLTextAreaElement.class);
116+
HTML_ELEMENTS.put("tr", HTMLTableRowElement.class);
117+
HTML_ELEMENTS.put("track", HTMLTrackElement.class);
118+
HTML_ELEMENTS.put("ul", HTMLUListElement.class);
119+
HTML_ELEMENTS.put("video", HTMLVideoElement.class);
120+
}
121+
122+
public static Optional<Class<? extends Element>> getTypeForElementTag(String elementTag) {
123+
return Optional.ofNullable(HTML_ELEMENTS.get(elementTag));
124+
}
125+
126+
public static <T extends Element> Map<String, Class<?>> getElementProperties(
127+
Class<T> elementClass) {
128+
if (HTML_ELEMENTS_PROPERTIES_CACHE.containsKey(elementClass)) {
129+
return HTML_ELEMENTS_PROPERTIES_CACHE.get(elementClass);
130+
}
131+
132+
Map<String, Class<?>> elementProperties = new HashMap<>();
133+
for (Field field : elementClass.getFields()) {
134+
if (!Modifier.isPublic(field.getModifiers())) {
135+
continue;
136+
}
137+
138+
elementProperties.put(propNameToAttributeName(field.getName()), field.getType());
139+
}
140+
141+
HTML_ELEMENTS_PROPERTIES_CACHE.put(elementClass, elementProperties);
142+
143+
return elementProperties;
144+
}
145+
}

processors/src/main/java/com/axellience/vuegwt/processors/utils/GeneratorsUtil.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,4 +193,22 @@ public static String getFieldMarkingValueForType(TypeName fieldType) {
193193

194194
return "null";
195195
}
196+
197+
public static boolean isBoundedAttribute(String attributeName) {
198+
if (attributeName.toLowerCase().startsWith("v-bind:")) {
199+
return true;
200+
}
201+
202+
return attributeName.startsWith(":");
203+
}
204+
205+
public static String boundedAttributeToAttributeName(String boundedAttributeName) {
206+
if (boundedAttributeName.toLowerCase().startsWith("v-bind:")) {
207+
boundedAttributeName = boundedAttributeName.substring("v-bind:".length());
208+
} else if (boundedAttributeName.startsWith(":")) {
209+
boundedAttributeName = boundedAttributeName.substring(1);
210+
}
211+
212+
return boundedAttributeName;
213+
}
196214
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.axellience.vuegwt.processors.component.propertybinding;
2+
3+
import static com.google.testing.compile.CompilationSubject.assertThat;
4+
import static com.google.testing.compile.Compiler.javac;
5+
6+
import com.axellience.vuegwt.processors.VueGwtProcessor;
7+
import com.google.testing.compile.Compilation;
8+
import com.google.testing.compile.JavaFileObjects;
9+
import org.junit.jupiter.api.DisplayName;
10+
import org.junit.jupiter.api.Test;
11+
12+
class PropertyBindingTest {
13+
14+
@Test
15+
@DisplayName("should enforce types for DOM element property binding")
16+
void propsAreRegisteredInOptionsCorrectly() {
17+
Compilation compilation =
18+
javac()
19+
.withProcessors(new VueGwtProcessor())
20+
.compile(JavaFileObjects.forResource("propertybinding/PropertyBindingComponent.java"));
21+
22+
assertThat(compilation)
23+
.generatedSourceFile("propertybinding.PropertyBindingComponentExposedType")
24+
.containsElementsIn(JavaFileObjects.forResource(
25+
"propertybinding/compileresult/PropertyBindingComponentExposedType.java"));
26+
}
27+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<div>
2+
<input type="text" :required="true"/>
3+
<div :id='"MyID"'></div>
4+
<input :max-length='10' :non-existing-property="15"/>
5+
</div>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package propertybinding;
2+
3+
import com.axellience.vuegwt.core.annotations.component.Component;
4+
import com.axellience.vuegwt.core.client.component.IsVueComponent;
5+
6+
@Component
7+
public class PropertyBindingComponent implements IsVueComponent {
8+
9+
}

0 commit comments

Comments
 (0)