Skip to content

Commit 2e5776d

Browse files
authored
Create lib_additional.gs
1 parent 62d48cc commit 2e5776d

File tree

1 file changed

+236
-0
lines changed

1 file changed

+236
-0
lines changed

src/lib_additional.gs

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
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

Comments
 (0)