Skip to content

Commit f949e8b

Browse files
committed
Implemented branch instrumentation
1 parent eb114a2 commit f949e8b

File tree

3 files changed

+332
-33
lines changed

3 files changed

+332
-33
lines changed

src/com/google/javascript/jscomp/ProductionCoverageInstrumentationCallback.java

Lines changed: 136 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@
1616

1717
package com.google.javascript.jscomp;
1818

19+
import static com.google.common.base.Preconditions.checkNotNull;
20+
1921
import com.google.common.annotations.GwtIncompatible;
2022
import com.google.common.collect.ImmutableMap;
2123
import com.google.debugging.sourcemap.Base64VLQ;
2224
import com.google.javascript.rhino.IR;
2325
import com.google.javascript.rhino.Node;
2426
import java.io.IOException;
27+
import java.util.ArrayDeque;
28+
import java.util.Deque;
2529
import java.util.HashMap;
2630
import java.util.LinkedHashMap;
2731
import java.util.Map;
@@ -37,7 +41,7 @@
3741
*/
3842
@GwtIncompatible
3943
final class ProductionCoverageInstrumentationCallback
40-
extends NodeTraversal.AbstractPostOrderCallback {
44+
implements NodeTraversal.Callback {
4145

4246
// TODO(psokol): Make this dynamic so that instrumentation does not rely on hardcoded files
4347
private static final String INSTRUMENT_CODE_FUNCTION_NAME = "instrumentCode";
@@ -60,14 +64,34 @@ final class ProductionCoverageInstrumentationCallback
6064
private final ParameterMapping parameterMapping;
6165
boolean visitedInstrumentCodeFile = false;
6266

63-
/** Stores the name of the current function that encapsulates the node being instrumented */
64-
private String cachedFunctionName = "Anonymous";
67+
private final String FUNCTION_TYPE = "Type.FUNCTION";
68+
private final String BRANCH_TYPE = "Type.BRANCH";
69+
private final String BRANCH_DEFAULT_TYPE = "Type.BRANCH_DEFAULT";
70+
71+
/**
72+
* Stores a stack of function names that encapsulates the children nodes being instrumented. The
73+
* function name is popped off the stack when the function node, and the entire subtree rooted at
74+
* the function node have been visited.
75+
*/
76+
private final Deque<String> functionNameStack = new ArrayDeque<>();
6577

6678
public ProductionCoverageInstrumentationCallback(AbstractCompiler compiler) {
6779
this.compiler = compiler;
6880
this.parameterMapping = new ParameterMapping();
6981
}
7082

83+
@Override
84+
public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
85+
86+
if (visitedInstrumentCodeFile && n.isFunction()) {
87+
String fnName = NodeUtil.getBestLValueName(NodeUtil.getBestLValue(n));
88+
fnName = (fnName == null) ? "Anonymous" : fnName;
89+
functionNameStack.push(fnName);
90+
}
91+
92+
return true;
93+
}
94+
7195
@Override
7296
public void visit(NodeTraversal traversal, Node node, Node parent) {
7397
String fileName = traversal.getSourceName();
@@ -91,23 +115,99 @@ public void visit(NodeTraversal traversal, Node node, Node parent) {
91115
return;
92116
}
93117

118+
String functionName = functionNameStack.peek();
119+
94120
if (node.isFunction()) {
95-
cachedFunctionName = NodeUtil.getBestLValueName(NodeUtil.getBestLValue(node));
96-
instrumentCode(traversal, node.getLastChild(), cachedFunctionName);
121+
// If the function node has been visited by visit() then we can be assured that all its
122+
// children nodes have been visited and properly instrumented.
123+
functionNameStack.pop();
124+
instrumentBlockNode(node.getLastChild(), fileName, functionName, FUNCTION_TYPE);
125+
} else if (node.isIf()) {
126+
if (node.getChildCount() == 2) {
127+
addDefaultBlock(node);
128+
}
129+
Node ifTrueNode = node.getSecondChild();
130+
Node ifFalseNode = node.getLastChild();
131+
instrumentBlockNode(ifTrueNode, sourceFileName, functionName, BRANCH_TYPE);
132+
instrumentBlockNode(ifFalseNode, sourceFileName, functionName, BRANCH_DEFAULT_TYPE);
133+
} else if (node.isSwitch()) {
134+
boolean hasDefaultCase = false;
135+
for (Node c = node.getSecondChild(); c != null; c = c.getNext()) {
136+
if (c.isDefaultCase()) {
137+
instrumentBlockNode(c.getLastChild(), sourceFileName, functionName, BRANCH_DEFAULT_TYPE);
138+
hasDefaultCase = true;
139+
} else {
140+
instrumentBlockNode(c.getLastChild(), sourceFileName, functionName, BRANCH_TYPE);
141+
}
142+
}
143+
if (!hasDefaultCase){
144+
Node defaultBlock = IR.block();
145+
defaultBlock.useSourceInfoIfMissingFromForTree(node);
146+
Node defaultCase = IR.defaultCase(defaultBlock).useSourceInfoIfMissingFromForTree(node);
147+
node.addChildToBack(defaultCase);
148+
instrumentBlockNode(defaultBlock, sourceFileName, functionName, BRANCH_DEFAULT_TYPE);
149+
}
150+
} else if (NodeUtil.isLoopStructure(node)) {
151+
Node blockNode = NodeUtil.getLoopCodeBlock(node);
152+
checkNotNull(blockNode);
153+
instrumentBlockNode(blockNode, sourceFileName, functionName, BRANCH_TYPE);
154+
155+
Node newNode =
156+
newInstrumentationNode(blockNode, sourceFileName, functionName, BRANCH_DEFAULT_TYPE);
157+
blockNode.getGrandparent().addChildAfter(newNode, blockNode.getParent());
158+
compiler.reportChangeToEnclosingScope(blockNode.getParent());
159+
} else if (node.isHook()) {
160+
Node ifTernaryIsTrueExpression = node.getSecondChild();
161+
Node ifTernaryIsFalseExpression = node.getLastChild();
162+
163+
addInstrumentationNodeWithComma(
164+
ifTernaryIsTrueExpression, sourceFileName, functionName, BRANCH_TYPE);
165+
addInstrumentationNodeWithComma(
166+
ifTernaryIsFalseExpression, sourceFileName, functionName, BRANCH_TYPE);
167+
168+
compiler.reportChangeToEnclosingScope(node);
169+
} else if (node.isOr() || node.isAnd() || node.isNullishCoalesce()) {
170+
// Only instrument the second child of the binary operation because the first child will
171+
// always execute, or the first child is part of a chain of binary operations and would have
172+
// already been instrumented.
173+
Node secondExpression = node.getLastChild();
174+
addInstrumentationNodeWithComma(
175+
secondExpression, sourceFileName, functionName, BRANCH_TYPE);
176+
177+
compiler.reportChangeToEnclosingScope(node);
97178
}
98179
}
99180

100181
/**
101-
* Iterate over all collected block nodes within a Script node and add a new child to the front of
102-
* each block node which is the instrumentation Node
182+
* Given a node, this function will create a new instrumentationNode and combine it with the
183+
* original node using a COMMA node.
184+
*/
185+
private void addInstrumentationNodeWithComma(
186+
Node originalNode, String fileName, String functionName, String type) {
187+
Node parentNode = originalNode.getParent();
188+
parentNode.removeChild(originalNode);
189+
Node newInstrumentationNode =
190+
newInstrumentationNode(originalNode, fileName, functionName, type);
191+
192+
// newInstrumentationNode returns an EXPR_RESULT which cannot be a child of a COMMA node.
193+
// Instead we use the child of of the newInstrumentatioNode which is a CALL node.
194+
Node childOfInstrumentationNode = newInstrumentationNode.getFirstChild().detach();
195+
Node infusedExp =
196+
StatementFusion.fuseExpressionIntoExpression(childOfInstrumentationNode, originalNode);
197+
parentNode.addChildToBack(infusedExp);
198+
}
199+
200+
/**
201+
* Consumes a block node and adds a new child to the front of the block node which is the
202+
* instrumentation Node
103203
*
104-
* @param traversal The node traversal context which maintains information such as fileName being
105-
* traversed
106-
* @param block The block node to be instrumented instrumented
107-
* @param fnName The function name that encapsulates the current node block being instrumented
204+
* @param block The block node to be instrumented.
205+
* @param fileName The file name of the node being instrumented.
206+
* @param fnName The function name of the node being instrumented.
207+
* @param type The type of the node being instrumented.
108208
*/
109-
private void instrumentCode(NodeTraversal traversal, Node block, String fnName) {
110-
block.addChildToFront(newInstrumentationNode(traversal, block, fnName));
209+
private void instrumentBlockNode(Node block, String fileName, String fnName, String type) {
210+
block.addChildToFront(newInstrumentationNode(block, fileName, fnName, type));
111211
compiler.reportChangeToEnclosingScope(block);
112212
}
113213

@@ -118,25 +218,41 @@ private void instrumentCode(NodeTraversal traversal, Node block, String fnName)
118218
* with the given constants evaluates to:
119219
* module$exports$instrument$code.instrumentCodeInstance.instrumentCode(encodedParam, lineNum);
120220
*
121-
* @param traversal The context of the current traversal.
122-
* @param node The block node to be instrumented.
123-
* @param fnName The function name that the node exists within.
221+
* @param node The node to be instrumented.
222+
* @param fileName The file name of the node being instrumented.
223+
* @param fnName The function name of the node being instrumented.
224+
* @param type The type of the node being instrumented.
124225
* @return The newly constructed function call node.
125226
*/
126-
private Node newInstrumentationNode(NodeTraversal traversal, Node node, String fnName) {
227+
private Node newInstrumentationNode(Node node, String fileName, String fnName, String type) {
228+
229+
String encodedParam = parameterMapping.getEncodedParam(fileName, fnName, type);
127230

128-
String type = "Type.FUNCTION";
231+
int lineNo = node.getLineno();
232+
int columnNo = node.getCharno();
129233

130-
String encodedParam = parameterMapping.getEncodedParam(traversal.getSourceName(), fnName, type);
234+
if(node.isBlock()){
235+
lineNo = node.getParent().getLineno();
236+
columnNo = node.getParent().getCharno();
237+
}
131238

132239
Node innerProp = IR.getprop(IR.name(MODULE_RENAMING), IR.string(INSTRUMENT_CODE_INSTANCE));
133240
Node outerProp = IR.getprop(innerProp, IR.string(INSTRUMENT_CODE_FUNCTION_NAME));
134-
Node functionCall = IR.call(outerProp, IR.string(encodedParam), IR.number(node.getLineno()));
241+
Node functionCall =
242+
IR.call(outerProp, IR.string(encodedParam), IR.number(lineNo), IR.number(columnNo));
135243
Node exprNode = IR.exprResult(functionCall);
136244

137245
return exprNode.useSourceInfoIfMissingFromForTree(node);
138246
}
139247

248+
/** Add a default block for If statements */
249+
private Node addDefaultBlock(Node node) {
250+
Node defaultBlock = IR.block();
251+
node.addChildToBack(defaultBlock);
252+
return defaultBlock.useSourceInfoIfMissingFromForTree(node);
253+
}
254+
255+
140256
public VariableMap getInstrumentationMapping() {
141257
return parameterMapping.getParamMappingAsVariableMap();
142258
}

test/com/google/javascript/jscomp/CommandLineRunnerTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2617,7 +2617,7 @@ public void testInstrumentCodeProductionCreatesInstrumentationMapping() throws I
26172617
"module$exports$instrument$code.instrumentCodeInstance = new"
26182618
+ " module$contents$instrument$code_InstrumentCode;",
26192619
"function foo() {",
2620-
" module$exports$instrument$code.instrumentCodeInstance.instrumentCode(\"C\", 1);",
2620+
" module$exports$instrument$code.instrumentCodeInstance.instrumentCode(\"C\", 1, 0);",
26212621
" console.log(\"Hello\");",
26222622
"}",
26232623
";");

0 commit comments

Comments
 (0)