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