javafx: Don't highlight as error the built-in "$controller" field and its properties visible in FXML (IDEA-150446)

This commit is contained in:
Pavel Dolgov
2016-02-24 17:39:26 +03:00
parent 6066adeff0
commit 66f6600ea2
9 changed files with 316 additions and 44 deletions
@@ -24,6 +24,7 @@ import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.codeStyle.VariableKind;
import com.intellij.psi.util.InheritanceUtil;
import com.intellij.psi.util.PropertyUtil;
import org.jetbrains.plugins.javaFX.fxml.FxmlConstants;
import org.jetbrains.plugins.javaFX.fxml.JavaFxCommonClassNames;
import org.jetbrains.plugins.javaFX.fxml.JavaFxPsiUtil;
@@ -53,10 +54,13 @@ public class JavaFxGetterSetterPrototypeProvider extends GetterSetterPrototypePr
final PsiCodeBlock getterBody = getter.getBody();
LOG.assertTrue(getterBody != null);
getterBody.getStatements()[0].replace(factory.createStatementFromText("return " + field.getName() + ".get();", field));
final String fieldName = field.getName();
getterBody.getStatements()[0].replace(factory.createStatementFromText("return " + fieldName + ".get();", field));
final PsiMethod propertyGetter = PropertyUtil.generateGetterPrototype(field);
propertyGetter.setName(JavaCodeStyleManager.getInstance(project).variableNameToPropertyName(field.getName(), VariableKind.FIELD) + "Property");
if (propertyGetter != null && fieldName != null) {
propertyGetter.setName(JavaCodeStyleManager.getInstance(project).variableNameToPropertyName(fieldName, VariableKind.FIELD) + FxmlConstants.PROPERTY_FIELD_SUFFIX);
}
return new PsiMethod[] {getter, GenerateMembersUtil.annotateOnOverrideImplement(field.getContainingClass(), propertyGetter)};
}
@@ -96,7 +100,7 @@ public class JavaFxGetterSetterPrototypeProvider extends GetterSetterPrototypePr
@Override
public String suggestGetterName(String propertyName) {
return propertyName + "Property";
return propertyName + FxmlConstants.PROPERTY_FIELD_SUFFIX;
}
@Override
@@ -40,6 +40,7 @@ public class FxmlConstants {
@NonNls public static final String TYPE = "type";
@NonNls public static final String RESOURCES = "resources";
@NonNls public static final String CHARSET = "charset";
@NonNls public static final String CONTROLLER = "controller";
@NonNls public static final String STYLE_CLASS = "styleClass";
@NonNls public static final String STYLESHEETS = "stylesheets";
@@ -50,6 +51,9 @@ public class FxmlConstants {
public static final String FX_ELEMENT_SOURCE = "source";
public static final Map<String, List<String>> FX_ELEMENT_ATTRIBUTES = new HashMap<String, List<String>>();
public static final String PROPERTY_FIELD_SUFFIX = "Property";
static {
FX_ELEMENT_ATTRIBUTES.put(FX_INCLUDE, Arrays.asList(FX_ELEMENT_SOURCE, FX_ID, RESOURCES, CHARSET));
FX_ELEMENT_ATTRIBUTES.put(FX_REFERENCE, Collections.singletonList(FX_ELEMENT_SOURCE));
@@ -33,6 +33,7 @@ import com.intellij.psi.search.searches.ReferencesSearch;
import com.intellij.psi.util.*;
import com.intellij.psi.xml.*;
import com.intellij.util.Processor;
import com.intellij.xml.XmlAttributeDescriptor;
import com.intellij.xml.XmlElementDescriptor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -373,7 +374,7 @@ public class JavaFxPsiUtil {
}
@Nullable
public static PsiType getPropertyType(final PsiType type, final Project project) {
public static PsiType getWritablePropertyType(final PsiType type, final Project project) {
final PsiClassType.ClassResolveResult resolveResult = PsiUtil.resolveGenericsClassInType(type);
final PsiClass psiClass = resolveResult.getElement();
if (psiClass != null) {
@@ -505,7 +506,7 @@ public class JavaFxPsiUtil {
private static String canCoerce(PsiClass aClass, PsiType type) {
PsiType collectionItemType = JavaGenericsUtil.getCollectionItemType(type, aClass.getResolveScope());
if (collectionItemType == null && InheritanceUtil.isInheritor(type, JavaFxCommonClassNames.JAVAFX_BEANS_PROPERTY)) {
collectionItemType = getPropertyType(type, aClass.getProject());
collectionItemType = getWritablePropertyType(type, aClass.getProject());
}
if (collectionItemType != null && PsiPrimitiveType.getUnboxedType(collectionItemType) == null) {
final PsiClass baseClass = PsiUtil.resolveClassInType(collectionItemType);
@@ -570,6 +571,99 @@ public class JavaFxPsiUtil {
});
}
@Nullable
public static PsiType getWritablePropertyType(PsiElement declaration) {
if (declaration instanceof PsiField) {
return getWrappedPropertyType((PsiField)declaration, declaration.getProject(), JavaFxCommonClassNames.ourWritableMap);
}
if (declaration instanceof PsiMethod) {
final PsiParameter[] parameters = ((PsiMethod)declaration).getParameterList().getParameters();
final boolean isStatic = ((PsiMethod)declaration).hasModifierProperty(PsiModifier.STATIC);
if (isStatic && parameters.length == 2 || !isStatic && parameters.length == 1) {
return parameters[parameters.length - 1].getType();
}
}
return null;
}
@Nullable
public static PsiType getReadablePropertyType(PsiElement declaration) {
if (declaration instanceof PsiField) {
return getWrappedPropertyType((PsiField)declaration, declaration.getProject(), JavaFxCommonClassNames.ourReadOnlyMap);
}
if (declaration instanceof PsiMethod) {
PsiMethod psiMethod = (PsiMethod)declaration;
final PsiParameter[] parameters = psiMethod.getParameterList().getParameters();
final boolean isStatic = psiMethod.hasModifierProperty(PsiModifier.STATIC);
if (!isStatic && parameters.length == 0) {
return psiMethod.getReturnType();
}
}
return null;
}
@NotNull
public static Map<String, XmlAttributeValue> collectFileIds(final XmlTag currentTag) {
final Map<String, XmlAttributeValue> fileIds = new HashMap<String, XmlAttributeValue>();
currentTag.getContainingFile().accept(new XmlRecursiveElementVisitor() {
@Override
public void visitXmlTag(XmlTag tag) {
super.visitXmlTag(tag);
if (currentTag != tag) {
final XmlAttribute attribute = tag.getAttribute(FxmlConstants.FX_ID);
if (attribute != null) {
fileIds.put(attribute.getValue(), attribute.getValueElement());
}
}
}
});
final PsiFile containingFile = currentTag.getContainingFile();
if (containingFile instanceof XmlFile) {
final XmlTag rootTag = ((XmlFile)containingFile).getRootTag();
if (rootTag != null) {
final XmlAttribute attribute = rootTag.getAttribute(FxmlConstants.FX_CONTROLLER);
if (attribute != null) {
fileIds.put(FxmlConstants.CONTROLLER, attribute.getValueElement());
}
}
}
return fileIds;
}
@Nullable
public static PsiClass getTagClassById(String id, PsiElement context, XmlAttributeValue xmlAttributeValue) {
return FxmlConstants.CONTROLLER.equals(id) ? getControllerClass(context.getContainingFile()) : getTagClass(xmlAttributeValue);
}
@Nullable
public static PsiClass getPropertyClass(XmlAttributeValue xmlAttributeValue) {
final PsiElement declaration = getPropertyDeclaration(xmlAttributeValue);
return getPropertyClass(getWritablePropertyType(declaration), xmlAttributeValue);
}
@Nullable
public static PsiClass getPropertyClass(PsiType propertyType, XmlAttributeValue context) {
if (propertyType instanceof PsiPrimitiveType) {
PsiClassType boxedType = ((PsiPrimitiveType)propertyType).getBoxedType(context);
return boxedType != null ? boxedType.resolve() : null;
}
return PsiUtil.resolveClassInType(propertyType);
}
@Nullable
private static PsiElement getPropertyDeclaration(XmlAttributeValue xmlAttributeValue) {
PsiClass tagClass = getTagClass(xmlAttributeValue);
if (tagClass != null) {
XmlAttribute xmlAttribute = (XmlAttribute)xmlAttributeValue.getParent();
final XmlAttributeDescriptor attributeDescriptor = xmlAttribute.getDescriptor();
if (attributeDescriptor != null) {
return attributeDescriptor.getDeclaration();
}
}
return null;
}
private static class JavaFxControllerCachedValueProvider implements CachedValueProvider<PsiClass> {
private final Project myProject;
private final PsiFile myContainingFile;
@@ -1,9 +1,13 @@
package org.jetbrains.plugins.javaFX.fxml.descriptors;
import com.intellij.openapi.util.Comparing;
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;
import com.intellij.psi.util.TypeConversionUtil;
import com.intellij.psi.xml.*;
import com.intellij.util.ArrayUtil;
import com.intellij.xml.XmlAttributeDescriptor;
@@ -19,6 +23,7 @@ import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* User: anna
@@ -148,6 +153,45 @@ public class JavaFxPropertyAttributeDescriptor extends BasicXmlAttributeDescript
}
}
}
else if (value.startsWith("$")) {
final String referencesId = value.substring(1);
final XmlTag currentTag = PsiTreeUtil.getParentOfType(xmlAttributeValue, XmlTag.class);
final Map<String, XmlAttributeValue> fileIds = JavaFxPsiUtil.collectFileIds(currentTag);
final PsiClass targetPropertyClass = JavaFxPsiUtil.getPropertyClass(xmlAttributeValue);
if (targetPropertyClass == null ||
Comparing.strEqual(targetPropertyClass.getQualifiedName(), CommonClassNames.JAVA_LANG_STRING) ||
JavaFxPsiUtil.findValueOfMethod(targetPropertyClass) != null) {
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 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();
}
else {
final XmlAttributeDescriptor attributeDescriptor = ((XmlAttribute)parent).getDescriptor();
if (attributeDescriptor != null) {
@@ -199,16 +243,7 @@ public class JavaFxPropertyAttributeDescriptor extends BasicXmlAttributeDescript
@Nullable
private static String getBoxedPropertyType(PsiElement declaration) {
PsiType attrType = null;
if (declaration instanceof PsiField) {
attrType = JavaFxPsiUtil.getWrappedPropertyType((PsiField)declaration, declaration.getProject(), JavaFxCommonClassNames.ourWritableMap);
} else if (declaration instanceof PsiMethod) {
final PsiParameter[] parameters = ((PsiMethod)declaration).getParameterList().getParameters();
final boolean isStatic = ((PsiMethod)declaration).hasModifierProperty(PsiModifier.STATIC);
if (isStatic && parameters.length == 2 || !isStatic && parameters.length == 1) {
attrType = parameters[parameters.length - 1].getType();
}
}
PsiType attrType = JavaFxPsiUtil.getWritablePropertyType(declaration);
String boxedQName = null;
if (attrType instanceof PsiPrimitiveType) {
@@ -88,7 +88,7 @@ public class JavaFxPropertyElementDescriptor implements XmlElementDescriptor {
descriptors.add(new JavaFxClassBackedElementDescriptor(aClass.getName(), aClass));
}
} else if (InheritanceUtil.isInheritor(psiType, JavaFxCommonClassNames.JAVAFX_BEANS_PROPERTY)) {
final PsiType propertyType = JavaFxPsiUtil.getPropertyType(psiType, project);
final PsiType propertyType = JavaFxPsiUtil.getWritablePropertyType(psiType, project);
final PsiClass aClass = PsiUtil.resolveClassInType(propertyType);
if (aClass != null) {
descriptors.add(new JavaFxClassBackedElementDescriptor(aClass.getName(), aClass));
@@ -15,9 +15,12 @@
*/
package org.jetbrains.plugins.javaFX.fxml.refs;
import com.intellij.codeInsight.lookup.LookupItem;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.util.InheritanceUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.TypeConversionUtil;
import com.intellij.psi.xml.XmlAttribute;
@@ -32,6 +35,7 @@ import org.jetbrains.plugins.javaFX.fxml.FxmlConstants;
import org.jetbrains.plugins.javaFX.fxml.JavaFxPsiUtil;
import java.util.*;
import java.util.stream.Collectors;
/**
* User: anna
@@ -46,42 +50,21 @@ class JavaFxComponentIdReferenceProvider extends PsiReferenceProvider {
final String value = xmlAttributeValue.getValue();
final boolean startsWithDollar = value.startsWith("$");
final String referencesId = startsWithDollar ? value.substring(1) : value;
final Map<String, XmlAttributeValue> fileIds = new HashMap<String, XmlAttributeValue>();
xmlAttributeValue.getContainingFile().accept(new XmlRecursiveElementVisitor() {
@Override
public void visitXmlTag(XmlTag tag) {
super.visitXmlTag(tag);
if (currentTag != tag) {
final XmlAttribute attribute = tag.getAttribute(FxmlConstants.FX_ID);
if (attribute != null) {
fileIds.put(attribute.getValue(), attribute.getValueElement());
}
}
}
});
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 PsiReferenceBase idReferenceBase;
final PsiClass controllerClass = JavaFxPsiUtil.getControllerClass(element.getContainingFile());
final PsiField controllerField = controllerClass != null ? controllerClass.findFieldByName(newId, false) : null;
if (controllerField == null) {
idReferenceBase = new JavaFxIdReferenceBase(xmlAttributeValue, fileIds, newId);
} else {
idReferenceBase = new JavaFxFieldIdReferenceProvider.JavaFxControllerFieldRef(xmlAttributeValue, controllerField, controllerClass);
}
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 XmlAttributeValue attributeValue = fileIds.get(newId);
final PsiClass tagClass = JavaFxPsiUtil.getTagClass(attributeValue);
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();
@@ -92,7 +75,20 @@ class JavaFxComponentIdReferenceProvider extends PsiReferenceProvider {
return new PsiReference[] {idReferenceBase};
}
if (startsWithDollar) {
final JavaFxIdReferenceBase idReferenceBase = new JavaFxIdReferenceBase(xmlAttributeValue, fileIds, referencesId);
final PsiClass controllerClass = JavaFxPsiUtil.getControllerClass(element.getContainingFile());
final PsiClass targetPropertyClass = JavaFxPsiUtil.getPropertyClass(xmlAttributeValue);
final boolean isConvertible = targetPropertyClass != null &&
(Comparing.strEqual(targetPropertyClass.getQualifiedName(), CommonClassNames.JAVA_LANG_STRING)
|| JavaFxPsiUtil.findValueOfMethod(targetPropertyClass) != null);
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};
@@ -112,9 +108,50 @@ class JavaFxComponentIdReferenceProvider extends PsiReferenceProvider {
}
}
@NotNull
private static PsiReferenceBase getIdReferenceBase(XmlAttributeValue xmlAttributeValue,
String referencesId,
Map<String, XmlAttributeValue> fileIds,
Map<String, TypeMatch> typeMatches,
PsiClass controllerClass) {
if (controllerClass != null && !FxmlConstants.CONTROLLER.equals(referencesId)) {
final PsiField controllerField = controllerClass.findFieldByName(referencesId, false);
if (controllerField != null) {
return new JavaFxFieldIdReferenceProvider.JavaFxControllerFieldRef(xmlAttributeValue, controllerField, controllerClass);
}
}
return new JavaFxIdReferenceBase(xmlAttributeValue, fileIds, typeMatches, referencesId);
}
private enum TypeMatch {
ASSIGNABLE(3.0),
CONVERTIBLE(2.0),
UNDEFINED(1.0),
INCOMPATIBLE(0.0);
private final double myPriority;
TypeMatch(double priority) {
myPriority = priority;
}
public static double getPriority(TypeMatch match) {
return match != null ? match.myPriority : 0.0;
}
@NotNull
public static TypeMatch getMatch(PsiClass valueClass, PsiClass targetPropertyClass, boolean isConvertible) {
if (valueClass == null || targetPropertyClass == null) return UNDEFINED;
if (InheritanceUtil.isInheritorOrSelf(valueClass, targetPropertyClass, true)) return ASSIGNABLE;
if (isConvertible) return CONVERTIBLE;
return INCOMPATIBLE;
}
}
private static class JavaFxIdReferenceBase extends PsiReferenceBase<XmlAttributeValue> {
private final Map<String, XmlAttributeValue> myFileIds;
private final Set<String> myAcceptableIds;
private final Map<String, TypeMatch> myTypeMatches;
private final String myReferencesId;
private JavaFxIdReferenceBase(XmlAttributeValue element,
@@ -125,13 +162,16 @@ class JavaFxComponentIdReferenceProvider extends PsiReferenceProvider {
myFileIds = fileIds;
myAcceptableIds = acceptableIds;
myReferencesId = referencesId;
myTypeMatches = Collections.emptyMap();
}
public JavaFxIdReferenceBase(XmlAttributeValue xmlAttributeValue,
Map<String, XmlAttributeValue> fileIds,
Map<String, TypeMatch> typeMatches,
String referencesId) {
super(xmlAttributeValue);
myFileIds = fileIds;
myTypeMatches = typeMatches;
myReferencesId = referencesId;
myAcceptableIds = myFileIds.keySet();
}
@@ -145,7 +185,12 @@ class JavaFxComponentIdReferenceProvider extends PsiReferenceProvider {
@NotNull
@Override
public Object[] getVariants() {
return ArrayUtil.toStringArray(myAcceptableIds);
return myAcceptableIds.stream().map(
id -> {
LookupItem<String> item = new LookupItem<String>(id, id);
item.setPriority(TypeMatch.getPriority(myTypeMatches.get(id)));
return item;
}).toArray(LookupItem[]::new);
}
}
@@ -183,7 +228,7 @@ class JavaFxComponentIdReferenceProvider extends PsiReferenceProvider {
private Object[] collectProperties(@NotNull PsiField psiField) {
final PsiType type = psiField.getType();
final PsiType propertyType = JavaFxPsiUtil.getPropertyType(type, psiField.getProject());
final PsiType propertyType = JavaFxPsiUtil.getWritablePropertyType(type, psiField.getProject());
final List<PsiField> objs = new ArrayList<PsiField>();
for (PsiField field : myTagClass.getAllFields()) {
if (field.hasModifierProperty(PsiModifier.STATIC)) continue;
@@ -0,0 +1,48 @@
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
public class ControllerInExpression {
@FXML
private ControllerInExpressionWrapper wrapper;
@FXML
private ControllerInExpressionWrapper wrapper2;
private StringProperty strProp = new SimpleStringProperty("Text");
public StringProperty strPropProperty() {
return strProp;
}
public String getStrProp() {
return strProp.get();
}
public void setStrProp(StringProperty strProp) {
this.strProp = strProp;
}
private IntegerProperty intProp = new SimpleIntegerProperty(123);
public IntegerProperty intPropProperty() {
return intProp;
}
public int getIntProp() {
return intProp.get();
}
public void setIntProp(int intProp) {
this.intProp.set(intProp);
}
private DoubleProperty doubleProp = new SimpleDoubleProperty(123.45);
public DoubleProperty doublePropProperty() {
return doubleProp;
}
public double getDoubleProp() {
return doubleProp.get();
}
public void setDoubleProp(double doubleProp) {
this.doubleProp.set(doubleProp);
}
@Override
public String toString() {
return "222.2";
}
}
@@ -0,0 +1,18 @@
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
public class ControllerInExpressionWrapper {
private ObjectProperty<ControllerInExpression> controller = new SimpleObjectProperty<>(this, "controller");
public ObjectProperty<ControllerInExpression> controllerProperty() {
return controller;
}
public ControllerInExpression getController() {
return controller.get();
}
public void setController(ControllerInExpression controller) {
this.controller.set(controller);
}
}
@@ -0,0 +1,24 @@
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Tooltip?>
<?import ControllerInExpressionWrapper?>
<?import ControllerInExpression?>
<VBox fx:controller="ControllerInExpression" xmlns:fx="http://javafx.com/fxml">
<fx:define>
<Tooltip text="tooltip" fx:id="tooltip"/>
</fx:define>
<fx:define>
<ControllerInExpressionWrapper fx:id="wrapper" controller="$controller"/>
</fx:define>
<fx:define>
<ControllerInExpressionWrapper fx:id="wrapper2" controller=<error descr="Invalid value: unable to coerce to ControllerInExpression">"$tooltip"</error>/>
</fx:define>
<Label text="$controller"/>
<Label text="${controller.strProp}"
maxWidth="$controller"
minHeight="${controller.intProp}"
maxHeight="${controller.doubleProp}"
tooltip=<error descr="Invalid value: unable to coerce to javafx.scene.control.Tooltip">"${controller.strProp}"</error>
/>
<Label text="${controller.<error descr="Cannot resolve symbol 'unknownProp'">unknownProp</error>}"/>
</VBox>