UNPKG

grunt-remove-logging-calls

Version:

Grunt task to remove or replace all or any parts of logging statements from javascript code

188 lines (171 loc) 6.99 kB
/* * grunt-remove-logging-calls * http://gruntjs.com/ * * Copyright (c) 2014 Jean-Daniel Borowy * Licensed under the MIT license. */ exports.init = function(grunt) { 'use strict'; var esprima = module.require('esprima'); /** * Compute the breadth first search on syntax tree to get the list of nodes * @param {Node} rootNode the root node of the tree * @return {Node[]} the list of all the tree nodes */ var breadthFirstSearch = function(rootNode) { // queue to compute de breadth first search var queue = [rootNode]; var nodes = [rootNode]; while (queue.length !== 0) { // fetch child nodes of the first node of the queue var currentNode = queue.shift(); for (var property in currentNode) { // compute only non-inherited properties if (currentNode.hasOwnProperty(property)) { if (Array.isArray(currentNode[property])) { var nodeArray = currentNode[property]; for (var i = 0, ii = nodeArray.length; i < ii; ++i) { // enqueue only if it is a Node instance if (nodeArray[i] instanceof rootNode.constructor) { queue.push(nodeArray[i]); nodes.push(nodeArray[i]); } } } else // enqueue only if it is a Node instance if (currentNode[property] instanceof rootNode.constructor) { queue.push(currentNode[property]); nodes.push(currentNode[property]); } } } } return nodes; }; /** * Identify nodes that relate to console statements * @param {Node[]} nodes the list of nodes * @param {Node[]} methods the methods to identify * @param {String[]} methods the properties (functions) to catch for blanking */ var getSegmentsToReplace = function(nodes, methods) { var segmentsToBlank = []; for (var i = 0, ii = nodes.length; i < ii; ++i) { // checks if the statement is a CallExpression if (nodes[i].type === 'CallExpression' && nodes[i].callee.type === 'MemberExpression') { var nodeExpression = nodes[i]; // checks if the called function is a property of console or window.console if (nodeExpression.callee.object.name === 'console' || ( nodeExpression.callee.object.property !== undefined && nodeExpression.callee.object.object !== undefined && nodeExpression.callee.object.property.name === 'console' && nodeExpression.callee.object.object.name === 'window' )) { // push the node if the current function is within the methods array if (methods.indexOf(nodeExpression.callee.property.name) > -1) { segmentsToBlank.push(nodeExpression.range); } } } } // sort the range segments list segmentsToBlank.sort(function(a, b) { return a[0] - b[0]; }); // handle the case of nested segments : "console.log(console.log('foo'))" removeNestedSegments(segmentsToBlank); return segmentsToBlank; }; /** * Remove segments that are nested in other * @param {Node[]} node the console calls nodes */ var removeNestedSegments = function(segments) { var previousSegment = [-1, -1]; var segmentIndicesToRemove = []; var cleanedSegmentArray = []; var i, ii; for (i = 0, ii = segments.length; i < ii; ++i) { if (!(previousSegment[0] <= segments[i][0] && previousSegment[1] >= segments[i][1])) { cleanedSegmentArray.push(segments[i]); } previousSegment = segments[i]; } segments.length = 0; for (i = 0, ii = cleanedSegmentArray.length; i < ii; ++i) { segments.push(cleanedSegmentArray[i]); } }; var spaceCode = ' '.charCodeAt(0); var tabCode = '\t'.charCodeAt(0); var semicolonCode = ';'.charCodeAt(0); /** * * @param {String} sourceCode the javascript source * @param {Number} semicolonCode the ascii code of semicolon character * @param {Number} start the start index * @param {Number} end the end index */ var jumpAfterSemicolon = function(sourceCode, start, end) { for (var i = start; i < end; ++i) { switch (sourceCode.charCodeAt(i)) { case semicolonCode: return i + 1; case spaceCode: case tabCode: continue; default: return start; } } return start; }; /** * * @param {String} sourceCode the javascript source code to be cleaned * @param {String[]} methodNames the methods to catch on the console object for blanking * @param {function} strategy the strategy to replace console calls * @return {} */ var process = function(sourceCode, methodNames, strategy, removeSemicolonIfPossible) { // default console calls to replace methodNames = methodNames === undefined ? ['log', 'info', 'assert'] : methodNames; methodNames = Array.isArray(methodNames) ? methodNames : [methodNames]; // by default do not remove the semicolon removeSemicolonIfPossible = removeSemicolonIfPossible === undefined ? false : true; if (strategy === undefined) { // default strategy to replace console statements if (removeSemicolonIfPossible) { strategy = function(loggingStatement) { return '/* ' + loggingStatement + ' */'; }; } else { strategy = function(loggingStatement) { return 'null /* ' + loggingStatement + ' */'; }; } } var syntaxTree = esprima.parse(sourceCode, { range: true }); var nodes = breadthFirstSearch(syntaxTree); var segments = getSegmentsToReplace(nodes, methodNames); var result = ''; var previousSegment = [0, 0]; for (var i = 0, ii = segments.length; i < ii; ++i) { result += sourceCode.slice(previousSegment[1], segments[i][0]); if (removeSemicolonIfPossible) { segments[i][1] = jumpAfterSemicolon(sourceCode, segments[i][1], i + 1 < ii ? segments[i + 1][0] : sourceCode.length); } result += strategy(sourceCode.slice(segments[i][0], segments[i][1])); previousSegment = segments[i]; } result += sourceCode.slice(previousSegment[1], sourceCode.length); return result; }; return { process: process }; };