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 ;
37
41
*/
38
42
@ GwtIncompatible
39
43
final class ProductionCoverageInstrumentationCallback
40
- extends NodeTraversal .AbstractPostOrderCallback {
44
+ implements NodeTraversal .Callback {
41
45
42
46
// TODO(psokol): Make this dynamic so that instrumentation does not rely on hardcoded files
43
47
private static final String INSTRUMENT_CODE_FUNCTION_NAME = "instrumentCode" ;
@@ -60,14 +64,34 @@ final class ProductionCoverageInstrumentationCallback
60
64
private final ParameterMapping parameterMapping ;
61
65
boolean visitedInstrumentCodeFile = false ;
62
66
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 <>();
65
77
66
78
public ProductionCoverageInstrumentationCallback (AbstractCompiler compiler ) {
67
79
this .compiler = compiler ;
68
80
this .parameterMapping = new ParameterMapping ();
69
81
}
70
82
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
+
71
95
@ Override
72
96
public void visit (NodeTraversal traversal , Node node , Node parent ) {
73
97
String fileName = traversal .getSourceName ();
@@ -91,23 +115,99 @@ public void visit(NodeTraversal traversal, Node node, Node parent) {
91
115
return ;
92
116
}
93
117
118
+ String functionName = functionNameStack .peek ();
119
+
94
120
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 );
97
178
}
98
179
}
99
180
100
181
/**
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
103
203
*
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.
108
208
*/
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 ));
111
211
compiler .reportChangeToEnclosingScope (block );
112
212
}
113
213
@@ -118,25 +218,41 @@ private void instrumentCode(NodeTraversal traversal, Node block, String fnName)
118
218
* with the given constants evaluates to:
119
219
* module$exports$instrument$code.instrumentCodeInstance.instrumentCode(encodedParam, lineNum);
120
220
*
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.
124
225
* @return The newly constructed function call node.
125
226
*/
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 );
127
230
128
- String type = "Type.FUNCTION" ;
231
+ int lineNo = node .getLineno ();
232
+ int columnNo = node .getCharno ();
129
233
130
- String encodedParam = parameterMapping .getEncodedParam (traversal .getSourceName (), fnName , type );
234
+ if (node .isBlock ()){
235
+ lineNo = node .getParent ().getLineno ();
236
+ columnNo = node .getParent ().getCharno ();
237
+ }
131
238
132
239
Node innerProp = IR .getprop (IR .name (MODULE_RENAMING ), IR .string (INSTRUMENT_CODE_INSTANCE ));
133
240
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 ));
135
243
Node exprNode = IR .exprResult (functionCall );
136
244
137
245
return exprNode .useSourceInfoIfMissingFromForTree (node );
138
246
}
139
247
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
+
140
256
public VariableMap getInstrumentationMapping () {
141
257
return parameterMapping .getParamMappingAsVariableMap ();
142
258
}
0 commit comments