/testability-explorer/src/main/java/com/google/test/metric/TestabilityVisitor.java
Java | 494 lines | 369 code | 56 blank | 69 comment | 48 complexity | 52ab5699c8720fb92c470f278235b968 MD5 | raw file
1/* 2 * Copyright 2007 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16package com.google.test.metric; 17 18import static com.google.test.metric.Reason.NON_OVERRIDABLE_METHOD_CALL; 19import com.google.test.metric.method.Constant; 20import com.google.test.metric.method.op.turing.Operation; 21 22import java.io.PrintStream; 23import java.util.HashMap; 24import java.util.HashSet; 25import java.util.List; 26import java.util.Map; 27import java.util.Set; 28 29public class TestabilityVisitor { 30 31 public static class CostRecordingFrame extends Frame { 32 33 private final MethodCost methodCost; 34 private final Map<MethodInfo, MethodCost> methodCosts; 35 private final int remainingDepth; 36 37 public CostRecordingFrame(PrintStream err, ClassRepository classRepository, 38 ParentFrame parentFrame, WhiteList whitelist, 39 VariableState globalVariables, Map<MethodInfo, MethodCost> methodCosts, 40 Set<MethodInfo> alreadyVisited, MethodInfo method, int remainingDepth) { 41 super(err, classRepository, parentFrame, whitelist, globalVariables, 42 alreadyVisited, method); 43 this.methodCosts = methodCosts; 44 this.remainingDepth = remainingDepth; 45 this.methodCost = getMethodCostCache(method); 46 } 47 48 public CostRecordingFrame(PrintStream err, ClassRepository classRepository, 49 WhiteList whitelist, VariableState globalVariables, MethodInfo method, 50 int remainingDepth) { 51 this(err, classRepository, new ParentFrame(globalVariables), whitelist, 52 globalVariables, new HashMap<MethodInfo, MethodCost>(), 53 new HashSet<MethodInfo>(), method, remainingDepth); 54 } 55 56 @Override 57 protected void addCyclomaticCost(int lineNumber) { 58 super.addCyclomaticCost(lineNumber); 59 SourceLocation location = new SourceLocation(method.getClassInfo().getFileName(), lineNumber); 60 ViolationCost cost = new CyclomaticCost(location, Cost.cyclomatic(1)); 61 methodCost.addCostSource(cost); 62 } 63 64 @Override 65 protected void addGlobalCost(int lineNumber, Variable variable) { 66 super.addGlobalCost(lineNumber, variable); 67 SourceLocation location = new SourceLocation(method.getClassInfo().getFileName(), lineNumber); 68 ViolationCost cost = new GlobalCost(location, variable, Cost.global(1)); 69 methodCost.addCostSource(cost); 70 } 71 72 @Override 73 protected void addLoDCost(int lineNumber, MethodInfo method, int distance) { 74 super.addLoDCost(lineNumber, method, distance); 75 SourceLocation location = new SourceLocation(this.method.getClassInfo().getFileName(), 76 lineNumber); 77 ViolationCost cost = new LoDViolation(location, method.getName(), 78 Cost.lod(distance), distance); 79 methodCost.addCostSource(cost); 80 } 81 82 @Override 83 protected void addMethodInvocationCost(int lineNumber, MethodInfo to, 84 Cost methodInvocationCost, Reason reason) { 85 super.addMethodInvocationCost(lineNumber, to, methodInvocationCost, reason); 86 if (!methodInvocationCost.isEmpty()) { 87 String fileName = (reason.isImplicit() ? to.getClassInfo().getFileName() : 88 this.method.getClassInfo().getFileName()); 89 SourceLocation location = new SourceLocation(fileName, lineNumber); 90 ViolationCost cost; 91 if (to.isConstructor()) { 92 cost = new ConstructorInvocationCost(location, getMethodCostCache(to), reason, 93 methodInvocationCost); 94 } else { 95 cost = new MethodInvocationCost(location, getMethodCostCache(to), reason, 96 methodInvocationCost); 97 } 98 methodCost.addCostSource(cost); 99 } 100 } 101 102 public MethodCost getMethodCost() { 103 return methodCost; 104 } 105 106 protected MethodCost getMethodCostCache(MethodInfo method) { 107 MethodCost methodCost = methodCosts.get(method); 108 if (methodCost == null) { 109 methodCost = new MethodCost(method.getClassInfo().getName(), 110 method.getName(), method.getStartingLineNumber(), 111 method.isConstructor(), method.isStatic(), method.isStaticConstructor()); 112 methodCosts.put(method, methodCost); 113 } 114 return methodCost; 115 } 116 117 /** 118 * Implicit costs are added to the {@code from} method's costs when it is 119 * assumed that the costs must be incurred in order for the {@code from} 120 * method to execute. Example: 121 * 122 * <pre> 123 * void fromMethod() { 124 * this.someObject.toMethod(); 125 * } 126 * </pre> 127 * <p> 128 * We would add the implicit cost of the toMethod() to the fromMethod(). 129 * Implicit Costs consist of: 130 * <ul> 131 * <li>Cost of construction for the someObject field referenced in 132 * fromMethod()</li> 133 * <li>Static initialization blocks in someObject 134 * </ul> 135 * <li>The cost of calling all the methods starting with "set" on 136 * someObject.</ul> 137 * <li>Note that the same implicit costs apply for the class that has the 138 * fromMethod. (Meaning a method will always have the implicit costs of the 139 * containing class and super-classes at a minimum).</li> </ul> 140 * 141 * @param implicitMethod 142 * the method that is getting called by {@code from} and 143 * contributes cost transitively. 144 * @param reason 145 * the type of implicit cost to record, for giving the user 146 * information about why they have the costs they have. 147 * @return 148 */ 149 public void applyImplicitCost(MethodInfo implicitMethod, Reason reason) { 150 if (whitelist != null && whitelist.isClassWhiteListed(implicitMethod.getClassInfo().getName())) { 151 return; 152 } 153 if (implicitMethod.getMethodThis() != null) { 154 variableState.setInjectable(implicitMethod.getMethodThis()); 155 } 156 setInjectable(implicitMethod.getParameters()); 157 Constant ret = new Constant("return", JavaType.OBJECT); 158 int lineNumber = implicitMethod.getStartingLineNumber(); 159 recordNonOveridableMethodCall(reason, lineNumber, implicitMethod, implicitMethod 160 .getMethodThis(), implicitMethod.getParameters(), ret); 161 } 162 163 public MethodCost applyMethodOperations() { 164 for (Integer lineNumberWithComplexity : method.getLinesOfComplexity()) { 165 addCyclomaticCost(lineNumberWithComplexity); 166 } 167 if (method.getMethodThis() != null) { 168 variableState.setInjectable(method.getMethodThis()); 169 } 170 setInjectable(method.getParameters()); 171 Constant returnVariable = new Constant("rootReturn", JavaType.OBJECT); 172 applyMethodOperations(-1, method, method.getMethodThis(), method 173 .getParameters(), returnVariable); 174 return methodCost; 175 } 176 177 @Override 178 protected Frame createChildFrame(MethodInfo method) { 179 if (remainingDepth == 0) { 180 return super.createChildFrame(method); 181 } else { 182 return new CostRecordingFrame(err, classRepository, this, whitelist, 183 globalVariableState, methodCosts, alreadyVisited, method, 184 remainingDepth - 1); 185 } 186 } 187 188 @Override 189 void assignVariable(Variable destination, int lineNumber, 190 ParentFrame sourceFrame, Variable source) { 191 super.assignVariable(destination, lineNumber, sourceFrame, source); 192 int loDCount = sourceFrame.variableState.getLoDCount(source); 193 variableState.setLoDCount(destination, loDCount); 194 } 195 196 @Override 197 protected void incrementLoD(int lineNumber, MethodInfo toMethod, 198 Variable destination, Variable source, ParentFrame destinationFrame) { 199 if (source != null) { 200 int thisCount = variableState.getLoDCount(destination); 201 int distance = thisCount + 1; 202 destinationFrame.variableState.setLoDCount(source, distance); 203 if (distance > 1) { 204 destinationFrame.addLoDCost(lineNumber, toMethod, distance); 205 } 206 } 207 } 208 } 209 210 public static class Frame extends ParentFrame { 211 212 protected final ParentFrame parentFrame; 213 protected final Cost direct = new Cost(); 214 protected final Cost indirect = new Cost(); 215 protected final MethodInfo method; 216 protected Variable returnValue; 217 protected final WhiteList whitelist; 218 protected final PrintStream err; 219 protected final ClassRepository classRepository; 220 protected final Set<MethodInfo> alreadyVisited; 221 222 public Frame(PrintStream err, ClassRepository classRepository, 223 ParentFrame parentFrame, WhiteList whitelist, 224 VariableState globalVariables, Set<MethodInfo> alreadyVisited, 225 MethodInfo method) { 226 super(globalVariables); 227 this.err = err; 228 this.classRepository = classRepository; 229 this.parentFrame = parentFrame; 230 this.whitelist = whitelist; 231 this.alreadyVisited = alreadyVisited; 232 this.method = method; 233 alreadyVisited.add(method); 234 } 235 236 protected void addCyclomaticCost(int lineNumber) { 237 direct.addCyclomaticCost(1); 238 } 239 240 protected void addGlobalCost(int lineNumber, Variable variable) { 241 direct.addGlobalCost(1); 242 } 243 244 @Override 245 protected void addLoDCost(int lineNumber, MethodInfo method, int distance) { 246 direct.addLodDistance(distance); 247 } 248 249 protected void addMethodInvocationCost(int lineNumber, MethodInfo to, 250 Cost methodInvocationCost, Reason reason) { 251 indirect.add(methodInvocationCost); 252 } 253 254 /** 255 * If and only if the array is a static, then add it as a Global State Cost 256 * for the {@code inMethod}. 257 */ 258 public void assignArray(Variable array, Variable index, Variable value, 259 int lineNumber) { 260 if (variableState.isGlobal(array)) { 261 addGlobalCost(lineNumber, array); 262 } 263 } 264 265 /** 266 * The method propagates the global property of a field onto any field it is 267 * assigned to. The globality is propagated because global state is 268 * transitive (static cling) So any modification on class which is 269 * transitively global should also be penalized. 270 * 271 * <p> 272 * Note: <em>final</em> static fields are not added, because they are 273 * assumed to be constants, thus this will miss some actual global state. 274 * (The justification is that if costs were included for constants it would 275 * penalize people for a good practice -- removing magic values from code). 276 */ 277 public void assignField(Variable fieldInstance, FieldInfo field, 278 Variable value, int lineNumber) { 279 assignVariable(field, lineNumber, this, value); 280 if (fieldInstance == null || variableState.isGlobal(fieldInstance)) { 281 if (!field.isFinal()) { 282 addGlobalCost(lineNumber, field); 283 } 284 variableState.setGlobal(field); 285 } 286 } 287 288 public void assignLocal(int lineNumber, Variable destination, 289 Variable source) { 290 assignVariable(destination, lineNumber, this, source); 291 } 292 293 void assignParameter(int lineNumber, Variable destination, 294 ParentFrame sourceFrame, Variable source) { 295 assignVariable(destination, lineNumber, sourceFrame, source); 296 } 297 298 public void assignReturnValue(int lineNumber, Variable destination) { 299 assignVariable(destination, lineNumber, this, returnValue); 300 } 301 302 void assignVariable(Variable destination, int lineNumber, 303 ParentFrame sourceFrame, Variable source) { 304 if (sourceFrame.variableState.isInjectable(source)) { 305 variableState.setInjectable(destination); 306 } 307 if (destination.isGlobal() || sourceFrame.variableState.isGlobal(source)) { 308 variableState.setGlobal(destination); 309 if (source instanceof LocalField && !source.isFinal()) { 310 addGlobalCost(lineNumber, source); 311 } 312 } 313 } 314 315 int getLoDCount(Variable variable) { 316 return variableState.getLoDCount(variable); 317 } 318 319 ParentFrame getParentFrame() { 320 return parentFrame; 321 } 322 323 private Cost getTotalCost() { 324 Cost totalCost = new Cost(); 325 totalCost.add(direct); 326 totalCost.add(indirect); 327 return totalCost; 328 } 329 330 public VariableState getVariableState() { 331 return variableState; 332 } 333 334 protected void applyMethodOperations(int lineNumber, MethodInfo toMethod, 335 Variable methodThis, List<? extends Variable> parameters, 336 Variable returnVariable) { 337 if (parameters.size() != toMethod.getParameters().size()) { 338 throw new IllegalStateException( 339 "Argument count does not match method parameter count."); 340 } 341 int i = 0; 342 for (Variable var : parameters) { 343 assignParameter(lineNumber, toMethod.getParameters().get(i++), 344 parentFrame, var); 345 } 346 returnValue = null; 347 for (Operation operation : toMethod.getOperations()) { 348 operation.visit(this); 349 } 350 incrementLoD(lineNumber, toMethod, methodThis, returnVariable, parentFrame); 351 } 352 353 private void recordMethodCall(int lineNumber, MethodInfo toMethod, 354 Variable methodThis, List<? extends Variable> parameters, 355 Variable returnVariable) { 356 for (Integer lineNumberWithComplexity : toMethod.getLinesOfComplexity()) { 357 addCyclomaticCost(lineNumberWithComplexity); 358 } 359 if (toMethod.isInstance()) { 360 assignParameter(lineNumber, toMethod.getMethodThis(), parentFrame, 361 methodThis); 362 } 363 applyMethodOperations(lineNumber, toMethod, methodThis, parameters, 364 returnVariable); 365 assignReturnValue(lineNumber, returnVariable); 366 } 367 368 public void recordMethodCall(String clazzName, int lineNumber, 369 String methodName, Variable methodThis, List<Variable> parameters, 370 Variable returnVariable) { 371 try { 372 if (whitelist != null && whitelist.isClassWhiteListed(clazzName)) { 373 return; 374 } 375 MethodInfo toMethod = classRepository.getClass(clazzName).getMethod( 376 methodName); 377 if (alreadyVisited.contains(toMethod)) { 378 // Method already counted, skip (to prevent recursion) 379 incrementLoD(lineNumber, toMethod, methodThis, returnVariable, parentFrame); 380 } else if (toMethod.canOverride() 381 && variableState.isInjectable(methodThis)) { 382 // Method can be overridden / injectable 383 recordOverridableMethodCall(lineNumber, toMethod, methodThis, 384 returnVariable); 385 } else { 386 // Method can not be intercepted we have to add the cost 387 // recursively 388 recordNonOveridableMethodCall(NON_OVERRIDABLE_METHOD_CALL, lineNumber, toMethod, 389 methodThis, parameters, returnVariable); 390 } 391 } catch (ClassNotFoundException e) { 392 err.println("WARNING: class not found: " + clazzName); 393 } catch (MethodNotFoundException e) { 394 err.println("WARNING: method not found: " + e.getMethodName() + " in " 395 + e.getClassInfo().getName()); 396 } 397 } 398 399 protected void recordNonOveridableMethodCall(Reason reason, int lineNumber, 400 MethodInfo toMethod, Variable methodThis, 401 List<? extends Variable> parameters, Variable returnVariable) { 402 Frame childFrame = createChildFrame(toMethod); 403 childFrame.recordMethodCall(lineNumber, toMethod, methodThis, parameters, 404 returnVariable); 405 addMethodInvocationCost(lineNumber, toMethod, childFrame.getTotalCost() 406 .copyNoLOD(), reason); 407 } 408 409 protected Frame createChildFrame(MethodInfo toMethod) { 410 return new Frame(err, classRepository, this, whitelist, 411 getGlobalVariables(), alreadyVisited, toMethod); 412 } 413 414 private void recordOverridableMethodCall(int lineNumber, 415 MethodInfo toMethod, Variable methodThis, Variable returnVariable) { 416 if (returnVariable != null) { 417 variableState.setInjectable(returnVariable); 418 setReturnValue(returnVariable); 419 } 420 incrementLoD(lineNumber, toMethod, methodThis, returnVariable, this); 421 } 422 423 protected void incrementLoD(int lineNumber, MethodInfo toMethod, 424 Variable destination, Variable source, ParentFrame destinationFrame) { 425 } 426 427 protected void setInjectable(List<? extends Variable> parameters) { 428 for (Variable variable : parameters) { 429 variableState.setInjectable(variable); 430 } 431 } 432 433 public void setReturnValue(Variable value) { 434 boolean isWorse = variableState.isGlobal(value) 435 && !variableState.isGlobal(returnValue); 436 if (isWorse) { 437 returnValue = value; 438 } 439 } 440 441 @Override 442 public String toString() { 443 return "MethodCost: " + method + "\n" + super.toString(); 444 } 445 446 } 447 448 public static class ParentFrame { 449 protected final VariableState globalVariableState; 450 protected final LocalVariableState variableState; 451 452 public ParentFrame(VariableState globalVariableState) { 453 this.globalVariableState = globalVariableState; 454 this.variableState = new LocalVariableState(globalVariableState); 455 } 456 457 protected void addLoDCost(int lineNumber, MethodInfo toMethod, int distance) { 458 } 459 460 public VariableState getGlobalVariables() { 461 return globalVariableState; 462 } 463 464 } 465 466 // TODO: refactor me. The root frame needs to be of different class so that 467 // we can remove all of the ifs in Frame 468 private final VariableState globalVariables; 469 private final ClassRepository classRepository; 470 private final PrintStream err; 471 private final WhiteList whitelist; 472 473 public TestabilityVisitor(ClassRepository classRepository, 474 VariableState variableState, PrintStream err, WhiteList whitelist) { 475 this.classRepository = classRepository; 476 this.globalVariables = variableState; 477 this.err = err; 478 this.whitelist = whitelist; 479 } 480 481 public CostRecordingFrame createFrame(MethodInfo method, int recordingDepth) { 482 return new CostRecordingFrame(err, classRepository, whitelist, 483 globalVariables, method, recordingDepth); 484 } 485 486 @Override 487 public String toString() { 488 StringBuilder buf = new StringBuilder(); 489 buf.append("MethodCost:"); 490 buf.append("\n==============\nROOT FRAME:\n" + globalVariables); 491 return buf.toString(); 492 } 493 494}