-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
CoverageInstrumentationCallback.java
209 lines (186 loc) · 7.53 KB
/
CoverageInstrumentationCallback.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
/*
* Copyright 2009 The Closure Compiler Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.javascript.jscomp;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.annotations.GwtIncompatible;
import com.google.javascript.jscomp.CoverageInstrumentationPass.CoverageReach;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import java.util.Map;
/**
* This class implements a traversal to instrument an AST for code coverage.
* @author praveenk@google.com (Praveen Kumashi)
*
*/
@GwtIncompatible("FileInstrumentationData")
class CoverageInstrumentationCallback extends
NodeTraversal.AbstractPostOrderCallback {
private final Map<String, FileInstrumentationData> instrumentationData;
private final CoverageReach reach;
static final String ARRAY_NAME_PREFIX = "JSCompiler_lcov_data_";
public CoverageInstrumentationCallback(
Map<String, FileInstrumentationData> instrumentationData,
CoverageReach reach) {
this.instrumentationData = instrumentationData;
this.reach = reach;
}
/**
* Returns the name of the source file from which the given node originates.
* @param traversal the traversal
* @return the name of the file it originates from
*/
private static String getFileName(NodeTraversal traversal) {
return traversal.getSourceName();
}
/**
* Returns a string that can be used as array name. The name is based on the
* source filename of the AST node.
*/
private String createArrayName(NodeTraversal traversal) {
return ARRAY_NAME_PREFIX
+ CoverageUtil.createIdentifierFromText(getFileName(traversal));
}
/**
* Creates and return a new instrumentation node. The instrumentation Node is
* of the form: "arrayName[lineNumber] = true;"
* Note 1: Node returns a 1-based line number.
* Note 2: Line numbers in instrumentation are made 0-based. This is done to
* map to their bit representation in BitField. Thus, there's a one-to-one
* correspondence of the line number seen on instrumented files and their bit
* encodings.
*
* @return an instrumentation node corresponding to the line number
*/
private Node newInstrumentationNode(NodeTraversal traversal, Node node) {
int lineNumber = node.getLineno();
String arrayName = createArrayName(traversal);
// Create instrumentation Node
Node getElemNode = IR.getelem(
IR.name(arrayName),
IR.number(lineNumber - 1)); // Make line number 0-based
Node exprNode = IR.exprResult(IR.assign(getElemNode, IR.trueNode()));
// Note line as instrumented
String fileName = getFileName(traversal);
if (!instrumentationData.containsKey(fileName)) {
instrumentationData.put(fileName,
new FileInstrumentationData(fileName, arrayName));
}
instrumentationData.get(fileName).setLineAsInstrumented(lineNumber);
return exprNode.useSourceInfoIfMissingFromForTree(node);
}
/**
* Create and return a new array declaration node. The array name is
* generated based on the source filename, and declaration is of the form:
* "var arrayNameUsedInFile = [];"
*/
private Node newArrayDeclarationNode(NodeTraversal traversal) {
return IR.var(
IR.name(createArrayName(traversal)),
IR.arraylit());
}
/**
* @return a Node containing file specific setup logic.
*/
private Node newHeaderNode(NodeTraversal traversal, Node srcref) {
String fileName = getFileName(traversal);
String arrayName = createArrayName(traversal);
FileInstrumentationData data = instrumentationData.get(fileName);
checkNotNull(data);
String objName = CoverageInstrumentationPass.JS_INSTRUMENTATION_OBJECT_NAME;
// var JSCompiler_lcov_data_xx = [];
// __jscov['executedLines'].push(JSCompiler_lcov_data_xx);
// __jscov['instrumentedLines'].push(hex-data);
// __jscov['fileNames'].push(filename);
return IR.block(
newArrayDeclarationNode(traversal),
IR.exprResult(
IR.call(
IR.getprop(IR.getelem(IR.name(objName), IR.string("executedLines")), "push"),
IR.name(arrayName))),
IR.exprResult(
IR.call(
IR.getprop(
IR.getelem(IR.name(objName), IR.string("instrumentedLines")), "push"),
IR.string(data.getInstrumentedLinesAsHexString()))),
IR.exprResult(
IR.call(
IR.getprop(IR.getelem(IR.name(objName), IR.string("fileNames")), "push"),
IR.string(fileName))))
.useSourceInfoIfMissingFromForTree(srcref);
}
/**
* Instruments the JS code by inserting appropriate nodes into the AST. The
* instrumentation logic is tuned to collect "line coverage" data only.
*/
@Override
public void visit(NodeTraversal traversal, Node node, Node parent) {
// SCRIPT node is special - it is the root of the AST for code from a file.
// Append code to declare and initialize structures used in instrumentation.
if (node.isScript()) {
String fileName = getFileName(traversal);
if (instrumentationData.get(fileName) != null) {
if (node.hasChildren() && node.getFirstChild().isModuleBody()) {
node = node.getFirstChild();
}
node.addChildrenToFront(newHeaderNode(traversal, node).removeChildren());
}
traversal.reportCodeChange();
return;
}
// Don't instrument global statements
if (reach == CoverageReach.CONDITIONAL
&& parent != null && parent.isScript()) {
return;
}
// For arrow functions whose body is an expression instead of a block,
// convert it to a block so that it can be instrumented.
if (node.isFunction() && !NodeUtil.getFunctionBody(node).isBlock()) {
Node returnValue = NodeUtil.getFunctionBody(node);
Node body = IR.block(IR.returnNode(returnValue.detach()));
body.useSourceInfoIfMissingFromForTree(returnValue);
node.addChildToBack(body);
}
// Add instrumentation code just before a function block.
// Similarly before other constructs: 'with', 'case', 'default', 'catch'
if (node.isFunction()
|| node.isWith()
|| node.isCase()
|| node.isDefaultCase()
|| node.isCatch()) {
Node codeBlock = node.getLastChild();
codeBlock.addChildToFront(
newInstrumentationNode(traversal, node));
traversal.reportCodeChange();
return;
}
// Add instrumentation code as the first child of a 'try' block.
if (node.isTry()) {
Node firstChild = node.getFirstChild();
firstChild.addChildToFront(
newInstrumentationNode(traversal, node));
traversal.reportCodeChange();
return;
}
// For any other statement, add instrumentation code just before it.
if (parent != null && NodeUtil.isStatementBlock(parent) && !node.isModuleBody()) {
parent.addChildBefore(
newInstrumentationNode(traversal, node),
node);
traversal.reportCodeChange();
return;
}
}
}