16
16
17
17
package com .google .javascript .jscomp ;
18
18
19
+ import static com .google .common .base .Preconditions .checkNotNull ;
20
+
19
21
import com .google .common .annotations .GwtIncompatible ;
20
22
import com .google .common .collect .ImmutableMap ;
21
23
import com .google .debugging .sourcemap .Base64VLQ ;
22
24
import com .google .javascript .rhino .IR ;
23
25
import com .google .javascript .rhino .Node ;
24
26
import java .io .IOException ;
27
+ import java .util .ArrayDeque ;
28
+ import java .util .Deque ;
25
29
import java .util .HashMap ;
26
30
import java .util .LinkedHashMap ;
27
31
import java .util .Map ;
36
40
* instrument with a function call which is provided in the source code as opposed to an array.
37
41
*/
38
42
@ GwtIncompatible
39
- final class ProductionCoverageInstrumentationCallback
40
- extends NodeTraversal .AbstractPostOrderCallback {
43
+ final class ProductionCoverageInstrumentationCallback implements NodeTraversal .Callback {
41
44
42
45
// TODO(psokol): Make this dynamic so that instrumentation does not rely on hardcoded files
43
46
private static final String INSTRUMENT_CODE_FUNCTION_NAME = "instrumentCode" ;
@@ -60,14 +63,36 @@ final class ProductionCoverageInstrumentationCallback
60
63
private final ParameterMapping parameterMapping ;
61
64
boolean visitedInstrumentCodeFile = false ;
62
65
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 <>();
65
78
66
79
public ProductionCoverageInstrumentationCallback (AbstractCompiler compiler ) {
67
80
this .compiler = compiler ;
68
81
this .parameterMapping = new ParameterMapping ();
69
82
}
70
83
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
+
71
96
@ Override
72
97
public void visit (NodeTraversal traversal , Node node , Node parent ) {
73
98
String fileName = traversal .getSourceName ();
@@ -91,23 +116,112 @@ public void visit(NodeTraversal traversal, Node node, Node parent) {
91
116
return ;
92
117
}
93
118
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
+ }
97
193
}
98
194
}
99
195
100
196
/**
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
103
217
*
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.
108
222
*/
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 ));
111
225
compiler .reportChangeToEnclosingScope (block );
112
226
}
113
227
@@ -118,25 +232,40 @@ private void instrumentCode(NodeTraversal traversal, Node block, String fnName)
118
232
* with the given constants evaluates to:
119
233
* module$exports$instrument$code.instrumentCodeInstance.instrumentCode(encodedParam, lineNum);
120
234
*
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.
124
239
* @return The newly constructed function call node.
125
240
*/
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 );
127
244
128
- String type = "Type.FUNCTION" ;
245
+ int lineNo = node .getLineno ();
246
+ int columnNo = node .getCharno ();
129
247
130
- String encodedParam = parameterMapping .getEncodedParam (traversal .getSourceName (), fnName , type );
248
+ if (node .isBlock ()) {
249
+ lineNo = node .getParent ().getLineno ();
250
+ columnNo = node .getParent ().getCharno ();
251
+ }
131
252
132
253
Node innerProp = IR .getprop (IR .name (MODULE_RENAMING ), IR .string (INSTRUMENT_CODE_INSTANCE ));
133
254
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 ));
135
257
Node exprNode = IR .exprResult (functionCall );
136
258
137
259
return exprNode .useSourceInfoIfMissingFromForTree (node );
138
260
}
139
261
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
+
140
269
public VariableMap getInstrumentationMapping () {
141
270
return parameterMapping .getParamMappingAsVariableMap ();
142
271
}
@@ -178,18 +307,18 @@ private static final class ParameterMapping {
178
307
typeToIndex = new LinkedHashMap <>();
179
308
}
180
309
181
- private String getEncodedParam (String fileName , String functionName , String type ) {
310
+ private String getEncodedParam (String fileName , String functionName , Type type ) {
182
311
183
312
fileNameToIndex .putIfAbsent (fileName , fileNameToIndex .size ());
184
313
functionNameToIndex .putIfAbsent (functionName , functionNameToIndex .size ());
185
- typeToIndex .putIfAbsent (type , typeToIndex .size ());
314
+ typeToIndex .putIfAbsent (type . name () , typeToIndex .size ());
186
315
187
316
StringBuilder sb = new StringBuilder ();
188
317
189
318
try {
190
319
Base64VLQ .encode (sb , fileNameToIndex .get (fileName ));
191
320
Base64VLQ .encode (sb , functionNameToIndex .get (functionName ));
192
- Base64VLQ .encode (sb , typeToIndex .get (type ));
321
+ Base64VLQ .encode (sb , typeToIndex .get (type . name () ));
193
322
} catch (IOException e ) {
194
323
throw new AssertionError (e );
195
324
}
0 commit comments