Javafx: Fixed resolving variables and getter chains in FXML property expressions. Tests added (IDEA-153754)

This commit is contained in:
Pavel Dolgov
2016-03-30 20:04:28 +03:00
parent ec759dcab2
commit 77b5d5469c
11 changed files with 207 additions and 150 deletions
@@ -276,6 +276,18 @@ public class JavaFXHighlightingTest extends AbstractJavaFXTestCase {
doTest("s1.js");
}
public void testPropertyNameExpression() throws Exception {
doTest();
}
public void testPropertyChainExpression() throws Exception {
doTest();
}
public void testIncorrectPropertyExpressionSyntax() throws Exception {
doTest();
}
private void doTest(String additionalPath) {
myFixture.configureByFiles(getTestName(true) + ".fxml", additionalPath);
myFixture.testHighlighting(false, false, false, getTestName(true) + ".fxml");
@@ -252,19 +252,6 @@ public class JavaFxCompletionTest extends LightFixtureCompletionTestCase {
assertSameElements(myFixture.getLookupElementStrings(),"pane", "node", "box", "model", "text", "target");
}
public void testVariableCompletionBooleanFirst() throws Exception {
doOrderTest("zAssignable", "dConvertible", "tConvertible", "controller", "mUnknown");
}
public void testVariableCompletionTooltipFirst() throws Exception {
doOrderTest("tAssignable", "controller", "mUnknown", "dIncompatible");
}
private void doOrderTest(String... expected) {
configureAndComplete();
assertOrderedEquals(myFixture.getLookupElementStrings(), expected);
}
private void configureAndComplete(final String... extraFiles) {
final String fxmlFileName = getTestName(true) + ".fxml";
if (extraFiles.length != 0) {
@@ -361,8 +361,20 @@ public class JavaFxPsiUtil {
return false;
}
public static boolean isExpressionBinding(String value) {
return value.startsWith("${") && value.endsWith("}") && value.contains(".");
public static boolean isExpressionBinding(@Nullable String value) {
return value != null && value.startsWith("${") && value.endsWith("}");
}
public static boolean isIncorrectExpressionBinding(@Nullable String value) {
if (value == null || !value.startsWith("$")) return false;
if (value.length() == 1) return true;
final boolean expressionStarts = value.startsWith("${");
final boolean expressionEnds = value.endsWith("}");
if (expressionStarts && expressionEnds && value.length() == 3) return true;
if (expressionStarts != expressionEnds) return true;
if (expressionStarts && value.indexOf('{', 2) >= 2) return true;
if (expressionEnds && value.indexOf('}') < value.length() - 1) return true;
return false;
}
@Nullable
@@ -697,7 +709,7 @@ public class JavaFxPsiUtil {
@Nullable
public static PsiType getReadablePropertyType(PsiElement declaration) {
public static PsiType getReadablePropertyType(@Nullable PsiElement declaration) {
if (declaration instanceof PsiField) {
return getWrappedPropertyType((PsiField)declaration, declaration.getProject(), JavaFxCommonNames.ourReadOnlyMap);
}
@@ -751,7 +763,7 @@ public class JavaFxPsiUtil {
}
@Nullable
public static PsiClass getTagClassById(String id, PsiElement context, XmlAttributeValue xmlAttributeValue) {
public static PsiClass getTagClassById(@Nullable XmlAttributeValue xmlAttributeValue, @Nullable String id, @NotNull PsiElement context) {
return FxmlConstants.CONTROLLER.equals(id) ? getControllerClass(context.getContainingFile()) : getTagClass(xmlAttributeValue);
}
@@ -768,7 +780,7 @@ public class JavaFxPsiUtil {
}
@Nullable
public static PsiClass getPropertyClass(PsiType propertyType, PsiElement context) {
public static PsiClass getPropertyClass(@Nullable PsiType propertyType, @NotNull PsiElement context) {
if (propertyType instanceof PsiPrimitiveType) {
PsiClassType boxedType = ((PsiPrimitiveType)propertyType).getBoxedType(context);
return boxedType != null ? boxedType.resolve() : null;
@@ -801,17 +813,17 @@ public class JavaFxPsiUtil {
}
@NotNull
public static List<PsiMember> collectReadableProperties(@Nullable PsiClass psiClass) {
public static Map<String, PsiMember> collectReadableProperties(@Nullable PsiClass psiClass) {
if (psiClass != null) {
return CachedValuesManager.getCachedValue(psiClass, () ->
CachedValueProvider.Result.create(prepareReadableProperties(psiClass), PsiModificationTracker.JAVA_STRUCTURE_MODIFICATION_COUNT));
}
return Collections.emptyList();
return Collections.emptyMap();
}
@NotNull
private static List<PsiMember> prepareReadableProperties(@NotNull PsiClass psiClass) {
private static Map<String, PsiMember> prepareReadableProperties(@NotNull PsiClass psiClass) {
final Map<String, PsiMember> acceptableMembers = new THashMap<>();
for (PsiMethod method : psiClass.getAllMethods()) {
if (method.hasModifierProperty(PsiModifier.STATIC) || !method.hasModifierProperty(PsiModifier.PUBLIC)) continue;
@@ -821,7 +833,7 @@ public class JavaFxPsiUtil {
acceptableMembers.put(propertyName, method);
}
}
return new ArrayList<>(acceptableMembers.values());
return acceptableMembers;
}
@NotNull
@@ -2,8 +2,6 @@ package org.jetbrains.plugins.javaFX.fxml.descriptors;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.codeStyle.VariableKind;
import com.intellij.psi.util.InheritanceUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
@@ -19,6 +17,7 @@ import org.jetbrains.plugins.javaFX.fxml.JavaFxPsiUtil;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -171,40 +170,40 @@ public class JavaFxPropertyAttributeDescriptor extends BasicXmlAttributeDescript
@Nullable
private static String validatePropertyExpression(@NotNull XmlAttributeValue xmlAttributeValue, @NotNull String value) {
final String referencesId = value.substring(1);
if (JavaFxPsiUtil.isIncorrectExpressionBinding(value)) {
return "Incorrect expression syntax";
}
final List<String> propertyNames = JavaFxPsiUtil.isExpressionBinding(value)
? StringUtil.split(value.substring(2, value.length() - 1), ".", true, false)
: Collections.singletonList(value.substring(1));
if (isIncompletePropertyChain(propertyNames)) {
return "Incorrect expression syntax";
}
final XmlTag currentTag = PsiTreeUtil.getParentOfType(xmlAttributeValue, XmlTag.class);
final Map<String, XmlAttributeValue> fileIds = JavaFxPsiUtil.collectFileIds(currentTag);
final PsiClass targetPropertyClass = JavaFxPsiUtil.getWritablePropertyClass(xmlAttributeValue);
if (targetPropertyClass == null || JavaFxPsiUtil.hasConversionFromAnyType(targetPropertyClass)) return null;
final PsiClass valueClass;
if (JavaFxPsiUtil.isExpressionBinding(value)) {
final String expressionText = referencesId.substring(1, referencesId.length() - 1);
final String newId = StringUtil.getPackageName(expressionText);
final PsiClass tagClass = JavaFxPsiUtil.getTagClassById(newId, xmlAttributeValue, fileIds.get(newId));
if (tagClass == null) return null;
final String fieldRef = StringUtil.getShortName(expressionText);
final String fieldName =
JavaCodeStyleManager.getInstance(tagClass.getProject()).propertyNameToVariableName(fieldRef, VariableKind.FIELD);
PsiField psiField = tagClass.findFieldByName(fieldName, true);
final String firstPropertyName = propertyNames.get(0);
final Map<String, XmlAttributeValue> fileIds = JavaFxPsiUtil.collectFileIds(currentTag);
final PsiClass tagClass = JavaFxPsiUtil.getTagClassById(fileIds.get(firstPropertyName), firstPropertyName, xmlAttributeValue);
if (tagClass != null) {
PsiClass aClass = tagClass;
final List<String> remainingPropertyNames = propertyNames.subList(1, propertyNames.size());
for (String propertyName : remainingPropertyNames) {
if (aClass == null) break;
final PsiMember member = JavaFxPsiUtil.collectReadableProperties(aClass).get(propertyName);
aClass = JavaFxPsiUtil.getPropertyClass(JavaFxPsiUtil.getReadablePropertyType(member), xmlAttributeValue);
}
if (aClass != null && !InheritanceUtil.isInheritorOrSelf(aClass, targetPropertyClass, true)) {
return "Invalid value: unable to coerce to " + targetPropertyClass.getQualifiedName();
}
}
return null;
}
final PsiMember propertyDeclaration;
if (psiField != null && psiField.hasModifierProperty(PsiModifier.PUBLIC)) {
propertyDeclaration = psiField;
}
else {
propertyDeclaration = JavaFxPsiUtil.findPropertyGetter(fieldRef, tagClass);
}
if (propertyDeclaration == null) return null;
valueClass = JavaFxPsiUtil.getPropertyClass(JavaFxPsiUtil.getReadablePropertyType(propertyDeclaration), xmlAttributeValue);
}
else {
valueClass = JavaFxPsiUtil.getTagClassById(referencesId, xmlAttributeValue, fileIds.get(referencesId));
}
if (valueClass == null || InheritanceUtil.isInheritorOrSelf(valueClass, targetPropertyClass, true)) {
return null;
}
return "Invalid value: unable to coerce to " + targetPropertyClass.getQualifiedName();
public static boolean isIncompletePropertyChain(@NotNull List<String> propertyNames) {
return propertyNames.isEmpty() || propertyNames.contains("");
}
@Nullable
@@ -58,7 +58,8 @@ public class JavaFxAnnotator implements Annotator {
if (!JavaFxFileTypeFactory.isFxml(containingFile)) return;
if (element instanceof XmlAttributeValue) {
final PsiReference[] references = element.getReferences();
if (!JavaFxPsiUtil.isExpressionBinding(((XmlAttributeValue)element).getValue())) {
final String value = ((XmlAttributeValue)element).getValue();
if (!JavaFxPsiUtil.isExpressionBinding(value) && !JavaFxPsiUtil.isIncorrectExpressionBinding(value)) {
for (PsiReference reference : references) {
final PsiElement resolve = reference.resolve();
if (resolve instanceof PsiMember) {
@@ -34,6 +34,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.javaFX.fxml.FxmlConstants;
import org.jetbrains.plugins.javaFX.fxml.JavaFxPsiUtil;
import org.jetbrains.plugins.javaFX.fxml.descriptors.JavaFxPropertyAttributeDescriptor;
import java.util.*;
import java.util.stream.Collectors;
@@ -42,69 +43,110 @@ import java.util.stream.Collectors;
* User: anna
*/
class JavaFxComponentIdReferenceProvider extends PsiReferenceProvider {
@NotNull
@Override
public PsiReference[] getReferencesByElement(@NotNull PsiElement element,
@NotNull ProcessingContext context) {
final XmlAttributeValue xmlAttributeValue = (XmlAttributeValue)element;
final XmlTag currentTag = PsiTreeUtil.getParentOfType(xmlAttributeValue, XmlTag.class);
final String value = xmlAttributeValue.getValue();
final boolean startsWithDollar = value.startsWith("$");
final String referencesId = startsWithDollar ? value.substring(1) : value;
if (JavaFxPsiUtil.isIncorrectExpressionBinding(value)) {
return PsiReference.EMPTY_ARRAY;
}
final XmlTag currentTag = PsiTreeUtil.getParentOfType(xmlAttributeValue, XmlTag.class);
final Map<String, XmlAttributeValue> fileIds = JavaFxPsiUtil.collectFileIds(currentTag);
if (JavaFxPsiUtil.isExpressionBinding(value)) {
final String expressionText = referencesId.substring(1, referencesId.length() - 1);
final String newId = StringUtil.getPackageName(expressionText);
final String fieldRef = StringUtil.getShortName(expressionText);
final PsiClass controllerClass = JavaFxPsiUtil.getControllerClass(element.getContainingFile());
final PsiReferenceBase idReferenceBase = getIdReferenceBase(xmlAttributeValue, newId, fileIds, Collections.emptyMap(), controllerClass);
final TextRange range = idReferenceBase.getRangeInElement();
final int startOffset = range.getStartOffset() + 2;
final int endOffset = startOffset + newId.length();
idReferenceBase.setRangeInElement(new TextRange(startOffset, endOffset));
if (fileIds.containsKey(newId)) {
final PsiClass tagClass = FxmlConstants.CONTROLLER.equals(newId) ? controllerClass : JavaFxPsiUtil.getTagClass(fileIds.get(newId));
if (tagClass != null) {
final JavaFxExpressionReferenceBase referenceBase = new JavaFxExpressionReferenceBase(xmlAttributeValue, tagClass, fieldRef);
final TextRange textRange = referenceBase.getRangeInElement();
referenceBase.setRangeInElement(new TextRange(endOffset + 1, textRange.getEndOffset() - 1));
return new PsiReference[] {idReferenceBase, referenceBase};
return getExpressionReferences(element, xmlAttributeValue, value, fileIds);
}
if (value.startsWith("$")) {
return getSinglePropertyReferences(xmlAttributeValue, value, fileIds);
}
final Set<String> acceptableIds = new HashSet<String>();
if (currentTag != null) {
final XmlTag parentTag = currentTag.getParentTag();
for (final String id : fileIds.keySet()) {
final XmlAttributeValue resolvedAttrValue = fileIds.get(id);
if (JavaFxPsiUtil.isClassAcceptable(parentTag, JavaFxPsiUtil.getTagClass(resolvedAttrValue))) {
acceptableIds.add(id);
}
}
return new PsiReference[] {idReferenceBase};
}
if (startsWithDollar) {
final PsiClass controllerClass = JavaFxPsiUtil.getControllerClass(element.getContainingFile());
return new PsiReference[]{new JavaFxIdReferenceBase(xmlAttributeValue, fileIds, acceptableIds, value)};
}
final PsiClass targetPropertyClass = JavaFxPsiUtil.getWritablePropertyClass(xmlAttributeValue);
final boolean isConvertible = targetPropertyClass != null && JavaFxPsiUtil.hasConversionFromAnyType(targetPropertyClass);
final Map<String, TypeMatch> typeMatches = fileIds.entrySet().stream().collect(
Collectors.toMap(Map.Entry::getKey, e -> {
final PsiClass valueClass = JavaFxPsiUtil.getTagClassById(e.getKey(), xmlAttributeValue, e.getValue());
return TypeMatch.getMatch(valueClass, targetPropertyClass, isConvertible);
}));
final PsiReferenceBase idReferenceBase = getIdReferenceBase(xmlAttributeValue, referencesId, fileIds, typeMatches, controllerClass);
final TextRange rangeInElement = idReferenceBase.getRangeInElement();
idReferenceBase.setRangeInElement(new TextRange(rangeInElement.getStartOffset() + 1, rangeInElement.getEndOffset()));
return new PsiReference[]{idReferenceBase};
} else {
final Set<String> acceptableIds = new HashSet<String>();
if (currentTag != null) {
final XmlTag parentTag = currentTag.getParentTag();
for (final String id : fileIds.keySet()) {
final XmlAttributeValue resolvedAttrValue = fileIds.get(id);
if (JavaFxPsiUtil.isClassAcceptable(parentTag, JavaFxPsiUtil.getTagClass(resolvedAttrValue))) {
acceptableIds.add(id);
}
}
}
JavaFxIdReferenceBase idReferenceBase = new JavaFxIdReferenceBase(xmlAttributeValue, fileIds, acceptableIds, referencesId);
return new PsiReference[]{idReferenceBase};
@NotNull
private static PsiReference[] getExpressionReferences(@NotNull PsiElement element,
@NotNull XmlAttributeValue xmlAttributeValue,
@NotNull String value,
@NotNull Map<String, XmlAttributeValue> fileIds) {
final String expressionBody = value.substring(2, value.length() - 1);
final List<String> propertyNames = StringUtil.split(expressionBody, ".", true, false);
if (JavaFxPropertyAttributeDescriptor.isIncompletePropertyChain(propertyNames)) return PsiReference.EMPTY_ARRAY;
if (propertyNames.size() == 1) {
return getSinglePropertyReferences(xmlAttributeValue, fileIds, expressionBody, 2);
}
final PsiClass controllerClass = JavaFxPsiUtil.getControllerClass(element.getContainingFile());
final String firstPropertyName = propertyNames.get(0);
int positionInExpression = 2;
final List<PsiReference> result = new ArrayList<>();
final PsiReferenceBase firstReference =
getIdReferenceBase(xmlAttributeValue, firstPropertyName, fileIds, Collections.emptyMap(), controllerClass);
positionInExpression = adjustTextRange(firstPropertyName, firstReference, positionInExpression);
PsiClass propertyOwnerClass = FxmlConstants.CONTROLLER.equals(firstPropertyName) ?
controllerClass : JavaFxPsiUtil.getTagClass(fileIds.get(firstPropertyName));
result.add(firstReference);
final List<String> remainingPropertyNames = propertyNames.subList(1, propertyNames.size());
for (String propertyName : remainingPropertyNames) {
final JavaFxExpressionReferenceBase reference =
new JavaFxExpressionReferenceBase(xmlAttributeValue, propertyOwnerClass, propertyName);
positionInExpression = adjustTextRange(propertyName, reference, positionInExpression);
final PsiType propertyType = JavaFxPsiUtil.getReadablePropertyType(reference.resolve());
propertyOwnerClass = propertyType instanceof PsiClassType ? ((PsiClassType)propertyType).resolve() : null;
result.add(reference);
}
return result.toArray(PsiReference.EMPTY_ARRAY);
}
@NotNull
private static PsiReference[] getSinglePropertyReferences(@NotNull XmlAttributeValue xmlAttributeValue,
@NotNull String value,
@NotNull Map<String, XmlAttributeValue> fileIds) {
return getSinglePropertyReferences(xmlAttributeValue, fileIds, value.substring(1), 1);
}
@NotNull
private static PsiReference[] getSinglePropertyReferences(@NotNull XmlAttributeValue xmlAttributeValue,
@NotNull Map<String, XmlAttributeValue> fileIds,
@NotNull String propertyName,
int positionInExpression) {
final PsiClass controllerClass = JavaFxPsiUtil.getControllerClass(xmlAttributeValue.getContainingFile());
final Map<String, TypeMatch> typeMatches = getTypeMatches(xmlAttributeValue, fileIds);
final PsiReferenceBase reference = getIdReferenceBase(xmlAttributeValue, propertyName, fileIds, typeMatches, controllerClass);
adjustTextRange(propertyName, reference, positionInExpression);
return new PsiReference[]{reference};
}
private static Map<String, TypeMatch> getTypeMatches(@NotNull XmlAttributeValue xmlAttributeValue,
@NotNull Map<String, XmlAttributeValue> fileIds) {
final PsiClass targetPropertyClass = JavaFxPsiUtil.getWritablePropertyClass(xmlAttributeValue);
final boolean isConvertible = targetPropertyClass != null && JavaFxPsiUtil.hasConversionFromAnyType(targetPropertyClass);
return fileIds.entrySet().stream().collect(
Collectors.toMap(Map.Entry::getKey, e -> {
final PsiClass valueClass = JavaFxPsiUtil.getTagClassById(e.getValue(), e.getKey(), xmlAttributeValue);
return TypeMatch.getMatch(valueClass, targetPropertyClass, isConvertible);
}));
}
private static int adjustTextRange(@NotNull String propertyName, @NotNull PsiReferenceBase reference, int positionInExpression) {
final TextRange range = reference.getRangeInElement();
final int startOffset = range.getStartOffset() + positionInExpression;
final int endOffset = startOffset + propertyName.length();
reference.setRangeInElement(new TextRange(startOffset, endOffset));
return positionInExpression + propertyName.length() + 1;
}
@NotNull
@@ -203,7 +245,7 @@ class JavaFxComponentIdReferenceProvider extends PsiReferenceProvider {
@Nullable
@Override
public PsiElement resolve() {
return myTagClass.findFieldByName(myFieldName, true);
return JavaFxPsiUtil.collectReadableProperties(myTagClass).get(myFieldName);
}
@NotNull
@@ -221,7 +263,7 @@ class JavaFxComponentIdReferenceProvider extends PsiReferenceProvider {
private Object[] collectProperties(@NotNull PsiType propertyType, @NotNull Project project) {
final PsiType resolvedType = JavaFxPsiUtil.getWritablePropertyType(propertyType, project);
final List<LookupElement> objs = new ArrayList<>();
final List<PsiMember> readableProperties = JavaFxPsiUtil.collectReadableProperties(myTagClass);
final Collection<PsiMember> readableProperties = JavaFxPsiUtil.collectReadableProperties(myTagClass).values();
for (PsiMember readableMember : readableProperties) {
final PsiType readableType = JavaFxPsiUtil.getReadablePropertyType(readableMember);
if (readableType == null) continue;
@@ -1,25 +0,0 @@
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Tooltip?>
<?import java.lang.Double?>
<?import java.lang.Boolean?>
<?import sample.UnknownModel?>
<GridPane fx:controller="sample.Controller" xmlns:fx="http://javafx.com/fxml">
<fx:define>
<Tooltip text="tooltip" fx:id="tConvertible"/>
</fx:define>
<fx:define>
<Double fx:value="123.45" fx:id="dConvertible"/>
</fx:define>
<fx:define>
<Boolean fx:value="123.45" fx:id="zAssignable"/>
</fx:define>
<fx:define>
<UnknownModel fx:id="mUnknown"/>
</fx:define>
<Button onAction="#sayHello" text="Hello" fx:id="b1" defaultButton="$<caret>" />
</GridPane>
@@ -1,20 +0,0 @@
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Tooltip?>
<?import java.lang.Double?>
<?import sample.UnknownModel?>
<GridPane fx:controller="sample.Controller" xmlns:fx="http://javafx.com/fxml">
<fx:define>
<Tooltip text="tooltip" fx:id="tAssignable"/>
</fx:define>
<fx:define>
<Double fx:value="123.45" fx:id="dIncompatible"/>
</fx:define>
<fx:define>
<UnknownModel fx:id="mUnknown"/>
</fx:define>
<Button onAction="#sayHello" text="Hello" fx:id="b1" tooltip="$<caret>" />
</GridPane>
@@ -0,0 +1,22 @@
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>
<?import java.lang.Double?>
<VBox xmlns:fx="http://javafx.com/fxml">
<fx:define>
<Label fx:id="barLabel" text="bar"/>
<Double fx:id="myDouble" fx:value="31.5"/>
</fx:define>
<Label text=<error descr="Incorrect expression syntax">"$"</error>
opacity=<error descr="Incorrect expression syntax">"${}"</error>
minHeight=<error descr="Incorrect expression syntax">"${.myDouble}"</error>
maxHeight=<error descr="Incorrect expression syntax">"${myDouble.}"</error>
prefHeight=<error descr="Incorrect expression syntax">"${"</error>
minWidth=<error descr="Incorrect expression syntax">"${barLabel..minWidth}"</error>
maxWidth=<error descr="Incorrect expression syntax">"${barLabel.maxWidth.}"</error>
prefWidth=<error descr="Incorrect expression syntax">"${.barLabel.maxWidth}"</error>
alignment=<error descr="Incorrect expression syntax">"${{"</error>
disable=<error descr="Incorrect expression syntax">"${}}"</error>
effect=<error descr="Incorrect expression syntax">"$}"</error>
/>
</VBox>
@@ -0,0 +1,11 @@
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>
<VBox xmlns:fx="http://javafx.com/fxml">
<fx:define>
<Label fx:id="barLabel" text="bar"/>
</fx:define>
<Label disable="${barLabel.border.empty}"/>
<Label disable="${<error descr="Cannot resolve symbol 'unknown'">unknown</error>.<error descr="Cannot resolve symbol 'border'">border</error>.<error descr="Cannot resolve symbol 'empty'">empty</error>}"/>
<Label padding=<error descr="Invalid value: unable to coerce to javafx.geometry.Insets">"${barLabel.border.empty}"</error>/>
<Label padding="${barLabel.border.<error descr="Cannot resolve symbol 'full'">full</error>}"/>
</VBox>
@@ -0,0 +1,16 @@
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>
<?import java.lang.Double?>
<?import java.lang.String?>
<VBox xmlns:fx="http://javafx.com/fxml">
<fx:define>
<Label fx:id="barLabel" text="bar"/>
<Double fx:id="myDouble" fx:value="31.5"/>
<String fx:id="myString" fx:value="Press Me"/>
</fx:define>
<Label text="foo" minHeight="${myDouble}"/>
<Label text="${barLabel.text}"/>
<Button text="${myString}"/>
</VBox>