/* * Copyright (C) 2013 The Android Open Source Project * * 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 com.android.tools.idea.folding; import com.android.resources.ResourceType; import com.android.tools.idea.AndroidPsiUtils; import com.android.tools.idea.rendering.AppResourceRepository; import com.android.tools.idea.rendering.LocalResourceRepository; import com.intellij.codeInsight.AnnotationUtil; import com.intellij.lang.ASTNode; import com.intellij.lang.folding.FoldingBuilderEx; import com.intellij.lang.folding.FoldingDescriptor; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.editor.Document; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleUtilCore; import com.intellij.openapi.util.TextRange; import com.intellij.psi.*; import com.intellij.psi.impl.source.SourceTreeToPsiMap; import com.intellij.psi.xml.XmlAttributeValue; import com.intellij.psi.xml.XmlFile; import com.intellij.util.ArrayUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import static com.android.SdkConstants.STRING_PREFIX; import static com.android.tools.idea.folding.InlinedResource.NONE; public class ResourceFoldingBuilder extends FoldingBuilderEx { private static final String ANDROID_RESOURCE_INT = "android.annotation.ResourceInt"; private static final boolean FORCE_PROJECT_RESOURCE_LOADING = true; private static final boolean ONLY_FOLD_ANNOTATED_METHODS = false; private static final boolean UNIT_TEST_MODE = ApplicationManager.getApplication().isUnitTestMode(); public static final String DIMEN_PREFIX = "@dimen/"; public static final String INTEGER_PREFIX = "@integer/"; public ResourceFoldingBuilder() { } private static boolean isFoldingEnabled() { return AndroidFoldingSettings.getInstance().isCollapseAndroidStrings(); } @Override public boolean isCollapsedByDefault(@NotNull ASTNode node) { return isFoldingEnabled(); } @Override public String getPlaceholderText(@NotNull ASTNode node) { PsiElement element = SourceTreeToPsiMap.treeElementToPsi(node); if (element != null) { InlinedResource string = getResolvedString(element); if (string != NONE) { String foldLabel = string.getResolvedString(); if (foldLabel != null) { return foldLabel; } } } return element != null ? element.getText() : node.getText(); } @Override @NotNull public FoldingDescriptor[] buildFoldRegions(@NotNull PsiElement element, @NotNull Document document, boolean quick) { if (!(element instanceof PsiJavaFile || element instanceof XmlFile) || quick && !UNIT_TEST_MODE || !isFoldingEnabled()) { return FoldingDescriptor.EMPTY; } final List<FoldingDescriptor> result = new ArrayList<FoldingDescriptor>(); if (element instanceof PsiJavaFile) { final PsiJavaFile file = (PsiJavaFile) element; file.accept(new JavaRecursiveElementWalkingVisitor() { @Override public void visitReferenceExpression(PsiReferenceExpression expression) { InlinedResource inlinedResource = findJavaExpressionReference(expression); if (inlinedResource != NONE) { result.add(inlinedResource.getDescriptor()); } super.visitReferenceExpression(expression); } }); } else { final XmlFile file = (XmlFile) element; file.accept(new XmlRecursiveElementVisitor() { @Override public void visitXmlAttributeValue(XmlAttributeValue value) { InlinedResource inlinedResource = findXmlValueReference(value); if (inlinedResource != NONE) { FoldingDescriptor descriptor = inlinedResource.getDescriptor(); if (descriptor != null) { result.add(descriptor); } } super.visitXmlAttributeValue(value); } }); } return result.toArray(new FoldingDescriptor[result.size()]); } @NotNull private static InlinedResource getResolvedString(PsiElement element) { if (element instanceof PsiReferenceExpression) { return findJavaExpressionReference((PsiReferenceExpression)element); } else if (element instanceof XmlAttributeValue) { return findXmlValueReference((XmlAttributeValue)element); } else if (element instanceof PsiMethodCallExpression) { // This can happen when a folding lookup for a parameter ends up returning the // surrounding method call as the folding region; in that case we have to map // back to the right parameter PsiMethodCallExpression call = (PsiMethodCallExpression)element; for (PsiExpression expression : call.getArgumentList().getExpressions()) { if (expression instanceof PsiReferenceExpression) { InlinedResource string = findJavaExpressionReference((PsiReferenceExpression)expression); if (string != NONE) { return string; } } } } return NONE; } @NotNull private static InlinedResource findXmlValueReference(XmlAttributeValue element) { String value = element.getValue(); if (value.startsWith(STRING_PREFIX)) { String name = value.substring(STRING_PREFIX.length()); return createdInlinedResource(ResourceType.STRING, name, element); } else if (value.startsWith(DIMEN_PREFIX)) { String name = value.substring(DIMEN_PREFIX.length()); return createdInlinedResource(ResourceType.DIMEN, name, element); } else if (value.startsWith(INTEGER_PREFIX)) { String name = value.substring(INTEGER_PREFIX.length()); return createdInlinedResource(ResourceType.INTEGER, name, element); } else { return NONE; } } @NotNull private static InlinedResource findJavaExpressionReference(PsiReferenceExpression expression) { AndroidPsiUtils.ResourceReferenceType referenceType = AndroidPsiUtils.getResourceReferenceType(expression); if (referenceType != AndroidPsiUtils.ResourceReferenceType.APP) { return NONE; } ResourceType type = AndroidPsiUtils.getResourceType(expression); if (type == null || !(type == ResourceType.STRING || type == ResourceType.DIMEN || type == ResourceType.INTEGER || type == ResourceType.PLURALS)) { return NONE; } PsiElement parameterList = expression.getParent(); String name = AndroidPsiUtils.getResourceName(expression); if (parameterList instanceof PsiExpressionList) { PsiElement call = parameterList.getParent(); if (call instanceof PsiMethodCallExpression) { PsiMethodCallExpression callExpression = (PsiMethodCallExpression)call; PsiReferenceExpression methodExpression = callExpression.getMethodExpression(); String methodName = methodExpression.getReferenceName(); if (methodName != null && (methodName.equals("getString") || methodName.equals("getText") || methodName.equals("getInteger") || methodName.startsWith("getDimension") || methodName.startsWith("getQuantityString"))) { // This seems to be an IntelliJ bug; it complains that type can be null, but it clearly can not // (and if I insert assert type != null it correctly says that the assertion is not necessary) //noinspection ConstantConditions @NotNull ResourceType resourceType = type; //noinspection ConstantConditions return createdInlinedResource(resourceType, name, callExpression); } //noinspection ConstantConditions if (!UNIT_TEST_MODE && ONLY_FOLD_ANNOTATED_METHODS) { PsiParameter[] parameters = null; int parameterIndex = ArrayUtil.indexOf(callExpression.getArgumentList().getExpressions(), expression); if (parameterIndex == -1) { return NONE; } PsiMethod method = callExpression.resolveMethod(); if (!UNIT_TEST_MODE) { // For some reason, we can't resolve PsiMethods from the unit tests if (method == null) { return NONE; } parameters = method.getParameterList().getParameters(); if (parameters.length <= parameterIndex) { return NONE; } } if (!allowsResourceType(ResourceType.STRING, parameters[parameterIndex])) { return NONE; } } } } // Suppress null warning; see @NotNull comment further up in this method //noinspection ConstantConditions return createdInlinedResource(type, name, expression); } @Nullable private static LocalResourceRepository getAppResources(PsiElement element) { Module module = ModuleUtilCore.findModuleForPsiElement(element); if (module == null) { return null; } return AppResourceRepository.getAppResources(module, FORCE_PROJECT_RESOURCE_LOADING); } private static InlinedResource createdInlinedResource(@NotNull ResourceType type, @NotNull String name, @NotNull PsiElement foldElement) { // Not part of a call: just fold the R.string reference itself LocalResourceRepository appResources = getAppResources(foldElement); if (appResources != null && appResources.hasResourceItem(type, name)) { ASTNode node = foldElement.getNode(); if (node != null) { TextRange textRange = foldElement.getTextRange(); HashSet<Object> dependencies = new HashSet<Object>(); dependencies.add(foldElement); FoldingDescriptor descriptor = new FoldingDescriptor(node, textRange, null, dependencies); InlinedResource inlinedResource = new InlinedResource(type, name, appResources, descriptor, foldElement); dependencies.add(inlinedResource); return inlinedResource; } } return NONE; } /** * Returns true if the given modifier list owner (such as a method or parameter) * specifies a {@code @ResourceInt} which includes the given {@code type}. * * @param type the resource type to check * @param owner the potentially annotated element to check * @return true if the resource type is allowed */ public static boolean allowsResourceType(@NotNull ResourceType type, @Nullable PsiModifierListOwner owner) { if (owner == null) { return false; } PsiAnnotation annotation = AnnotationUtil.findAnnotation(owner, ANDROID_RESOURCE_INT); Boolean allowed = allowsResourceType(type, annotation); return allowed != null && allowed.booleanValue(); } /** * Returns true if the given {@code @ResourceInt} annotation usage specifies that the given resource type * is allowed * * @param type the resource type to check * @param annotation an annotation usage on an element * @return true if the resource type is allowed, false if it is not, and null if no annotation * was found */ @Nullable public static Boolean allowsResourceType(@NotNull ResourceType type, @Nullable PsiAnnotation annotation) { if (annotation == null) { return null; } assert ANDROID_RESOURCE_INT.equals(annotation.getQualifiedName()); PsiAnnotationParameterList annotationParameters = annotation.getParameterList(); for (PsiNameValuePair pair : annotationParameters.getAttributes()) { PsiAnnotationMemberValue value = pair.getValue(); if (value instanceof PsiReferenceExpression) { PsiReferenceExpression expression = (PsiReferenceExpression) value; return allowsResourceType(type, expression); } else if (value instanceof PsiArrayInitializerMemberValue) { PsiArrayInitializerMemberValue mv = (PsiArrayInitializerMemberValue) value; for (PsiAnnotationMemberValue v : mv.getInitializers()) { if (v instanceof PsiReferenceExpression) { Boolean b = allowsResourceType(type, (PsiReferenceExpression)v); if (b != null) { return b; } } } } } return null; } private static Boolean allowsResourceType(ResourceType type, PsiReferenceExpression v) { // When the @ResourceInt annotation is added to the SDK and potentially added to the // user's classpath, we should resolve this constant properly as follows: // PsiElement resolved = v.resolve(); // if (resolved instanceof PsiEnumConstant) { // String name = ((PsiEnumConstant) resolved).getName(); // However, for now these are just annotations in the external annotations file, // so we simply use the text tokens: String name = v.getText(); if (name.equals("all")) { // Corresponds to ResourceInt.Type.ALL return true; } else if (name.equals("none")) { // Corresponds to ResourceInt.Type.NONE return false; } else { return type.getName().equalsIgnoreCase(name); } } }