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