|
| 1 | +/** |
| 2 | + * Creates a map of function calls within a script, optionally visualizing it. |
| 3 | + * @param {string} script_id - The ID of the script containing the functions.\ |
| 4 | + * Optional parameter. If this parameter is not provided, |
| 5 | + * script_id will be the current user script. |
| 6 | + * @param {string} filename - The name of the file in the script containing the functions. |
| 7 | + * @param {boolean} isDraw - Optional parameter. Determines whether to visualize |
| 8 | + * the function call map.\ |
| 9 | + * If set to 'true', the function call map will be visualized. |
| 10 | + * @returns {Object} Returns a map of function calls within the script. |
| 11 | + */ |
| 12 | +function createCalleeMap(script_id, filename, isDraw) { |
| 13 | + |
| 14 | + var lines = []; |
| 15 | + |
| 16 | + function getLineNumber(position) { |
| 17 | + let lineNumber = 0; |
| 18 | + let currentPos = 0; |
| 19 | + |
| 20 | + for (let i = 0; i < lines.length; i++) { |
| 21 | + currentPos += lines[i].length + 1; |
| 22 | + if (currentPos > position) { |
| 23 | + lineNumber = i + 1; |
| 24 | + break; |
| 25 | + } |
| 26 | + } |
| 27 | + return lineNumber; |
| 28 | + } |
| 29 | + |
| 30 | + function findFunctionsInSource(sourceCode) { |
| 31 | + //regex for searching function and class defs |
| 32 | + const functionRegex = /(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)\s*{/g; |
| 33 | + const arrowFunctionRegex = /(?:const\s+)?(\w+)\s*=\s*\(([^)]*)\)\s*=>\s*{/g; |
| 34 | + const classRegex = /class\s+(\w+)\s*{/g; |
| 35 | + |
| 36 | + let functions = []; |
| 37 | + let classes = []; |
| 38 | + let match; |
| 39 | + |
| 40 | + //searching for functions using regex |
| 41 | + while ((match = functionRegex.exec(sourceCode)) !== null) { |
| 42 | + const name = match[1]; |
| 43 | + const args = match[2] !== '' ? match[2].split(',').map(arg => arg.trim()) : []; |
| 44 | + functions.push({ |
| 45 | + name: name, |
| 46 | + type: 'function', |
| 47 | + args: args, |
| 48 | + startAt: match.index, |
| 49 | + line: getLineNumber(match.index) |
| 50 | + }); |
| 51 | + } |
| 52 | + while ((match = arrowFunctionRegex.exec(sourceCode)) !== null) { |
| 53 | + const name = match[1]; |
| 54 | + const args = match[2] !== '' ? match[2].split(',').map(arg => arg.trim()) : []; |
| 55 | + functions.push({ |
| 56 | + name: name, |
| 57 | + type: 'arrow function', |
| 58 | + args: args, |
| 59 | + startAt: match.index, |
| 60 | + line: getLineNumber(match.index) |
| 61 | + }); |
| 62 | + } |
| 63 | + //searching for classes using regex |
| 64 | + while ((match = classRegex.exec(sourceCode)) !== null) { |
| 65 | + classes.push({ |
| 66 | + name: match[1] |
| 67 | + }); |
| 68 | + } |
| 69 | + |
| 70 | + functions.sort((a, b) => a.startAt - b.startAt); |
| 71 | + |
| 72 | + return {functions, classes}; |
| 73 | + } |
| 74 | + |
| 75 | + function addCalleeMap(sourceCode, mapFunctions) { |
| 76 | + |
| 77 | + function parseFunctionCode(functionBody) { |
| 78 | + let [level, i] = [0, 0]; |
| 79 | + let result = ''; |
| 80 | + while (i < functionBody.length) { |
| 81 | + const char = functionBody[i]; |
| 82 | + if (char === '{') { |
| 83 | + level++; |
| 84 | + result += '{'; |
| 85 | + } else if (char === '}') { |
| 86 | + level--; |
| 87 | + result += '}'; |
| 88 | + if (level === 0) break; |
| 89 | + } else { |
| 90 | + result += char; |
| 91 | + } |
| 92 | + i++; |
| 93 | + } |
| 94 | + return result; |
| 95 | + } |
| 96 | + |
| 97 | + //searching for function calls inside the source code |
| 98 | + function findFunctionCalls(code, functionName, startAt) { |
| 99 | + const regex = new RegExp(`(?<!\\w)${functionName}\\s*\\((?<functionArguments>(?:[^()]+).*)?\\s*\\)`, 'g'); |
| 100 | + let matches = []; |
| 101 | + let match; |
| 102 | + while ((match = regex.exec(code)) !== null && matches.length < 10) { |
| 103 | + matches.push({ |
| 104 | + arguments: (match?.groups?.functionArguments ?? "") |
| 105 | + .split(",") |
| 106 | + .map(arg => arg.trim()) |
| 107 | + .filter(arg => arg !== "") |
| 108 | + .join(','), |
| 109 | + position: match.index + startAt + 1, |
| 110 | + line: getLineNumber(match.index + startAt + 1) |
| 111 | + }); |
| 112 | + } |
| 113 | + return matches; |
| 114 | + } |
| 115 | + |
| 116 | + //building a chain of function calls |
| 117 | + let allCallee = []; |
| 118 | + sourceCode = sourceCode.trim(); |
| 119 | + |
| 120 | + mapFunctions.forEach((func, idx) => { |
| 121 | + const nxStartAt = mapFunctions[idx+1]?.startAt || sourceCode.length; |
| 122 | + let funcBody = parseFunctionCode(sourceCode.slice(func.startAt, nxStartAt)); |
| 123 | + |
| 124 | + if (funcBody) { |
| 125 | + //looking for function calls inside the function body |
| 126 | + let calls = []; |
| 127 | + for (const calleeFunc of mapFunctions) { |
| 128 | + if (calleeFunc.name !== func.name) { |
| 129 | + let callArguments = findFunctionCalls(funcBody, calleeFunc.name, func.startAt); |
| 130 | + if (callArguments.length > 0) { |
| 131 | + calls.push({ |
| 132 | + name: calleeFunc.name, |
| 133 | + calls: callArguments, |
| 134 | + counts: callArguments.length |
| 135 | + }); |
| 136 | + allCallee.push(calleeFunc.name); |
| 137 | + } |
| 138 | + } |
| 139 | + } |
| 140 | + //add call information to the 'callee' field of the function |
| 141 | + func.callee = calls; |
| 142 | + } |
| 143 | + }); |
| 144 | + |
| 145 | + //check if functions are included in the 'callee' |
| 146 | + mapFunctions.forEach(func => { |
| 147 | + func.unused = !allCallee.includes(func.name); |
| 148 | + }); |
| 149 | + |
| 150 | + //functions with callee |
| 151 | + return mapFunctions; |
| 152 | + } |
| 153 | + |
| 154 | + function drawFunctionCallMap(calleeMap) { |
| 155 | + |
| 156 | + const LOG_LIMIT = 7800; |
| 157 | + const LINE_FILLER = '═'; |
| 158 | + const BLOCK_SIDE = '║'.padStart(6); |
| 159 | + const BLOCK_LB = '╚'.padStart(6); |
| 160 | + const BLOCK_RB = '╝'; |
| 161 | + const BLOCK_RT = '╗'; |
| 162 | + const BLOCK_LT = '╔'; |
| 163 | + const ARROW_UP = '▲'.padStart(10); |
| 164 | + const ARROW_DOWN = '▼'.padStart(10); |
| 165 | + const V_LINE = '│'.padStart(10); |
| 166 | + |
| 167 | + //object for storing positions & res draw |
| 168 | + const positions = {}; |
| 169 | + let drawMap = [""]; |
| 170 | + let printFunctions = []; |
| 171 | + |
| 172 | + function addLine(text, count = 1) { |
| 173 | + drawMap[drawMap.length - 1] += (text + '\n').repeat(count); |
| 174 | + } |
| 175 | + |
| 176 | + //calculating block position by function |
| 177 | + let _position = (name) => { |
| 178 | + if (!positions[name]) |
| 179 | + positions[name] = Object.keys(positions).length + 1; |
| 180 | + return positions[name]; |
| 181 | + } |
| 182 | + |
| 183 | + //draw blocks for each function and linking |
| 184 | + calleeMap.forEach(func => { |
| 185 | + const position = _position(func.name).toString().padStart(3); |
| 186 | + const tag = func.unused ? '*' : BLOCK_RT; |
| 187 | + const filler = LINE_FILLER.repeat(Math.max(func.name.length+5, 36)); |
| 188 | + addLine(`${position}. ${BLOCK_LT}${filler}${tag}`); |
| 189 | + addLine(`${BLOCK_SIDE} ${func.name.padEnd(31)} ${BLOCK_SIDE.trim()}`); |
| 190 | + addLine(BLOCK_LB + filler + BLOCK_RB); |
| 191 | + func.callee.forEach((call, _) => { |
| 192 | + const thisType = _ == 0 ? func.type + ', at: ' + func.line : '' |
| 193 | + const calleePosition = _position(call.name); |
| 194 | + addLine(`${ARROW_UP} ${thisType}`); |
| 195 | + addLine(V_LINE, 3); |
| 196 | + addLine(ARROW_DOWN); |
| 197 | + addLine(`${position} ────── ${calleePosition} (${call.name})`); |
| 198 | + }); |
| 199 | + printFunctions.push(`${_position(func.name)}. ${func.name}`); |
| 200 | + if (drawMap[drawMap.length - 1].length > LOG_LIMIT) drawMap.push(""); |
| 201 | + }); |
| 202 | + |
| 203 | + drawMap[drawMap.length - 1] += drawMap[drawMap.length - 1].length > 0 |
| 204 | + || drawMap.length > 1 |
| 205 | + ? `* - Likely unused functions` |
| 206 | + : `Nothing to view`; |
| 207 | + |
| 208 | + L('Function list:', printFunctions); |
| 209 | + drawMap.forEach(chunk => { |
| 210 | + L(chunk); |
| 211 | + }); |
| 212 | + } |
| 213 | + |
| 214 | + script_id = script_id ? script_id : ScriptApp.getScriptId(); |
| 215 | + const scriptData = getScriptContent(script_id); |
| 216 | + |
| 217 | + if(!scriptData) throw new Error("Unable to access the contents of the script."); |
| 218 | + |
| 219 | + const fileData = scriptData.files.find(file => { |
| 220 | + return file.name === filename |
| 221 | + }); |
| 222 | + |
| 223 | + L("Script file:", filename); |
| 224 | + if (fileData && fileData.source) { |
| 225 | + lines = fileData.source.split('\n'); |
| 226 | + const map_functions = findFunctionsInSource(fileData.source); |
| 227 | + const callee_map = addCalleeMap(fileData.source, map_functions.functions); |
| 228 | + if (isDraw) drawFunctionCallMap(callee_map); |
| 229 | + return {classes: map_functions.classes, functions: callee_map}; |
| 230 | + } else { |
| 231 | + L("The file was not found or does not contain the source."); |
| 232 | + } |
| 233 | + |
| 234 | + return { classes:[], functions:[] }; |
| 235 | +} |
| 236 | + |
0 commit comments