Add usage type support for class usages
diff --git a/smalidea/META-INF/plugin.xml b/smalidea/META-INF/plugin.xml
index 4f827bd..d14d8cc 100644
--- a/smalidea/META-INF/plugin.xml
+++ b/smalidea/META-INF/plugin.xml
@@ -30,6 +30,7 @@
implementationClass="org.jf.smalidea.findUsages.SmaliFindUsagesProvider"/>
<referencesSearch implementation="org.jf.smalidea.findUsages.SmaliClassReferenceSearcher"/>
<usageTargetProvider implementation="org.jf.smalidea.findUsages.SmaliUsageTargetProvider" />
+ <usageTypeProvider implementation="org.jf.smalidea.findUsages.SmaliUsageTypeProvider"/>
</extensions>
<application-components>
diff --git a/smalidea/src/main/java/org/jf/smalidea/findUsages/SmaliUsageTypeProvider.java b/smalidea/src/main/java/org/jf/smalidea/findUsages/SmaliUsageTypeProvider.java
new file mode 100644
index 0000000..a0069b8
--- /dev/null
+++ b/smalidea/src/main/java/org/jf/smalidea/findUsages/SmaliUsageTypeProvider.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2015, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.smalidea.findUsages;
+
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.usages.impl.rules.UsageType;
+import com.intellij.usages.impl.rules.UsageTypeProvider;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jf.dexlib2.Opcode;
+import org.jf.smalidea.SmaliTokens;
+import org.jf.smalidea.psi.impl.*;
+
+import java.util.EnumSet;
+import java.util.Set;
+
+public class SmaliUsageTypeProvider implements UsageTypeProvider {
+
+ static final UsageType CLASS_DECLARATION = new UsageType("Class declaration");
+ static final UsageType VERIFICATION_ERROR = new UsageType("Usage in verification error");
+ static final UsageType FIELD_TYPE_REFERENCE = new UsageType("Usage as field type in a field reference");
+ static final UsageType FIELD_DECLARING_TYPE_REFERENCE = new UsageType("Usage as a declaring type in a field reference");
+ static final UsageType METHOD_RETURN_TYPE_REFERENCE = new UsageType("Usage as return type in a method reference");
+ static final UsageType METHOD_PARAM_REFERENCE = new UsageType("Usage as parameter in a method reference");
+ static final UsageType METHOD_DECLARING_TYPE_REFERENCE = new UsageType("Usage as a declaring type in a method reference");
+ static final UsageType LITERAL = new UsageType("Usage as a literal");
+
+ @Nullable @Override public UsageType getUsageType(PsiElement element) {
+ if (element instanceof PsiReference) {
+ PsiElement referenced = ((PsiReference) element).resolve();
+ if (referenced != null) {
+ if (referenced instanceof PsiClass) {
+ return findClassUsageType(element);
+ }
+ }
+ }
+ return UsageType.UNCLASSIFIED;
+ }
+
+ private final Set<Opcode> newArrayInstructions = EnumSet.of(Opcode.FILLED_NEW_ARRAY, Opcode.NEW_ARRAY,
+ Opcode.FILLED_NEW_ARRAY_RANGE);
+
+ @Nullable
+ private UsageType findClassUsageType(@NotNull PsiElement element) {
+ PsiElement originalElement = element;
+
+ while (element != null) {
+ if (element instanceof SmaliFieldReference) {
+ PsiElement prev = originalElement.getPrevSibling();
+ while (prev != null) {
+ // if the element is to the right of a colon, then it is the field type, otherwise it is
+ // the declaring class
+ if (prev.getNode().getElementType() == SmaliTokens.COLON) {
+ return FIELD_TYPE_REFERENCE;
+ }
+ prev = prev.getPrevSibling();
+ }
+ return FIELD_DECLARING_TYPE_REFERENCE;
+ } else if (element instanceof SmaliMethodReferenceParamList) {
+ return METHOD_PARAM_REFERENCE;
+ } else if (element instanceof SmaliMethodReference) {
+ PsiElement prev = originalElement.getPrevSibling();
+ while (prev != null) {
+ IElementType elementType = prev.getNode().getElementType();
+ // if the element is to the right of a close paren, then it is the return type,
+ // otherwise it is the declaring class. Any parameter type will be taken care of by the previous
+ // "if" for SmaliMethodReferenceParamList
+ if (elementType == SmaliTokens.CLOSE_PAREN) {
+ return METHOD_RETURN_TYPE_REFERENCE;
+ }
+ prev = prev.getPrevSibling();
+ }
+ return METHOD_DECLARING_TYPE_REFERENCE;
+ } else if (element instanceof SmaliInstruction) {
+ Opcode opcode = ((SmaliInstruction) element).getOpcode();
+ if (opcode == Opcode.INSTANCE_OF) {
+ return UsageType.CLASS_INSTANCE_OF;
+ } else if (opcode == Opcode.CHECK_CAST) {
+ return UsageType.CLASS_CAST_TO;
+ } else if (newArrayInstructions.contains(opcode)) {
+ return UsageType.CLASS_NEW_ARRAY;
+ } else if (opcode == Opcode.NEW_INSTANCE) {
+ return UsageType.CLASS_NEW_OPERATOR;
+ } else if (opcode == Opcode.CONST_CLASS) {
+ return UsageType.CLASS_CLASS_OBJECT_ACCESS;
+ } else if (opcode == Opcode.THROW_VERIFICATION_ERROR) {
+ return VERIFICATION_ERROR;
+ }
+ } else if (element instanceof SmaliSuperStatement || element instanceof SmaliImplementsStatement) {
+ return UsageType.CLASS_EXTENDS_IMPLEMENTS_LIST;
+ } else if (element instanceof SmaliClassStatement) {
+ return CLASS_DECLARATION;
+ } else if (element instanceof SmaliMethodParamList) {
+ return UsageType.CLASS_METHOD_PARAMETER_DECLARATION;
+ } else if (element instanceof SmaliMethodPrototype) {
+ return UsageType.CLASS_METHOD_RETURN_TYPE;
+ } else if (element instanceof SmaliField) {
+ return UsageType.CLASS_FIELD_DECLARATION;
+ } else if (element instanceof SmaliCatchStatement) {
+ return UsageType.CLASS_CATCH_CLAUSE_PARAMETER_DECLARATION;
+ } else if (element instanceof SmaliLocalDebugStatement) {
+ return UsageType.CLASS_LOCAL_VAR_DECLARATION;
+ } else if (element instanceof SmaliAnnotation) {
+ return UsageType.ANNOTATION;
+ } else if (element instanceof SmaliLiteral) {
+ return LITERAL;
+ }
+ element = element.getParent();
+ }
+ return UsageType.UNCLASSIFIED;
+ }
+}
diff --git a/smalidea/src/test/java/org/jf/smalidea/findUsages/ClassUsageTypeTest.java b/smalidea/src/test/java/org/jf/smalidea/findUsages/ClassUsageTypeTest.java
new file mode 100644
index 0000000..18a6194
--- /dev/null
+++ b/smalidea/src/test/java/org/jf/smalidea/findUsages/ClassUsageTypeTest.java
@@ -0,0 +1,74 @@
+package org.jf.smalidea.findUsages;
+
+import com.intellij.usages.impl.rules.UsageType;
+
+public class ClassUsageTypeTest extends UsageTypeTest {
+ public ClassUsageTypeTest() {
+ super(new SmaliUsageTypeProvider());
+ }
+
+ public void testClassUsageTypes() throws Exception {
+ doTest("blah.smali", "" +
+ ".class public Lbl<ref:1>ah;\n" +
+ ".super Lbl<ref:2>ah;\n" +
+ ".implements Lbl<ref:3>ah;\n" +
+ "\n" +
+ ".annotation build Lbl<ref:22>ah;\n" +
+ " value = .subannotation Lbl<ref:23>ah;\n" +
+ " value = Lbl<ref:24>ah;\n" +
+ " .end subannotation\n" +
+ ".end annotation\n" +
+ "\n" +
+ ".field static public blah:Lbl<ref:4>ah; = Lbl<ref:25>ah;\n" +
+ "\n" +
+ ".method public blah(Lbl<ref:5>ah;)Lbl<ref:6>ah;\n" +
+ " .registers 2\n" +
+ " .local p0, \"this\":Lbl<ref:7>ah;\n" +
+ "\n" +
+ " :start\n" +
+ " iget-object v0, v0, Lbl<ref:8>ah;->blah:Lbl<ref:9>ah;\n" +
+ "\n" +
+ " invoke-virtual {v0}, Lbl<ref:10>ah;->blah(Lbl<ref:11>ah;)Lbl<ref:12>ah;\n" +
+ "\n" +
+ " instance-of v0, v0, Lbl<ref:13>ah;\n" +
+ " check-cast v0, Lbl<ref:14>ah;\n" +
+ " new-instance v0, Lbl<ref:15>ah;\n" +
+ " const-class v0, Lbl<ref:16>ah;\n" +
+ " throw-verification-error generic-error, Lbl<ref:17>ah;\n" +
+ "\n" +
+ " filled-new-array {v0, v0, v0, v0, v0}, Lbl<ref:18>ah;\n" +
+ " new-array v0, v0, Lbl<ref:19>ah;\n" +
+ " filled-new-array/range {v0}, Lbl<ref:20>ah;\n" +
+ " :end\n" +
+ "\n" +
+ " .catch Lbl<ref:21>ah; { :start .. :end } :handler\n" +
+ " :handler\n" +
+ " return-void\n" +
+ ".end method",
+ 1, SmaliUsageTypeProvider.CLASS_DECLARATION,
+ 2, UsageType.CLASS_EXTENDS_IMPLEMENTS_LIST,
+ 3, UsageType.CLASS_EXTENDS_IMPLEMENTS_LIST,
+ 4, UsageType.CLASS_FIELD_DECLARATION,
+ 5, UsageType.CLASS_METHOD_PARAMETER_DECLARATION,
+ 6, UsageType.CLASS_METHOD_RETURN_TYPE,
+ 7, UsageType.CLASS_LOCAL_VAR_DECLARATION,
+ 8, SmaliUsageTypeProvider.FIELD_DECLARING_TYPE_REFERENCE,
+ 9, SmaliUsageTypeProvider.FIELD_TYPE_REFERENCE,
+ 10, SmaliUsageTypeProvider.METHOD_DECLARING_TYPE_REFERENCE,
+ 11, SmaliUsageTypeProvider.METHOD_PARAM_REFERENCE,
+ 12, SmaliUsageTypeProvider.METHOD_RETURN_TYPE_REFERENCE,
+ 13, UsageType.CLASS_INSTANCE_OF,
+ 14, UsageType.CLASS_CAST_TO,
+ 15, UsageType.CLASS_NEW_OPERATOR,
+ 16, UsageType.CLASS_CLASS_OBJECT_ACCESS,
+ 17, SmaliUsageTypeProvider.VERIFICATION_ERROR,
+ 18, UsageType.CLASS_NEW_ARRAY,
+ 19, UsageType.CLASS_NEW_ARRAY,
+ 20, UsageType.CLASS_NEW_ARRAY,
+ 21, UsageType.CLASS_CATCH_CLAUSE_PARAMETER_DECLARATION,
+ 22, UsageType.ANNOTATION,
+ 23, UsageType.ANNOTATION,
+ 24, SmaliUsageTypeProvider.LITERAL,
+ 25, SmaliUsageTypeProvider.LITERAL);
+ }
+}
diff --git a/smalidea/src/test/java/org/jf/smalidea/findUsages/UsageTypeTest.java b/smalidea/src/test/java/org/jf/smalidea/findUsages/UsageTypeTest.java
new file mode 100644
index 0000000..44c2f49
--- /dev/null
+++ b/smalidea/src/test/java/org/jf/smalidea/findUsages/UsageTypeTest.java
@@ -0,0 +1,73 @@
+package org.jf.smalidea.findUsages;
+
+import com.google.common.collect.Maps;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.impl.source.resolve.reference.impl.PsiMultiReference;
+import com.intellij.testFramework.PsiTestCase;
+import com.intellij.usages.impl.rules.UsageType;
+import com.intellij.usages.impl.rules.UsageTypeProvider;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Assert;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public abstract class UsageTypeTest extends PsiTestCase {
+ // e.g. <ref:1>, <ref:1234>, etc.
+ private static final Pattern REF_PATTERN = Pattern.compile("(<ref:([0-9]+)>)");
+
+ @NotNull
+ private final UsageTypeProvider usageTypeProvider;
+
+ public UsageTypeTest(@NotNull UsageTypeProvider usageTypeProvider) {
+ this.usageTypeProvider = usageTypeProvider;
+ }
+
+ protected void doTest(@NotNull String fileName, @NotNull String text, @NotNull Object... expectedUsageTypes)
+ throws Exception {
+ Assert.assertTrue(expectedUsageTypes.length % 2 == 0);
+
+ Map<Integer, UsageType> expectedUsageTypesMap = Maps.newHashMap();
+ for (int i=0; i<expectedUsageTypes.length; i+=2) {
+ expectedUsageTypesMap.put((Integer) expectedUsageTypes[i], (UsageType) expectedUsageTypes[i + 1]);
+ }
+
+ PsiFile psiFile = createFile(fileName, REF_PATTERN.matcher(text).replaceAll(""));
+ Map<Integer, Integer> refIndexMap = getRefIndexes(text);
+
+ for (Map.Entry<Integer, Integer> entry: refIndexMap.entrySet()) {
+ int refId = entry.getKey();
+ int index = entry.getValue();
+
+ PsiReference reference = psiFile.getFirstChild().findReferenceAt(index);
+ Assert.assertNotNull(reference);
+ if (reference instanceof PsiMultiReference) {
+ // If there are multiple reference parents, the default seems to be the last one,
+ // i.e. the highest parent. We actually want the lowest one here.
+ reference = ((PsiMultiReference) reference).getReferences()[0];
+ }
+
+ UsageType usageType = usageTypeProvider.getUsageType(reference.getElement());
+ Assert.assertNotNull(usageType);
+ Assert.assertSame(expectedUsageTypesMap.get(refId), usageType);
+ expectedUsageTypesMap.remove(refId);
+ }
+ Assert.assertTrue(expectedUsageTypesMap.isEmpty());
+ }
+
+ @NotNull
+ private Map<Integer, Integer> getRefIndexes(@NotNull String text) {
+ Matcher m = REF_PATTERN.matcher(text);
+ int correction = 0;
+ Map<Integer, Integer> refIndexes = new HashMap<Integer, Integer>();
+ while (m.find()) {
+ int refId = Integer.parseInt(m.group(2));
+ refIndexes.put(refId, m.start() - correction);
+ correction += m.end() - m.start();
+ }
+ return refIndexes;
+ }
+}