Home | History | Annotate | Download | only in checks
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.tools.lint.checks;
     18 
     19 import com.android.tools.lint.detector.api.Category;
     20 import com.android.tools.lint.detector.api.ClassContext;
     21 import com.android.tools.lint.detector.api.Context;
     22 import com.android.tools.lint.detector.api.Detector;
     23 import com.android.tools.lint.detector.api.Issue;
     24 import com.android.tools.lint.detector.api.Location;
     25 import com.android.tools.lint.detector.api.Scope;
     26 import com.android.tools.lint.detector.api.Severity;
     27 import com.android.tools.lint.detector.api.Speed;
     28 import com.google.common.collect.Maps;
     29 
     30 import org.objectweb.asm.Opcodes;
     31 import org.objectweb.asm.tree.AbstractInsnNode;
     32 import org.objectweb.asm.tree.ClassNode;
     33 import org.objectweb.asm.tree.FieldInsnNode;
     34 import org.objectweb.asm.tree.InsnList;
     35 import org.objectweb.asm.tree.LineNumberNode;
     36 import org.objectweb.asm.tree.MethodInsnNode;
     37 import org.objectweb.asm.tree.MethodNode;
     38 
     39 import java.io.File;
     40 import java.util.ArrayList;
     41 import java.util.EnumSet;
     42 import java.util.HashSet;
     43 import java.util.List;
     44 import java.util.Map;
     45 import java.util.Set;
     46 
     47 /**
     48  * Looks for getter calls within the same class that could be replaced by
     49  * direct field references instead.
     50  */
     51 public class FieldGetterDetector extends Detector implements Detector.ClassScanner {
     52     /** The main issue discovered by this detector */
     53     public static final Issue ISSUE = Issue.create(
     54             "FieldGetter", //$NON-NLS-1$
     55             "Suggests replacing uses of getters with direct field access within a class",
     56 
     57             "Accessing a field within the class that defines a getter for that field is " +
     58             "at least 3 times faster than calling the getter. For simple getters that do " +
     59             "nothing other than return the field, you might want to just reference the " +
     60             "local field directly instead.",
     61 
     62             Category.PERFORMANCE,
     63             4,
     64             Severity.WARNING,
     65             FieldGetterDetector.class,
     66             EnumSet.of(Scope.CLASS_FILE)).
     67             // This is a micro-optimization: not enabled by default
     68             setEnabledByDefault(false).setMoreInfo(
     69            "http://developer.android.com/guide/practices/design/performance.html#internal_get_set"); //$NON-NLS-1$
     70 
     71     /** Constructs a new {@link FieldGetterDetector} check */
     72     public FieldGetterDetector() {
     73     }
     74 
     75     @Override
     76     public boolean appliesTo(Context context, File file) {
     77         return true;
     78     }
     79 
     80     @Override
     81     public Speed getSpeed() {
     82         return Speed.FAST;
     83     }
     84 
     85     // ---- Implements ClassScanner ----
     86 
     87     @SuppressWarnings("rawtypes")
     88     @Override
     89     public void checkClass(ClassContext context, ClassNode classNode) {
     90         List<Entry> pendingCalls = null;
     91         int currentLine = 0;
     92         List methodList = classNode.methods;
     93         for (Object m : methodList) {
     94             MethodNode method = (MethodNode) m;
     95             InsnList nodes = method.instructions;
     96             for (int i = 0, n = nodes.size(); i < n; i++) {
     97                 AbstractInsnNode instruction = nodes.get(i);
     98                 int type = instruction.getType();
     99                 if (type == AbstractInsnNode.LINE) {
    100                     currentLine = ((LineNumberNode) instruction).line;
    101                 } else if (type == AbstractInsnNode.METHOD_INSN) {
    102                     MethodInsnNode node = (MethodInsnNode) instruction;
    103                     String name = node.name;
    104                     String owner = node.owner;
    105 
    106                     if (((name.startsWith("get") && name.length() > 3     //$NON-NLS-1$
    107                             && Character.isUpperCase(name.charAt(3)))
    108                         || (name.startsWith("is") && name.length() > 2    //$NON-NLS-1$
    109                             && Character.isUpperCase(name.charAt(2))))
    110                             && owner.equals(classNode.name)) {
    111                         // Calling a potential getter method on self. We now need to
    112                         // investigate the method body of the getter call and make sure
    113                         // it's really a plain getter, not just a method which happens
    114                         // to have a method name like a getter, or a method which not
    115                         // only returns a field but possibly computes it or performs
    116                         // other initialization or side effects. This is done in a
    117                         // second pass over the bytecode, initiated by the finish()
    118                         // method.
    119                         if (pendingCalls == null) {
    120                             pendingCalls = new ArrayList<Entry>();
    121                         }
    122 
    123                         pendingCalls.add(new Entry(name, currentLine, method));
    124                     }
    125                 }
    126             }
    127         }
    128 
    129         if (pendingCalls != null) {
    130             Set<String> names = new HashSet<String>(pendingCalls.size());
    131             for (Entry entry : pendingCalls) {
    132                 names.add(entry.name);
    133             }
    134 
    135             Map<String, String> getters = checkMethods(context.getClassNode(), names);
    136             if (getters.size() > 0) {
    137                 File source = context.getSourceFile();
    138                 String contents = context.getSourceContents();
    139                 for (String getter : getters.keySet()) {
    140                     for (Entry entry : pendingCalls) {
    141                         String name = entry.name;
    142                         // There can be more than one reference to the same name:
    143                         // one for each call site
    144                         if (name.equals(getter)) {
    145                             int line = entry.lineNumber;
    146                             Location location = null;
    147                             if (source != null) {
    148                                 // ASM line numbers are 1-based, Lint needs 0-based
    149                                 location = Location.create(source, contents, line - 1, name,
    150                                         null);
    151                             } else {
    152                                 location = Location.create(context.file);
    153                             }
    154                             String fieldName = getters.get(getter);
    155                             if (fieldName == null) {
    156                                 fieldName = "";
    157                             }
    158                             context.report(ISSUE, entry.method, location, String.format(
    159                                 "Calling getter method %1$s() on self is " +
    160                                 "slower than field access (%2$s)", getter, fieldName), fieldName);
    161                         }
    162                     }
    163                 }
    164             }
    165         }
    166     }
    167 
    168     // Holder class for getters to be checked
    169     private static class Entry {
    170         public final String name;
    171         public final int lineNumber;
    172         public final MethodNode method;
    173 
    174         public Entry(String name, int lineNumber, MethodNode method) {
    175             super();
    176             this.name = name;
    177             this.lineNumber = lineNumber;
    178             this.method = method;
    179         }
    180     }
    181 
    182     // Validate that these getter methods are really just simple field getters
    183     // like these int and STring getters:
    184     // public int getFoo();
    185     //   Code:
    186     //    0:   aload_0
    187     //    1:   getfield    #21; //Field mFoo:I
    188     //    4:   ireturn
    189     //
    190     // public java.lang.String getBar();
    191     //   Code:
    192     //    0:   aload_0
    193     //    1:   getfield    #25; //Field mBar:Ljava/lang/String;
    194     //    4:   areturn
    195     //
    196     // Returns a map of valid getters as keys, and if the field name is found, the field name
    197     // for each getter as its value.
    198     private static Map<String, String> checkMethods(ClassNode classNode, Set<String> names) {
    199         Map<String, String> validGetters = Maps.newHashMap();
    200         @SuppressWarnings("rawtypes")
    201         List methods = classNode.methods;
    202         String fieldName = null;
    203         checkMethod:
    204         for (Object methodObject : methods) {
    205             MethodNode method = (MethodNode) methodObject;
    206             if (names.contains(method.name)
    207                     && method.desc.startsWith("()")) { //$NON-NLS-1$ // (): No arguments
    208                 InsnList instructions = method.instructions;
    209                 int mState = 1;
    210                 for (AbstractInsnNode curr = instructions.getFirst();
    211                         curr != null;
    212                         curr = curr.getNext()) {
    213                     switch (curr.getOpcode()) {
    214                         case -1:
    215                             // Skip label and line number nodes
    216                             continue;
    217                         case Opcodes.ALOAD:
    218                             if (mState == 1) {
    219                                 fieldName = null;
    220                                 mState = 2;
    221                             } else {
    222                                 continue checkMethod;
    223                             }
    224                             break;
    225                         case Opcodes.GETFIELD:
    226                             if (mState == 2) {
    227                                 FieldInsnNode field = (FieldInsnNode) curr;
    228                                 fieldName = field.name;
    229                                 mState = 3;
    230                             } else {
    231                                 continue checkMethod;
    232                             }
    233                             break;
    234                         case Opcodes.ARETURN:
    235                         case Opcodes.FRETURN:
    236                         case Opcodes.IRETURN:
    237                         case Opcodes.DRETURN:
    238                         case Opcodes.LRETURN:
    239                         case Opcodes.RETURN:
    240                             if (mState == 3) {
    241                                 validGetters.put(method.name, fieldName);
    242                             }
    243                             continue checkMethod;
    244                         default:
    245                             continue checkMethod;
    246                     }
    247                 }
    248             }
    249         }
    250 
    251         return validGetters;
    252     }
    253 }
    254