diff --git a/plugins/javaFX/javaFX-CE/testSrc/org/jetbrains/plugins/javaFX/fxml/JavaFXHighlightingTest.java b/plugins/javaFX/javaFX-CE/testSrc/org/jetbrains/plugins/javaFX/fxml/JavaFXHighlightingTest.java index 4dbcaba2eba6..58448dcb7615 100644 --- a/plugins/javaFX/javaFX-CE/testSrc/org/jetbrains/plugins/javaFX/fxml/JavaFXHighlightingTest.java +++ b/plugins/javaFX/javaFX-CE/testSrc/org/jetbrains/plugins/javaFX/fxml/JavaFXHighlightingTest.java @@ -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"); diff --git a/plugins/javaFX/javaFX-CE/testSrc/org/jetbrains/plugins/javaFX/fxml/JavaFxCompletionTest.java b/plugins/javaFX/javaFX-CE/testSrc/org/jetbrains/plugins/javaFX/fxml/JavaFxCompletionTest.java index c967875805e7..0744d5da4d77 100644 --- a/plugins/javaFX/javaFX-CE/testSrc/org/jetbrains/plugins/javaFX/fxml/JavaFxCompletionTest.java +++ b/plugins/javaFX/javaFX-CE/testSrc/org/jetbrains/plugins/javaFX/fxml/JavaFxCompletionTest.java @@ -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) { diff --git a/plugins/javaFX/src/org/jetbrains/plugins/javaFX/fxml/JavaFxPsiUtil.java b/plugins/javaFX/src/org/jetbrains/plugins/javaFX/fxml/JavaFxPsiUtil.java index 3ad40a441eea..b929b361712c 100644 --- a/plugins/javaFX/src/org/jetbrains/plugins/javaFX/fxml/JavaFxPsiUtil.java +++ b/plugins/javaFX/src/org/jetbrains/plugins/javaFX/fxml/JavaFxPsiUtil.java @@ -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 collectReadableProperties(@Nullable PsiClass psiClass) { + public static Map 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 prepareReadableProperties(@NotNull PsiClass psiClass) { + private static Map prepareReadableProperties(@NotNull PsiClass psiClass) { final Map 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 diff --git a/plugins/javaFX/src/org/jetbrains/plugins/javaFX/fxml/descriptors/JavaFxPropertyAttributeDescriptor.java b/plugins/javaFX/src/org/jetbrains/plugins/javaFX/fxml/descriptors/JavaFxPropertyAttributeDescriptor.java index 100897fc4127..a60915ceff73 100644 --- a/plugins/javaFX/src/org/jetbrains/plugins/javaFX/fxml/descriptors/JavaFxPropertyAttributeDescriptor.java +++ b/plugins/javaFX/src/org/jetbrains/plugins/javaFX/fxml/descriptors/JavaFxPropertyAttributeDescriptor.java @@ -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 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 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 fileIds = JavaFxPsiUtil.collectFileIds(currentTag); + final PsiClass tagClass = JavaFxPsiUtil.getTagClassById(fileIds.get(firstPropertyName), firstPropertyName, xmlAttributeValue); + if (tagClass != null) { + PsiClass aClass = tagClass; + final List 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 propertyNames) { + return propertyNames.isEmpty() || propertyNames.contains(""); } @Nullable diff --git a/plugins/javaFX/src/org/jetbrains/plugins/javaFX/fxml/refs/JavaFxAnnotator.java b/plugins/javaFX/src/org/jetbrains/plugins/javaFX/fxml/refs/JavaFxAnnotator.java index 5c606ce52d84..f5090a3cbc9a 100644 --- a/plugins/javaFX/src/org/jetbrains/plugins/javaFX/fxml/refs/JavaFxAnnotator.java +++ b/plugins/javaFX/src/org/jetbrains/plugins/javaFX/fxml/refs/JavaFxAnnotator.java @@ -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) { diff --git a/plugins/javaFX/src/org/jetbrains/plugins/javaFX/fxml/refs/JavaFxComponentIdReferenceProvider.java b/plugins/javaFX/src/org/jetbrains/plugins/javaFX/fxml/refs/JavaFxComponentIdReferenceProvider.java index 4ff5bcc1e516..452119cf5f1b 100644 --- a/plugins/javaFX/src/org/jetbrains/plugins/javaFX/fxml/refs/JavaFxComponentIdReferenceProvider.java +++ b/plugins/javaFX/src/org/jetbrains/plugins/javaFX/fxml/refs/JavaFxComponentIdReferenceProvider.java @@ -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 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 acceptableIds = new HashSet(); + 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 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 acceptableIds = new HashSet(); - 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 fileIds) { + final String expressionBody = value.substring(2, value.length() - 1); + final List 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 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 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 fileIds) { + return getSinglePropertyReferences(xmlAttributeValue, fileIds, value.substring(1), 1); + } + + @NotNull + private static PsiReference[] getSinglePropertyReferences(@NotNull XmlAttributeValue xmlAttributeValue, + @NotNull Map fileIds, + @NotNull String propertyName, + int positionInExpression) { + final PsiClass controllerClass = JavaFxPsiUtil.getControllerClass(xmlAttributeValue.getContainingFile()); + final Map typeMatches = getTypeMatches(xmlAttributeValue, fileIds); + final PsiReferenceBase reference = getIdReferenceBase(xmlAttributeValue, propertyName, fileIds, typeMatches, controllerClass); + adjustTextRange(propertyName, reference, positionInExpression); + return new PsiReference[]{reference}; + } + + private static Map getTypeMatches(@NotNull XmlAttributeValue xmlAttributeValue, + @NotNull Map 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 objs = new ArrayList<>(); - final List readableProperties = JavaFxPsiUtil.collectReadableProperties(myTagClass); + final Collection readableProperties = JavaFxPsiUtil.collectReadableProperties(myTagClass).values(); for (PsiMember readableMember : readableProperties) { final PsiType readableType = JavaFxPsiUtil.getReadablePropertyType(readableMember); if (readableType == null) continue; diff --git a/plugins/javaFX/testData/completion/variableCompletionBooleanFirst.fxml b/plugins/javaFX/testData/completion/variableCompletionBooleanFirst.fxml deleted file mode 100644 index ab2adce254b0..000000000000 --- a/plugins/javaFX/testData/completion/variableCompletionBooleanFirst.fxml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - -