Skip to content

Commit 0ca5300

Browse files
Merge pull request #3655 from patrick-sko:Implementation_of_branch_instrumentation
PiperOrigin-RevId: 325849537
2 parents 1d85aae + b8e2268 commit 0ca5300

File tree

3 files changed

+377
-40
lines changed

3 files changed

+377
-40
lines changed

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

Lines changed: 154 additions & 25 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;
@@ -36,8 +40,7 @@
3640
* instrument with a function call which is provided in the source code as opposed to an array.
3741
*/
3842
@GwtIncompatible
39-
final class ProductionCoverageInstrumentationCallback
40-
extends NodeTraversal.AbstractPostOrderCallback {
43+
final class ProductionCoverageInstrumentationCallback implements NodeTraversal.Callback {
4144

4245
// TODO(psokol): Make this dynamic so that instrumentation does not rely on hardcoded files
4346
private static final String INSTRUMENT_CODE_FUNCTION_NAME = "instrumentCode";
@@ -60,14 +63,36 @@ final class ProductionCoverageInstrumentationCallback
6063
private final ParameterMapping parameterMapping;
6164
boolean visitedInstrumentCodeFile = false;
6265

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

6679
public ProductionCoverageInstrumentationCallback(AbstractCompiler compiler) {
6780
this.compiler = compiler;
6881
this.parameterMapping = new ParameterMapping();
6982
}
7083

84+
@Override
85+
public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
86+
87+
if (visitedInstrumentCodeFile && n.isFunction()) {
88+
String fnName = NodeUtil.getBestLValueName(NodeUtil.getBestLValue(n));
89+
fnName = (fnName == null) ? "Anonymous" : fnName;
90+
functionNameStack.push(fnName);
91+
}
92+
93+
return true;
94+
}
95+
7196
@Override
7297
public void visit(NodeTraversal traversal, Node node, Node parent) {
7398
String fileName = traversal.getSourceName();
@@ -91,23 +116,112 @@ public void visit(NodeTraversal traversal, Node node, Node parent) {
91116
return;
92117
}
93118

94-
if (node.isFunction()) {
95-
cachedFunctionName = NodeUtil.getBestLValueName(NodeUtil.getBestLValue(node));
96-
instrumentCode(traversal, node.getLastChild(), cachedFunctionName);
119+
String functionName = functionNameStack.peek();
120+
121+
switch (node.getToken()) {
122+
case FUNCTION:
123+
// If the function node has been visited by visit() then we can be assured that all its
124+
// children nodes have been visited and properly instrumented.
125+
functionNameStack.pop();
126+
instrumentBlockNode(node.getLastChild(), fileName, functionName, Type.FUNCTION);
127+
break;
128+
case IF:
129+
Node ifTrueNode = node.getSecondChild();
130+
instrumentBlockNode(ifTrueNode, sourceFileName, functionName, Type.BRANCH);
131+
if (node.getChildCount() == 2) {
132+
addElseBlock(node);
133+
}
134+
Node ifFalseNode = node.getLastChild();
135+
// The compiler converts all sets of If-Else if-Else blocks into a combination of nested
136+
// If-Else blocks. This case checks if the else blocks first child is an If statement, and
137+
// if it is it will not instrument. This avoids adding multiple instrumentation calls.
138+
// Since we also make sure an Else case is added to every If statement, we are still
139+
// assured that the else statement is being reached through a later instrumentation call.
140+
if (NodeUtil.isEmptyBlock(ifFalseNode)
141+
|| (ifFalseNode.getFirstChild() != null && !ifFalseNode.getFirstChild().isIf())) {
142+
instrumentBlockNode(ifFalseNode, sourceFileName, functionName, Type.BRANCH_DEFAULT);
143+
}
144+
break;
145+
case SWITCH:
146+
boolean hasDefaultCase = false;
147+
for (Node c = node.getSecondChild(); c != null; c = c.getNext()) {
148+
if (c.isDefaultCase()) {
149+
instrumentBlockNode(
150+
c.getLastChild(), sourceFileName, functionName, Type.BRANCH_DEFAULT);
151+
hasDefaultCase = true;
152+
} else {
153+
instrumentBlockNode(c.getLastChild(), sourceFileName, functionName, Type.BRANCH);
154+
}
155+
}
156+
if (!hasDefaultCase) {
157+
Node defaultBlock = IR.block();
158+
defaultBlock.useSourceInfoIfMissingFromForTree(node);
159+
Node defaultCase = IR.defaultCase(defaultBlock).useSourceInfoIfMissingFromForTree(node);
160+
node.addChildToBack(defaultCase);
161+
instrumentBlockNode(defaultBlock, sourceFileName, functionName, Type.BRANCH_DEFAULT);
162+
}
163+
break;
164+
case HOOK:
165+
Node ifTernaryIsTrueExpression = node.getSecondChild();
166+
Node ifTernaryIsFalseExpression = node.getLastChild();
167+
168+
addInstrumentationNodeWithComma(
169+
ifTernaryIsTrueExpression, sourceFileName, functionName, Type.BRANCH);
170+
addInstrumentationNodeWithComma(
171+
ifTernaryIsFalseExpression, sourceFileName, functionName, Type.BRANCH);
172+
173+
compiler.reportChangeToEnclosingScope(node);
174+
break;
175+
case OR:
176+
case AND:
177+
case COALESCE:
178+
// Only instrument the second child of the binary operation because the first child will
179+
// always execute, or the first child is part of a chain of binary operations and would have
180+
// already been instrumented.
181+
Node secondExpression = node.getLastChild();
182+
addInstrumentationNodeWithComma(
183+
secondExpression, sourceFileName, functionName, Type.BRANCH);
184+
185+
compiler.reportChangeToEnclosingScope(node);
186+
break;
187+
default:
188+
if (NodeUtil.isLoopStructure(node)) {
189+
Node blockNode = NodeUtil.getLoopCodeBlock(node);
190+
checkNotNull(blockNode);
191+
instrumentBlockNode(blockNode, sourceFileName, functionName, Type.BRANCH);
192+
}
97193
}
98194
}
99195

100196
/**
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
197+
* Given a node, this function will create a new instrumentationNode and combine it with the
198+
* original node using a COMMA node.
199+
*/
200+
private void addInstrumentationNodeWithComma(
201+
Node originalNode, String fileName, String functionName, Type type) {
202+
Node parentNode = originalNode.getParent();
203+
Node cloneOfOriginal = originalNode.cloneTree();
204+
Node newInstrumentationNode =
205+
newInstrumentationNode(cloneOfOriginal, fileName, functionName, type);
206+
207+
// newInstrumentationNode returns an EXPR_RESULT which cannot be a child of a COMMA node.
208+
// Instead we use the child of of the newInstrumentatioNode which is a CALL node.
209+
Node childOfInstrumentationNode = newInstrumentationNode.getFirstChild().detach();
210+
Node infusedExp = AstManipulations.fuseExpressions(childOfInstrumentationNode, cloneOfOriginal);
211+
parentNode.replaceChild(originalNode, infusedExp);
212+
}
213+
214+
/**
215+
* Consumes a block node and adds a new child to the front of the block node which is the
216+
* instrumentation Node
103217
*
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
218+
* @param block The block node to be instrumented.
219+
* @param fileName The file name of the node being instrumented.
220+
* @param fnName The function name of the node being instrumented.
221+
* @param type The type of the node being instrumented.
108222
*/
109-
private void instrumentCode(NodeTraversal traversal, Node block, String fnName) {
110-
block.addChildToFront(newInstrumentationNode(traversal, block, fnName));
223+
private void instrumentBlockNode(Node block, String fileName, String fnName, Type type) {
224+
block.addChildToFront(newInstrumentationNode(block, fileName, fnName, type));
111225
compiler.reportChangeToEnclosingScope(block);
112226
}
113227

@@ -118,25 +232,40 @@ private void instrumentCode(NodeTraversal traversal, Node block, String fnName)
118232
* with the given constants evaluates to:
119233
* module$exports$instrument$code.instrumentCodeInstance.instrumentCode(encodedParam, lineNum);
120234
*
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.
235+
* @param node The node to be instrumented.
236+
* @param fileName The file name of the node being instrumented.
237+
* @param fnName The function name of the node being instrumented.
238+
* @param type The type of the node being instrumented.
124239
* @return The newly constructed function call node.
125240
*/
126-
private Node newInstrumentationNode(NodeTraversal traversal, Node node, String fnName) {
241+
private Node newInstrumentationNode(Node node, String fileName, String fnName, Type type) {
242+
243+
String encodedParam = parameterMapping.getEncodedParam(fileName, fnName, type);
127244

128-
String type = "Type.FUNCTION";
245+
int lineNo = node.getLineno();
246+
int columnNo = node.getCharno();
129247

130-
String encodedParam = parameterMapping.getEncodedParam(traversal.getSourceName(), fnName, type);
248+
if (node.isBlock()) {
249+
lineNo = node.getParent().getLineno();
250+
columnNo = node.getParent().getCharno();
251+
}
131252

132253
Node innerProp = IR.getprop(IR.name(MODULE_RENAMING), IR.string(INSTRUMENT_CODE_INSTANCE));
133254
Node outerProp = IR.getprop(innerProp, IR.string(INSTRUMENT_CODE_FUNCTION_NAME));
134-
Node functionCall = IR.call(outerProp, IR.string(encodedParam), IR.number(node.getLineno()));
255+
Node functionCall =
256+
IR.call(outerProp, IR.string(encodedParam), IR.number(lineNo), IR.number(columnNo));
135257
Node exprNode = IR.exprResult(functionCall);
136258

137259
return exprNode.useSourceInfoIfMissingFromForTree(node);
138260
}
139261

262+
/** Add an else block for If statements if one is not already present. */
263+
private Node addElseBlock(Node node) {
264+
Node defaultBlock = IR.block();
265+
node.addChildToBack(defaultBlock);
266+
return defaultBlock.useSourceInfoIfMissingFromForTree(node);
267+
}
268+
140269
public VariableMap getInstrumentationMapping() {
141270
return parameterMapping.getParamMappingAsVariableMap();
142271
}
@@ -178,18 +307,18 @@ private static final class ParameterMapping {
178307
typeToIndex = new LinkedHashMap<>();
179308
}
180309

181-
private String getEncodedParam(String fileName, String functionName, String type) {
310+
private String getEncodedParam(String fileName, String functionName, Type type) {
182311

183312
fileNameToIndex.putIfAbsent(fileName, fileNameToIndex.size());
184313
functionNameToIndex.putIfAbsent(functionName, functionNameToIndex.size());
185-
typeToIndex.putIfAbsent(type, typeToIndex.size());
314+
typeToIndex.putIfAbsent(type.name(), typeToIndex.size());
186315

187316
StringBuilder sb = new StringBuilder();
188317

189318
try {
190319
Base64VLQ.encode(sb, fileNameToIndex.get(fileName));
191320
Base64VLQ.encode(sb, functionNameToIndex.get(functionName));
192-
Base64VLQ.encode(sb, typeToIndex.get(type));
321+
Base64VLQ.encode(sb, typeToIndex.get(type.name()));
193322
} catch (IOException e) {
194323
throw new AssertionError(e);
195324
}

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

Lines changed: 2 additions & 2 deletions
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
";");
@@ -2638,7 +2638,7 @@ public void testInstrumentCodeProductionCreatesInstrumentationMapping() throws I
26382638
assertThat(variableMapFile.get(0)).startsWith(" FileNames:[");
26392639
assertThat(variableMapFile.get(0)).endsWith("sourceCode.js]");
26402640
assertThat(variableMapFile.get(1)).isEqualTo(" FunctionNames:[foo]");
2641-
assertThat(variableMapFile.get(2)).isEqualTo(" Types:[Type.FUNCTION]");
2641+
assertThat(variableMapFile.get(2)).isEqualTo(" Types:[FUNCTION]");
26422642
assertThat(variableMapFile.get(3)).isEqualTo("C:AAA");
26432643
}
26442644

0 commit comments

Comments
 (0)