| /* |
| * Copyright (C) 2012 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.lint.checks; |
| |
| import static com.android.SdkConstants.CONSTRUCTOR_NAME; |
| |
| import com.android.annotations.Nullable; |
| import com.android.utils.Pair; |
| import com.google.common.collect.Lists; |
| |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| /** |
| * Represents a class and its methods/fields. |
| * |
| * {@link #getSince()} gives the API level it was introduced. |
| * |
| * {@link #getMethod} returns when the method was introduced. |
| * {@link #getField} returns when the field was introduced. |
| */ |
| public class ApiClass { |
| |
| private final String mName; |
| private final int mSince; |
| |
| private final List<Pair<String, Integer>> mSuperClasses = Lists.newArrayList(); |
| private final List<Pair<String, Integer>> mInterfaces = Lists.newArrayList(); |
| |
| private final Map<String, Integer> mFields = new HashMap<String, Integer>(); |
| private final Map<String, Integer> mMethods = new HashMap<String, Integer>(); |
| |
| ApiClass(String name, int since) { |
| mName = name; |
| mSince = since; |
| } |
| |
| /** |
| * Returns the name of the class. |
| * @return the name of the class |
| */ |
| String getName() { |
| return mName; |
| } |
| |
| /** |
| * Returns when the class was introduced. |
| * @return the api level the class was introduced. |
| */ |
| int getSince() { |
| return mSince; |
| } |
| |
| /** |
| * Returns when a field was added, or null if it doesn't exist. |
| * @param name the name of the field. |
| * @param info the corresponding info |
| */ |
| Integer getField(String name, Api info) { |
| // The field can come from this class or from a super class or an interface |
| // The value can never be lower than this introduction of this class. |
| // When looking at super classes and interfaces, it can never be lower than when the |
| // super class or interface was added as a super class or interface to this class. |
| // Look at all the values and take the lowest. |
| // For instance: |
| // This class A is introduced in 5 with super class B. |
| // In 10, the interface C was added. |
| // Looking for SOME_FIELD we get the following: |
| // Present in A in API 15 |
| // Present in B in API 11 |
| // Present in C in API 7. |
| // The answer is 10, which is when C became an interface |
| int min = Integer.MAX_VALUE; |
| Integer i = mFields.get(name); |
| if (i != null) { |
| min = i; |
| } |
| |
| // now look at the super classes |
| for (Pair<String, Integer> superClassPair : mSuperClasses) { |
| ApiClass superClass = info.getClass(superClassPair.getFirst()); |
| if (superClass != null) { |
| i = superClass.getField(name, info); |
| if (i != null) { |
| int tmp = superClassPair.getSecond() > i ? superClassPair.getSecond() : i; |
| if (tmp < min) { |
| min = tmp; |
| } |
| } |
| } |
| } |
| |
| // now look at the interfaces |
| for (Pair<String, Integer> superClassPair : mInterfaces) { |
| ApiClass superClass = info.getClass(superClassPair.getFirst()); |
| if (superClass != null) { |
| i = superClass.getField(name, info); |
| if (i != null) { |
| int tmp = superClassPair.getSecond() > i ? superClassPair.getSecond() : i; |
| if (tmp < min) { |
| min = tmp; |
| } |
| } |
| } |
| } |
| |
| return min; |
| } |
| |
| /** |
| * Returns when a method was added, or null if it doesn't exist. This goes through the super |
| * class to find method only present there. |
| * @param methodSignature the method signature |
| */ |
| int getMethod(String methodSignature, Api info) { |
| // The method can come from this class or from a super class. |
| // The value can never be lower than this introduction of this class. |
| // When looking at super classes, it can never be lower than when the super class became |
| // a super class of this class. |
| // Look at all the values and take the lowest. |
| // For instance: |
| // This class A is introduced in 5 with super class B. |
| // In 10, the super class changes to C. |
| // Looking for foo() we get the following: |
| // Present in A in API 15 |
| // Present in B in API 11 |
| // Present in C in API 7. |
| // The answer is 10, which is when C became the super class. |
| int min = Integer.MAX_VALUE; |
| Integer i = mMethods.get(methodSignature); |
| if (i != null) { |
| min = i; |
| |
| // Constructors aren't inherited |
| if (methodSignature.startsWith(CONSTRUCTOR_NAME)) { |
| return i; |
| } |
| } |
| |
| // now look at the super classes |
| for (Pair<String, Integer> superClassPair : mSuperClasses) { |
| ApiClass superClass = info.getClass(superClassPair.getFirst()); |
| if (superClass != null) { |
| i = superClass.getMethod(methodSignature, info); |
| if (i != null) { |
| int tmp = superClassPair.getSecond() > i ? superClassPair.getSecond() : i; |
| if (tmp < min) { |
| min = tmp; |
| } |
| } |
| } |
| } |
| |
| // now look at the interfaces classes |
| for (Pair<String, Integer> interfacePair : mInterfaces) { |
| ApiClass superClass = info.getClass(interfacePair.getFirst()); |
| if (superClass != null) { |
| i = superClass.getMethod(methodSignature, info); |
| if (i != null) { |
| int tmp = interfacePair.getSecond() > i ? interfacePair.getSecond() : i; |
| if (tmp < min) { |
| min = tmp; |
| } |
| } |
| } |
| } |
| |
| return min; |
| } |
| |
| void addField(String name, int since) { |
| Integer i = mFields.get(name); |
| if (i == null || i.intValue() > since) { |
| mFields.put(name, Integer.valueOf(since)); |
| } |
| } |
| |
| void addMethod(String name, int since) { |
| // Strip off the method type at the end to ensure that the code which |
| // produces inherited methods doesn't get confused and end up multiple entries. |
| // For example, java/nio/Buffer has the method "array()Ljava/lang/Object;", |
| // and the subclass java/nio/ByteBuffer has the method "array()[B". We want |
| // the lookup on mMethods to associate the ByteBuffer array method to be |
| // considered overriding the Buffer method. |
| int index = name.indexOf(')'); |
| if (index != -1) { |
| name = name.substring(0, index + 1); |
| } |
| |
| Integer i = mMethods.get(name); |
| if (i == null || i.intValue() > since) { |
| mMethods.put(name, Integer.valueOf(since)); |
| } |
| } |
| |
| void addSuperClass(String superClass, int since) { |
| addToArray(mSuperClasses, superClass, since); |
| } |
| |
| void addInterface(String interfaceClass, int since) { |
| addToArray(mInterfaces, interfaceClass, since); |
| } |
| |
| static void addToArray(List<Pair<String, Integer>> list, String name, int value) { |
| // check if we already have that name (at a lower level) |
| for (Pair<String, Integer> pair : list) { |
| if (name.equals(pair.getFirst())) { |
| return; |
| } |
| } |
| |
| list.add(Pair.of(name, Integer.valueOf(value))); |
| |
| } |
| |
| @Nullable |
| public String getPackage() { |
| int index = mName.lastIndexOf('/'); |
| if (index != -1) { |
| return mName.substring(0, index); |
| } |
| |
| return null; |
| } |
| |
| @Override |
| public String toString() { |
| return mName; |
| } |
| |
| /** |
| * Returns the set of all methods, including inherited |
| * ones. |
| * |
| * @param info the api to look up super classes from |
| * @return a set containing all the members fields |
| */ |
| Set<String> getAllMethods(Api info) { |
| Set<String> members = new HashSet<String>(100); |
| addAllMethods(info, members, true /*includeConstructors*/); |
| |
| return members; |
| } |
| |
| private void addAllMethods(Api info, Set<String> set, boolean includeConstructors) { |
| if (!includeConstructors) { |
| for (String method : mMethods.keySet()) { |
| if (!method.startsWith(CONSTRUCTOR_NAME)) { |
| set.add(method); |
| } |
| } |
| } else { |
| for (String method : mMethods.keySet()) { |
| set.add(method); |
| } |
| } |
| |
| for (Pair<String, Integer> superClass : mSuperClasses) { |
| ApiClass clz = info.getClass(superClass.getFirst()); |
| assert clz != null : superClass.getSecond(); |
| if (clz != null) { |
| clz.addAllMethods(info, set, false); |
| } |
| } |
| |
| // Get methods from implemented interfaces as well; |
| for (Pair<String, Integer> superClass : mInterfaces) { |
| ApiClass clz = info.getClass(superClass.getFirst()); |
| assert clz != null : superClass.getSecond(); |
| if (clz != null) { |
| clz.addAllMethods(info, set, false); |
| } |
| } |
| } |
| |
| /** |
| * Returns the set of all fields, including inherited |
| * ones. |
| * |
| * @param info the api to look up super classes from |
| * @return a set containing all the fields |
| */ |
| Set<String> getAllFields(Api info) { |
| Set<String> members = new HashSet<String>(100); |
| addAllFields(info, members); |
| |
| return members; |
| } |
| |
| private void addAllFields(Api info, Set<String> set) { |
| for (String field : mFields.keySet()) { |
| set.add(field); |
| } |
| |
| for (Pair<String, Integer> superClass : mSuperClasses) { |
| ApiClass clz = info.getClass(superClass.getFirst()); |
| assert clz != null : superClass.getSecond(); |
| if (clz != null) { |
| clz.addAllFields(info, set); |
| } |
| } |
| |
| // Get methods from implemented interfaces as well; |
| for (Pair<String, Integer> superClass : mInterfaces) { |
| ApiClass clz = info.getClass(superClass.getFirst()); |
| assert clz != null : superClass.getSecond(); |
| if (clz != null) { |
| clz.addAllFields(info, set); |
| } |
| } |
| } |
| |
| /* This code can be used to scan through all the fields and look for fields |
| that have moved to a higher class: |
| Field android/view/MotionEvent#CREATOR has api=1 but parent android/view/InputEvent provides it as 9 |
| Field android/provider/ContactsContract$CommonDataKinds$Organization#PHONETIC_NAME has api=5 but parent android/provider/ContactsContract$ContactNameColumns provides it as 11 |
| Field android/widget/ListView#CHOICE_MODE_MULTIPLE has api=1 but parent android/widget/AbsListView provides it as 11 |
| Field android/widget/ListView#CHOICE_MODE_NONE has api=1 but parent android/widget/AbsListView provides it as 11 |
| Field android/widget/ListView#CHOICE_MODE_SINGLE has api=1 but parent android/widget/AbsListView provides it as 11 |
| Field android/view/KeyEvent#CREATOR has api=1 but parent android/view/InputEvent provides it as 9 |
| This is used for example in the ApiDetector to filter out warnings which result |
| when people follow Eclipse's advice to replace |
| ListView.CHOICE_MODE_MULTIPLE |
| references with |
| AbsListView.CHOICE_MODE_MULTIPLE |
| since the latter has API=11 and the former has API=1; since the constant is unchanged |
| between the two, and the literal is copied into the class, using the AbsListView |
| reference works. |
| public void checkFields(Api info) { |
| fieldLoop: |
| for (String field : mFields.keySet()) { |
| Integer since = getField(field, info); |
| if (since == null || since == Integer.MAX_VALUE) { |
| continue; |
| } |
| |
| for (Pair<String, Integer> superClass : mSuperClasses) { |
| ApiClass clz = info.getClass(superClass.getFirst()); |
| assert clz != null : superClass.getSecond(); |
| if (clz != null) { |
| Integer superSince = clz.getField(field, info); |
| if (superSince == Integer.MAX_VALUE) { |
| continue; |
| } |
| |
| if (superSince != null && superSince > since) { |
| String declaredIn = clz.findFieldDeclaration(info, field); |
| System.out.println("Field " + getName() + "#" + field + " has api=" |
| + since + " but parent " + declaredIn + " provides it as " |
| + superSince); |
| continue fieldLoop; |
| } |
| } |
| } |
| |
| // Get methods from implemented interfaces as well; |
| for (Pair<String, Integer> superClass : mInterfaces) { |
| ApiClass clz = info.getClass(superClass.getFirst()); |
| assert clz != null : superClass.getSecond(); |
| if (clz != null) { |
| Integer superSince = clz.getField(field, info); |
| if (superSince == Integer.MAX_VALUE) { |
| continue; |
| } |
| if (superSince != null && superSince > since) { |
| String declaredIn = clz.findFieldDeclaration(info, field); |
| System.out.println("Field " + getName() + "#" + field + " has api=" |
| + since + " but parent " + declaredIn + " provides it as " |
| + superSince); |
| continue fieldLoop; |
| } |
| } |
| } |
| } |
| } |
| |
| private String findFieldDeclaration(Api info, String name) { |
| if (mFields.containsKey(name)) { |
| return getName(); |
| } |
| for (Pair<String, Integer> superClass : mSuperClasses) { |
| ApiClass clz = info.getClass(superClass.getFirst()); |
| assert clz != null : superClass.getSecond(); |
| if (clz != null) { |
| String declaredIn = clz.findFieldDeclaration(info, name); |
| if (declaredIn != null) { |
| return declaredIn; |
| } |
| } |
| } |
| |
| // Get methods from implemented interfaces as well; |
| for (Pair<String, Integer> superClass : mInterfaces) { |
| ApiClass clz = info.getClass(superClass.getFirst()); |
| assert clz != null : superClass.getSecond(); |
| if (clz != null) { |
| String declaredIn = clz.findFieldDeclaration(info, name); |
| if (declaredIn != null) { |
| return declaredIn; |
| } |
| } |
| } |
| |
| return null; |
| } |
| */ |
| } |