UNPKG

10.8 kBJavaScriptView Raw
1var traverse = require('ast-traverse');
2var astUtils = require('sl-esprima-ast-utils');
3var globalErrorHandler = require('../global-error-handler');
4var babelParser = require("@babel/parser");
5var createNodeKey = require('./create-node-key').createNodeKey;
6var AstNodeTypes = require("./ast-node-types");
7var consoleFileLogger = require("../sl-logger.js").consoleFileLogger;
8var MethodSignature = require("./method-signature");
9var BranchSignature = require("./branch-signature");
10var globalMethodIndexer = require("./globalMethodIndexer");
11
12/**
13 * Traverse the AST and handle specific node types.
14 * Information about the Node Types can be found in: https://github.com/babel/babylon/blob/master/ast/spec.md
15 * @param absolutePtah
16 * @param relativePath
17 * @param content
18 * @param {*} sourceMaps
19 * @param isbranchCoverage
20 * @param opts
21 */
22var FileSignature = function (absolutePtah, relativePath, content, sourceMaps, opts, scmFilesContainer) {
23 this.absolutePath = absolutePtah;
24 this.relativePath = relativePath;
25 this.methods = [];
26 this.branches = [];
27 this.content = content;
28 this.sourceMaps = sourceMaps;
29 this.opts = opts;
30 this.scmFilesContainer = scmFilesContainer;
31 this.hasSourceMaps = !!sourceMaps;
32 this.logger = consoleFileLogger("FileSignature");
33 this.positionToMethodMap = {};
34 this.positionToBranchMap = {};
35 this.methodIndex = 0;
36 this.branchIndex = 0;
37};
38
39FileSignature.prototype._createSignature = function () {
40 this.prepareContent();
41 var ast = this.parseContentToAST();
42 this.traverseAST(ast);
43};
44
45FileSignature.prototype.createDTO = function(){
46 return {
47 methods: this.methods,
48 branches: this.branches,
49 filename: this.relativePath,
50 hasSourceMaps: this.hasSourceMaps
51 }
52};
53
54FileSignature.prototype.parseContentToAST = function () {
55 var sourceType = (this.opts && this.opts.es6Modules) ? 'module' : 'script';
56
57 var plugins = ["*", ["decorators", {decoratorsBeforeExport: true}]];
58 if (this.opts && this.opts.babylonPlugins) {
59 plugins = plugins.concat(this.opts.babylonPlugins);
60 }
61 return babelParser.parse(this.content, {
62 // parse in strict mode and allow module declarations
63 sourceType: sourceType,
64 plugins: plugins
65 })
66};
67FileSignature.prototype.traverseAST = function (ast) {
68 var context = this;
69 var nodeToParent = {};
70 var activeMethodKey = null;
71 var activeMethodIndex = null;
72 traverse(ast, {
73 pre: function (node, parentNode) {
74 var key = createNodeKey(node);
75 nodeToParent[key] = parentNode;
76 if (AstNodeTypes.isMethodNode(node)) {
77 // If there is no active method it means we are in global context.
78 if(activeMethodKey == null) {
79 activeMethodKey = createNodeKey(node);
80 }
81 globalMethodIndexer.incrementIndex();
82 /*
83 We set the active method index to point the current method index index.
84 This value sets the 'enclosingMethodIdx' in branches.
85 */
86 activeMethodIndex = globalMethodIndexer.getCurrentIndex();
87 context.addMethod(node, parentNode, nodeToParent);
88 context.addDefaultParameterBranch(node.params, parentNode, activeMethodIndex)
89 } else if (context.opts.isBranchCoverage && AstNodeTypes.isBranchNode(node)) {
90 context.addBranch(node, parentNode, activeMethodKey == null, activeMethodIndex)
91 }
92 },
93 post: function (node) {
94 if(AstNodeTypes.isMethodNode(node)){
95 // If we are quiting method which is active, its mean we are in global context.
96 if(activeMethodKey === createNodeKey(node)) {
97 activeMethodKey = null;
98 }
99 /*
100 If a function contains branch after method, then the 'activeMethodIndex' will contains the index of
101 the inner method while the branch belongs to the parent method.
102 Here we decreasing the index after each method so the 'enclosingMethodIdx' of the branch will point to parent.
103 */
104 activeMethodIndex --;
105 }
106 }
107 });
108};
109FileSignature.prototype.addMethod = function (node, parentNode, nodeToParent) {
110 var method = MethodSignature.create(node, parentNode, nodeToParent, this.absolutePath,
111 this.relativePath, this.logger);
112 method.srcData = method.generatedData;
113 this.methods.push(method.createDTO());
114 this.positionToMethodMap[method.generatedData.position] = this.methodIndex;
115 this.methodIndex++;
116};
117FileSignature.prototype.addBranch = function (node, parentNode, isGlobalBranch, activeMethodIndex) {
118 var context = this;
119 var branches = [];
120 if (node.type !== AstNodeTypes.LogicalExpression) {
121 branches = branches.concat(this.getBranchesToAdd(node, isGlobalBranch, activeMethodIndex));
122 }
123
124 // If this node its not logical exp inside logical exp.
125 if (node.type == AstNodeTypes.LogicalExpression && (parentNode == undefined || parentNode.type !== AstNodeTypes.LogicalExpression)) {
126 branches = branches.concat(this.getBranchesToAdd(node, isGlobalBranch, activeMethodIndex));
127 }
128
129 branches.forEach(function (branch) {
130 branch.srcData = branch.generatedData;
131 context.branches.push(branch.createDTO());
132 context.positionToBranchMap[branch.generatedData.position] = context.branchIndex;
133 context.branchIndex ++;
134 })
135};
136FileSignature.prototype.prepareContent = function () {
137 this.content = trimShebang(this.content);
138};
139FileSignature.prototype.getBranchesToAdd = function (node, isGlobalBranch, activeMethodIndex) {
140 var branchesToAdd = [];
141 var context = this;
142 var nodeKey = createNodeKey(node)
143 switch (node.type) {
144 // Those types contains some kind of "if else" structure.
145 case AstNodeTypes.IfStatement:
146 case AstNodeTypes.ConditionalExpression:
147 var branchIndex = 0;
148
149 // consequent mean when condition equals true.
150 if (node.consequent !== null) {
151 var branch = BranchSignature.create(node.consequent, node, node.type, branchIndex++, this.absolutePath, this.relativePath,
152 isGlobalBranch, activeMethodIndex, this.logger);
153 branchesToAdd.push(branch);
154 }
155
156 // altrnate equivalent to "else".
157 var clonedNode = null;
158 if (node.alternate == null) {
159 clonedNode = createEmptyAlternate(node);
160 }
161 var nodeToAdd = clonedNode || node;
162 var branch = BranchSignature.create(nodeToAdd.alternate, nodeToAdd, node.type, branchIndex++, this.absolutePath, this.relativePath,
163 isGlobalBranch, activeMethodIndex, this.logger);
164 branchesToAdd.push(branch);
165 break;
166 case AstNodeTypes.SwitchStatement:
167 var branchIndex = 0;
168
169 // Create branch from each case statement.
170 node.cases.forEach(function (caseNode) {
171 if (caseNode !== null) {
172 var branch = BranchSignature.create(caseNode, node, node.type, branchIndex++, context.absolutePath, context.relativePath,
173 isGlobalBranch, activeMethodIndex, context.logger);
174 branchesToAdd.push(branch);
175 }
176 });
177 break;
178 case AstNodeTypes.LogicalExpression:
179 var branchIndex = 0;
180 var leaves = [];
181 findLogicalExpressionLeaves(node, leaves);
182 leaves.forEach(function (leafNode) {
183 if (leafNode !== null) {
184 var branch = BranchSignature.create(leafNode, node, node.type, branchIndex++, context.absolutePath, context.relativePath,
185 isGlobalBranch, activeMethodIndex, context.logger);
186 branchesToAdd.push(branch);
187 }
188 });
189 break;
190 case AstNodeTypes.AssignmentPattern:
191 /*
192 In babel es6 default arg is resolves with type 'AssignmentPattern' anf istanbul reports
193 a branch with the location of the default value.
194 For example: (a=2) => {console.log(a)} will be reported with branch on location of '2'
195 */
196 var branchIndex = 0;
197 var branch = BranchSignature.create(node.right, node, AstNodeTypes.DefaultArgument, branchIndex++, this.absolutePath, this.relativePath,
198 isGlobalBranch, activeMethodIndex, this.logger);
199 branchesToAdd.push(branch);
200 break;
201 }
202 return branchesToAdd;
203};
204
205FileSignature.prototype.addDefaultParameterBranch = function(functionParams, parentNode, activeMethodIndex){
206 var context = this;
207 functionParams = functionParams || [];
208 functionParams.forEach(function (param) {
209 if(param.type === AstNodeTypes.AssignmentPattern){
210 context.addBranch(param, parentNode, true, activeMethodIndex)
211 }
212 })
213}
214
215
216FileSignature.create = function (absolutePtah, relativePath, content, sourceMaps, opts, scmFilesContainer) {
217 var signature = new this(absolutePtah, relativePath, content, sourceMaps, opts, scmFilesContainer);
218 signature._createSignature();
219 return signature;
220}
221
222// Brake the expression to pieces (every "or" will be different branch).
223function findLogicalExpressionLeaves(node, leaves) {
224 if (node.type === AstNodeTypes.LogicalExpression) {
225 findLogicalExpressionLeaves(node.left, leaves);
226 findLogicalExpressionLeaves(node.right, leaves);
227 } else {
228 leaves.push(node);
229 }
230}
231
232/**
233 * Taken from eslint - https://github.com/eslint/eslint/blob/183def6115cad6f17c82ef1c1a245eb22d0bee83/lib/eslint.js#L800
234 */
235function trimShebang(text) {
236 return text.replace(/^#!([^\r\n]+)/, function (match, captured) {
237 return "//" + captured;
238 });
239}
240
241/**
242 * When we have inner method we set it to empty block statement, because we want to calculate the hash and diff
243 * for outer method only.
244 * @param {*} methodNode
245 */
246
247function _createEmptyBlock() {
248 return {
249 type: AstNodeTypes.BlockStatement,
250 body: []
251 }
252}
253
254function createEmptyAlternate(node) {
255// Istanbul counts empty alternates and takes the consequent's location
256 var clonedNode = astUtils.clone(node);
257 clonedNode.alternate = _createEmptyBlock();
258
259 // Set alternate location to consequent location (same as istanbul).
260 clonedNode.alternate.loc = node.consequent !== null ? node.consequent.loc : node.loc;
261 return clonedNode;
262}
263
264module.exports = FileSignature;