PageRenderTime 30ms CodeModel.GetById 1ms app.highlight 23ms RepoModel.GetById 2ms app.codeStats 0ms

/testability-explorer/src/main/java/com/google/test/metric/TestabilityVisitor.java

http://testability-explorer.googlecode.com/
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}