callograph 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/cli.cjs +301 -18
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +2 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +301 -19
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +36 -36
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Callograph** is a static analysis tool for TypeScript projects that builds a program-level call graph and detects side effects, impurity propagation, and unsafe async behavior.
|
|
4
4
|
|
|
5
|
-
It is designed for engineers who want deeper insight into how code behaves
|
|
5
|
+
It is designed for engineers who want deeper insight into how code behaves beyond lint rules, by analyzing symbol resolution and cross-file call relationships using the TypeScript Compiler API.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -21,7 +21,7 @@ Callograph provides structural visibility into function relationships and propag
|
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
24
|
-
## Core Capabilities (v0.
|
|
24
|
+
## Core Capabilities (v0.0.3)
|
|
25
25
|
|
|
26
26
|
### 1. Call Graph Construction
|
|
27
27
|
|
|
@@ -231,7 +231,7 @@ It prioritizes:
|
|
|
231
231
|
|
|
232
232
|
---
|
|
233
233
|
|
|
234
|
-
## Limitations (v0.
|
|
234
|
+
## Limitations (v0.0.3)
|
|
235
235
|
|
|
236
236
|
* Dynamic runtime dispatch cannot always be resolved.
|
|
237
237
|
* Higher-order functions are partially supported.
|
|
@@ -267,7 +267,7 @@ Contributions are welcome.
|
|
|
267
267
|
* Add tests for new analysis rules.
|
|
268
268
|
|
|
269
269
|
---
|
|
270
|
-
|
|
270
|
+
|
|
271
271
|
## Vision
|
|
272
272
|
|
|
273
273
|
Callograph aims to become:
|
package/dist/cli.cjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
"use strict";
|
|
3
2
|
var __create = Object.create;
|
|
4
3
|
var __defProp = Object.defineProperty;
|
|
@@ -65,7 +64,11 @@ function collectFunctions(program2) {
|
|
|
65
64
|
if (sf.isDeclarationFile) continue;
|
|
66
65
|
if (sf.fileName.includes("node_modules")) continue;
|
|
67
66
|
import_typescript2.default.forEachChild(sf, function visit(n) {
|
|
68
|
-
|
|
67
|
+
const isNamedFunctionDecl = import_typescript2.default.isFunctionDeclaration(n) && !!n.name;
|
|
68
|
+
const isMethod = import_typescript2.default.isMethodDeclaration(n);
|
|
69
|
+
const p = n.parent;
|
|
70
|
+
const isVarAssigned = (import_typescript2.default.isArrowFunction(n) || import_typescript2.default.isFunctionExpression(n)) && !!p && import_typescript2.default.isVariableDeclaration(p) && import_typescript2.default.isIdentifier(p.name);
|
|
71
|
+
if (isNamedFunctionDecl || isMethod || isVarAssigned) {
|
|
69
72
|
const name = safeName(n, sf);
|
|
70
73
|
const id = `${sf.fileName}:${n.pos}:${name}`;
|
|
71
74
|
out.push({ id, name, file: sf.fileName, node: n });
|
|
@@ -78,33 +81,284 @@ function collectFunctions(program2) {
|
|
|
78
81
|
|
|
79
82
|
// src/analyzer/buildCallGraph.ts
|
|
80
83
|
var import_typescript3 = __toESM(require("typescript"), 1);
|
|
84
|
+
function isProjectFile(fileName) {
|
|
85
|
+
return !fileName.includes("node_modules") && !fileName.endsWith(".d.ts");
|
|
86
|
+
}
|
|
81
87
|
function buildCallGraph(program2, functions2) {
|
|
82
88
|
const checker = program2.getTypeChecker();
|
|
83
89
|
const graph2 = /* @__PURE__ */ new Map();
|
|
84
|
-
const
|
|
90
|
+
const declToId = /* @__PURE__ */ new Map();
|
|
85
91
|
for (const fn of functions2) {
|
|
86
|
-
|
|
87
|
-
|
|
92
|
+
graph2.set(fn.id, /* @__PURE__ */ new Set());
|
|
93
|
+
declToId.set(fn.node, fn.id);
|
|
88
94
|
}
|
|
89
95
|
for (const fn of functions2) {
|
|
90
|
-
|
|
91
|
-
import_typescript3.default.forEachChild(fn.node, function visit(node) {
|
|
96
|
+
let visit2 = function(node) {
|
|
92
97
|
if (import_typescript3.default.isCallExpression(node)) {
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
96
|
-
const
|
|
97
|
-
if (
|
|
98
|
-
graph2.get(
|
|
98
|
+
const sig = checker.getResolvedSignature(node);
|
|
99
|
+
const decl = sig?.getDeclaration();
|
|
100
|
+
if (decl && isProjectFile(decl.getSourceFile().fileName)) {
|
|
101
|
+
const calleeId = declToId.get(decl);
|
|
102
|
+
if (calleeId) {
|
|
103
|
+
graph2.get(callerId).add(calleeId);
|
|
99
104
|
}
|
|
100
105
|
}
|
|
101
106
|
}
|
|
102
|
-
import_typescript3.default.forEachChild(node,
|
|
103
|
-
}
|
|
107
|
+
import_typescript3.default.forEachChild(node, visit2);
|
|
108
|
+
};
|
|
109
|
+
var visit = visit2;
|
|
110
|
+
const callerId = fn.id;
|
|
111
|
+
visit2(fn.node);
|
|
104
112
|
}
|
|
105
113
|
return graph2;
|
|
106
114
|
}
|
|
107
115
|
|
|
116
|
+
// src/analyzer/purity.ts
|
|
117
|
+
var import_typescript6 = __toESM(require("typescript"), 1);
|
|
118
|
+
|
|
119
|
+
// src/analyzer/helpers/globals.ts
|
|
120
|
+
var GLOBAL_OBJECTS = /* @__PURE__ */ new Set([
|
|
121
|
+
"window",
|
|
122
|
+
"document",
|
|
123
|
+
"global",
|
|
124
|
+
"process",
|
|
125
|
+
"localStorage",
|
|
126
|
+
"sessionStorage"
|
|
127
|
+
]);
|
|
128
|
+
var MUTATING_ARRAY_METHODS = /* @__PURE__ */ new Set([
|
|
129
|
+
"push",
|
|
130
|
+
"pop",
|
|
131
|
+
"splice",
|
|
132
|
+
"shift",
|
|
133
|
+
"unshift",
|
|
134
|
+
"sort",
|
|
135
|
+
"reverse",
|
|
136
|
+
"copyWithin",
|
|
137
|
+
"fill"
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
// src/analyzer/helpers/ast.ts
|
|
141
|
+
var import_typescript4 = __toESM(require("typescript"), 1);
|
|
142
|
+
function isInside(node, container) {
|
|
143
|
+
return node.pos >= container.pos && node.end <= container.end;
|
|
144
|
+
}
|
|
145
|
+
function rootIdentifier(expr) {
|
|
146
|
+
if (import_typescript4.default.isIdentifier(expr)) return expr;
|
|
147
|
+
if (import_typescript4.default.isPropertyAccessExpression(expr)) return rootIdentifier(expr.expression);
|
|
148
|
+
if (import_typescript4.default.isElementAccessExpression(expr)) return rootIdentifier(expr.expression);
|
|
149
|
+
if (import_typescript4.default.isParenthesizedExpression(expr)) return rootIdentifier(expr.expression);
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
function isWriteOperator(kind) {
|
|
153
|
+
return kind === import_typescript4.default.SyntaxKind.EqualsToken || kind === import_typescript4.default.SyntaxKind.PlusEqualsToken || kind === import_typescript4.default.SyntaxKind.MinusEqualsToken || kind === import_typescript4.default.SyntaxKind.AsteriskEqualsToken || kind === import_typescript4.default.SyntaxKind.SlashEqualsToken || kind === import_typescript4.default.SyntaxKind.PercentEqualsToken || kind === import_typescript4.default.SyntaxKind.AmpersandEqualsToken || kind === import_typescript4.default.SyntaxKind.BarEqualsToken || kind === import_typescript4.default.SyntaxKind.CaretEqualsToken || kind === import_typescript4.default.SyntaxKind.LessThanLessThanEqualsToken || kind === import_typescript4.default.SyntaxKind.GreaterThanGreaterThanEqualsToken || kind === import_typescript4.default.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/analyzer/helpers/async.ts
|
|
157
|
+
var import_typescript5 = __toESM(require("typescript"), 1);
|
|
158
|
+
function isPromiseLike(type, checker) {
|
|
159
|
+
if (!type) return false;
|
|
160
|
+
const thenProp = type.getProperty("then");
|
|
161
|
+
if (!thenProp) return false;
|
|
162
|
+
const decl = thenProp.valueDeclaration ?? thenProp.declarations?.[0];
|
|
163
|
+
if (!decl) return false;
|
|
164
|
+
const thenType = checker.getTypeOfSymbolAtLocation(thenProp, decl);
|
|
165
|
+
return thenType.getCallSignatures().length > 0;
|
|
166
|
+
}
|
|
167
|
+
function isAwaited(node) {
|
|
168
|
+
return import_typescript5.default.isAwaitExpression(node.parent);
|
|
169
|
+
}
|
|
170
|
+
function isReturned(node) {
|
|
171
|
+
return import_typescript5.default.isReturnStatement(node.parent);
|
|
172
|
+
}
|
|
173
|
+
function isHandledWithThen(node) {
|
|
174
|
+
if (!import_typescript5.default.isPropertyAccessExpression(node.parent)) return false;
|
|
175
|
+
const name = node.parent.name.text;
|
|
176
|
+
return name === "then" || name === "catch" || name === "finally";
|
|
177
|
+
}
|
|
178
|
+
function isAsyncForEach(node) {
|
|
179
|
+
if (!import_typescript5.default.isPropertyAccessExpression(node.expression)) return false;
|
|
180
|
+
if (node.expression.name.text !== "forEach") return false;
|
|
181
|
+
const cb = node.arguments[0];
|
|
182
|
+
if (!cb) return false;
|
|
183
|
+
if (import_typescript5.default.isArrowFunction(cb) || import_typescript5.default.isFunctionExpression(cb)) {
|
|
184
|
+
return cb.modifiers?.some((m) => m.kind === import_typescript5.default.SyntaxKind.AsyncKeyword) ?? false;
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
function isAsyncEventListener(node) {
|
|
189
|
+
if (!import_typescript5.default.isPropertyAccessExpression(node.expression)) return false;
|
|
190
|
+
if (node.expression.name.text !== "addEventListener") return false;
|
|
191
|
+
const cb = node.arguments[1];
|
|
192
|
+
if (!cb) return false;
|
|
193
|
+
if (import_typescript5.default.isArrowFunction(cb) || import_typescript5.default.isFunctionExpression(cb)) {
|
|
194
|
+
return cb.modifiers?.some((m) => m.kind === import_typescript5.default.SyntaxKind.AsyncKeyword) ?? false;
|
|
195
|
+
}
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
function isPromiseAll(node) {
|
|
199
|
+
return import_typescript5.default.isPropertyAccessExpression(node.expression) && import_typescript5.default.isIdentifier(node.expression.expression) && node.expression.expression.text === "Promise" && node.expression.name.text === "all";
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// src/analyzer/purity.ts
|
|
203
|
+
function analyzePurity(program2, functions2) {
|
|
204
|
+
const checker = program2.getTypeChecker();
|
|
205
|
+
const results = [];
|
|
206
|
+
for (const fn of functions2) {
|
|
207
|
+
let visit2 = function(node) {
|
|
208
|
+
if (import_typescript6.default.isBinaryExpression(node) && isWriteOperator(node.operatorToken.kind)) {
|
|
209
|
+
const left = node.left;
|
|
210
|
+
const root = rootIdentifier(left);
|
|
211
|
+
if (root && paramNames.has(root.text)) {
|
|
212
|
+
mark(`Mutates parameter \`${root.text}\``);
|
|
213
|
+
}
|
|
214
|
+
if (import_typescript6.default.isIdentifier(left)) {
|
|
215
|
+
const sym = checker.getSymbolAtLocation(left);
|
|
216
|
+
const decls = sym?.getDeclarations() ?? [];
|
|
217
|
+
const declaredInside = decls.some((d) => isInside(d, fn.node));
|
|
218
|
+
if (sym && decls.length > 0 && !declaredInside) {
|
|
219
|
+
mark(`Writes to outer scope \`${left.text}\``);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (import_typescript6.default.isPropertyAccessExpression(left) && left.expression.kind === import_typescript6.default.SyntaxKind.ThisKeyword) {
|
|
223
|
+
mark(`Mutates \`this.${left.name.text}\``);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (import_typescript6.default.isCallExpression(node) && import_typescript6.default.isPropertyAccessExpression(node.expression)) {
|
|
227
|
+
const method = node.expression.name.text;
|
|
228
|
+
const root = rootIdentifier(node.expression.expression);
|
|
229
|
+
if (root && paramNames.has(root.text) && MUTATING_ARRAY_METHODS.has(method)) {
|
|
230
|
+
mark(`Mutates parameter \`${root.text}\` via \`${method}()\``);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (import_typescript6.default.isIdentifier(node)) {
|
|
234
|
+
const sym = checker.getSymbolAtLocation(node);
|
|
235
|
+
if (sym && GLOBAL_OBJECTS.has(sym.getName())) {
|
|
236
|
+
mark(`Accesses global \`${sym.getName()}\``);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (import_typescript6.default.isPropertyAccessExpression(node)) {
|
|
240
|
+
const root = rootIdentifier(node.expression);
|
|
241
|
+
if (root?.text === "process" && node.name.text === "env") {
|
|
242
|
+
mark("Accesses `process.env` (non-deterministic)");
|
|
243
|
+
}
|
|
244
|
+
if (root?.text === "console") {
|
|
245
|
+
mark("Writes to console");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (import_typescript6.default.isCallExpression(node)) {
|
|
249
|
+
const type = checker.getTypeAtLocation(node);
|
|
250
|
+
const expr = node.expression;
|
|
251
|
+
if (import_typescript6.default.isPropertyAccessExpression(expr) && import_typescript6.default.isIdentifier(expr.expression) && expr.expression.text === "Date" && expr.name.text === "now") {
|
|
252
|
+
mark("Calls Date.now() (non-deterministic)");
|
|
253
|
+
}
|
|
254
|
+
if (import_typescript6.default.isPropertyAccessExpression(expr) && import_typescript6.default.isIdentifier(expr.expression) && expr.expression.text === "Math" && expr.name.text === "random") {
|
|
255
|
+
mark("Calls Math.random() (non-deterministic)");
|
|
256
|
+
}
|
|
257
|
+
if (isPromiseLike(type, checker)) {
|
|
258
|
+
if (!isAwaited(node) && !isReturned(node) && !isHandledWithThen(node)) {
|
|
259
|
+
mark("Floating Promise (result ignored)");
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (isPromiseAll(node) && !isAwaited(node) && !isReturned(node)) {
|
|
263
|
+
mark("Unawaited Promise.all()");
|
|
264
|
+
}
|
|
265
|
+
if (isAsyncForEach(node)) {
|
|
266
|
+
mark("Async callback inside forEach (not awaited)");
|
|
267
|
+
}
|
|
268
|
+
if (isAsyncEventListener(node)) {
|
|
269
|
+
mark("Async event listener without error boundary");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (import_typescript6.default.isNewExpression(node)) {
|
|
273
|
+
if (import_typescript6.default.isIdentifier(node.expression) && node.expression.text === "Date") {
|
|
274
|
+
mark("Instantiates Date (non-deterministic)");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (import_typescript6.default.isAwaitExpression(node)) {
|
|
278
|
+
containsAwait = true;
|
|
279
|
+
}
|
|
280
|
+
if (import_typescript6.default.isTryStatement(node) && node.catchClause) {
|
|
281
|
+
hasTryCatch = true;
|
|
282
|
+
}
|
|
283
|
+
if (import_typescript6.default.isCatchClause(node) && node.block.statements.length === 0) {
|
|
284
|
+
mark("Empty catch block (error swallowed)");
|
|
285
|
+
}
|
|
286
|
+
import_typescript6.default.forEachChild(node, visit2);
|
|
287
|
+
};
|
|
288
|
+
var visit = visit2;
|
|
289
|
+
const sf = fn.node.getSourceFile();
|
|
290
|
+
const { line, character } = sf.getLineAndCharacterOfPosition(
|
|
291
|
+
fn.node.getStart(sf)
|
|
292
|
+
);
|
|
293
|
+
const reasons = [];
|
|
294
|
+
const paramNames = /* @__PURE__ */ new Set();
|
|
295
|
+
for (const p of fn.node.parameters) {
|
|
296
|
+
if (import_typescript6.default.isIdentifier(p.name)) paramNames.add(p.name.text);
|
|
297
|
+
}
|
|
298
|
+
const mark = (reason) => {
|
|
299
|
+
if (!reasons.includes(reason)) reasons.push(reason);
|
|
300
|
+
};
|
|
301
|
+
let containsAwait = false;
|
|
302
|
+
let hasTryCatch = false;
|
|
303
|
+
const isAsyncFunction = fn.node.modifiers?.some((m) => m.kind === import_typescript6.default.SyntaxKind.AsyncKeyword) ?? false;
|
|
304
|
+
visit2(fn.node);
|
|
305
|
+
if (isAsyncFunction && !containsAwait) {
|
|
306
|
+
mark("Async function without await");
|
|
307
|
+
}
|
|
308
|
+
if (isAsyncFunction && containsAwait && !hasTryCatch) {
|
|
309
|
+
mark("Async function without error handling (no try/catch)");
|
|
310
|
+
}
|
|
311
|
+
results.push({
|
|
312
|
+
id: fn.id,
|
|
313
|
+
name: fn.name,
|
|
314
|
+
file: fn.file,
|
|
315
|
+
line: line + 1,
|
|
316
|
+
character: character + 1,
|
|
317
|
+
isPure: reasons.length === 0,
|
|
318
|
+
reasons
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
return results;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/analyzer/propagateImpurity.ts
|
|
325
|
+
function propagateImpurity(graph2, purity2) {
|
|
326
|
+
const resultMap = /* @__PURE__ */ new Map();
|
|
327
|
+
for (const r of purity2) {
|
|
328
|
+
resultMap.set(r.id, r);
|
|
329
|
+
}
|
|
330
|
+
const reverse = /* @__PURE__ */ new Map();
|
|
331
|
+
for (const [callerId, calleeIds] of graph2.entries()) {
|
|
332
|
+
for (const calleeId of calleeIds) {
|
|
333
|
+
if (!reverse.has(calleeId)) {
|
|
334
|
+
reverse.set(calleeId, /* @__PURE__ */ new Set());
|
|
335
|
+
}
|
|
336
|
+
reverse.get(calleeId).add(callerId);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const queue = [];
|
|
340
|
+
for (const r of purity2) {
|
|
341
|
+
if (!r.isPure) {
|
|
342
|
+
queue.push(r.id);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
while (queue.length > 0) {
|
|
346
|
+
const impureId = queue.shift();
|
|
347
|
+
const callers = reverse.get(impureId);
|
|
348
|
+
if (!callers) continue;
|
|
349
|
+
for (const callerId of callers) {
|
|
350
|
+
const caller = resultMap.get(callerId);
|
|
351
|
+
if (!caller) continue;
|
|
352
|
+
if (caller.isPure) {
|
|
353
|
+
caller.isPure = false;
|
|
354
|
+
caller.reasons.push(`Calls impure function`);
|
|
355
|
+
queue.push(callerId);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return Array.from(resultMap.values());
|
|
360
|
+
}
|
|
361
|
+
|
|
108
362
|
// src/cli.ts
|
|
109
363
|
var command = process.argv[2];
|
|
110
364
|
if (command !== "analyze") {
|
|
@@ -114,10 +368,39 @@ if (command !== "analyze") {
|
|
|
114
368
|
var program = createTsProgram("tsconfig.json");
|
|
115
369
|
var functions = collectFunctions(program);
|
|
116
370
|
var graph = buildCallGraph(program, functions);
|
|
371
|
+
var purity = analyzePurity(program, functions);
|
|
372
|
+
purity = propagateImpurity(graph, purity);
|
|
117
373
|
console.log("\nCall Graph:\n");
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
374
|
+
var edgeCount = 0;
|
|
375
|
+
for (const [callerId, callees] of graph.entries()) {
|
|
376
|
+
const caller = functions.find((f) => f.id === callerId);
|
|
377
|
+
const callerName = caller ? caller.name : callerId;
|
|
378
|
+
for (const calleeId of callees) {
|
|
379
|
+
const callee = functions.find((f) => f.id === calleeId);
|
|
380
|
+
const calleeName = callee ? callee.name : calleeId;
|
|
381
|
+
console.log(`${callerName} \u2192 ${calleeName}`);
|
|
382
|
+
edgeCount++;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (edgeCount === 0) {
|
|
386
|
+
console.log("(no project-level call edges found)");
|
|
387
|
+
}
|
|
388
|
+
console.log(`
|
|
389
|
+
Total call edges: ${edgeCount}`);
|
|
390
|
+
console.log("\nPurity Report:\n");
|
|
391
|
+
var impure = purity.filter((r) => !r.isPure);
|
|
392
|
+
if (impure.length === 0) {
|
|
393
|
+
console.log("All analyzed functions are pure.");
|
|
394
|
+
} else {
|
|
395
|
+
for (const r of impure) {
|
|
396
|
+
console.log(`${r.file}:${r.line}:${r.character}`);
|
|
397
|
+
console.log(`Function: ${r.name}`);
|
|
398
|
+
console.log(`Status: X Impure`);
|
|
399
|
+
console.log("Reasons:");
|
|
400
|
+
for (const reason of r.reasons) {
|
|
401
|
+
console.log(` - ${reason}`);
|
|
402
|
+
}
|
|
403
|
+
console.log("");
|
|
121
404
|
}
|
|
122
405
|
}
|
|
123
406
|
//# sourceMappingURL=cli.cjs.map
|
package/dist/cli.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/analyzer/createProgram.ts","../src/analyzer/collectFunctions.ts","../src/analyzer/buildCallGraph.ts","../src/cli.ts"],"sourcesContent":["import ts from \"typescript\";\r\n\r\nexport function createTsProgram(tsconfigPath: string) {\r\n const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);\r\n\r\n if (configFile.error) {\r\n throw new Error(\r\n ts.flattenDiagnosticMessageText(configFile.error.messageText, \"\\n\")\r\n );\r\n }\r\n\r\n const parsedConfig = ts.parseJsonConfigFileContent(\r\n configFile.config,\r\n ts.sys,\r\n process.cwd()\r\n );\r\n\r\n return ts.createProgram({\r\n rootNames: parsedConfig.fileNames,\r\n options: parsedConfig.options,\r\n });\r\n}\r\n","import ts from \"typescript\";\r\n\r\nexport interface FunctionNode {\r\n id: string;\r\n name: string;\r\n file: string;\r\n node: ts.FunctionLikeDeclaration;\r\n}\r\n\r\nfunction safeName(node: ts.FunctionLikeDeclaration, sf: ts.SourceFile): string {\r\n const anyNode = node as any;\r\n\r\n // Named declarations / methods: function foo() {} / class X { foo() {} }\r\n if (anyNode.name && ts.isIdentifier(anyNode.name)) return anyNode.name.text;\r\n\r\n // Methods with string/numeric names: class X { \"foo\"() {} }\r\n if (anyNode.name && (ts.isStringLiteral(anyNode.name) || ts.isNumericLiteral(anyNode.name))) {\r\n return String(anyNode.name.text);\r\n }\r\n\r\n // const foo = () => {} / const foo = function() {}\r\n const p = node.parent;\r\n if (p && ts.isVariableDeclaration(p) && ts.isIdentifier(p.name)) return p.name.text;\r\n\r\n // obj.foo = function() {}\r\n if (p && ts.isBinaryExpression(p) && ts.isPropertyAccessExpression(p.left)) {\r\n return p.left.name.text;\r\n }\r\n\r\n const { line, character } = sf.getLineAndCharacterOfPosition(node.getStart(sf));\r\n return `<anonymous@${line + 1}:${character + 1}>`;\r\n}\r\n\r\nexport function collectFunctions(program: ts.Program): FunctionNode[] {\r\n const out: FunctionNode[] = [];\r\n\r\n for (const sf of program.getSourceFiles()) {\r\n if (sf.isDeclarationFile) continue;\r\n if (sf.fileName.includes(\"node_modules\")) continue;\r\n\r\n ts.forEachChild(sf, function visit(n) {\r\n if (\r\n ts.isFunctionDeclaration(n) ||\r\n ts.isMethodDeclaration(n) ||\r\n ts.isArrowFunction(n) ||\r\n ts.isFunctionExpression(n)\r\n ) {\r\n const name = safeName(n, sf);\r\n const id = `${sf.fileName}:${n.pos}:${name}`;\r\n out.push({ id, name, file: sf.fileName, node: n });\r\n }\r\n ts.forEachChild(n, visit);\r\n });\r\n }\r\n\r\n return out;\r\n}\r\n","import ts from \"typescript\";\r\nimport { FunctionNode } from \"./collectFunctions\";\r\n\r\nexport type CallGraph = Map<string, Set<string>>;\r\n\r\nexport function buildCallGraph(\r\n program: ts.Program,\r\n functions: FunctionNode[]\r\n): CallGraph {\r\n const checker = program.getTypeChecker();\r\n const graph: CallGraph = new Map();\r\n\r\n const functionMap = new Map<string, ts.FunctionLikeDeclaration>();\r\n\r\n for (const fn of functions) {\r\n functionMap.set(fn.name, fn.node);\r\n graph.set(fn.name, new Set());\r\n }\r\n\r\n for (const fn of functions) {\r\n const fnName = fn.name;\r\n\r\n ts.forEachChild(fn.node, function visit(node) {\r\n if (ts.isCallExpression(node)) {\r\n const expression = node.expression;\r\n const symbol = checker.getSymbolAtLocation(expression);\r\n\r\n if (symbol) {\r\n const name = symbol.getName();\r\n if (graph.has(fnName)) {\r\n graph.get(fnName)!.add(name);\r\n }\r\n }\r\n }\r\n\r\n ts.forEachChild(node, visit);\r\n });\r\n }\r\n\r\n return graph;\r\n}\r\n","#!/usr/bin/env node\r\nimport { createTsProgram } from \"./analyzer/createProgram\";\r\nimport { collectFunctions } from \"./analyzer/collectFunctions\";\r\nimport { buildCallGraph } from \"./analyzer/buildCallGraph\";\r\n\r\nconst command = process.argv[2];\r\n\r\nif (command !== \"analyze\") {\r\n console.log(\"Usage: callograph analyze\");\r\n process.exit(1);\r\n}\r\n\r\nconst program = createTsProgram(\"tsconfig.json\");\r\nconst functions = collectFunctions(program);\r\nconst graph = buildCallGraph(program, functions);\r\n\r\nconsole.log(\"\\nCall Graph:\\n\");\r\n\r\nfor (const [fn, calls] of graph.entries()) {\r\n for (const call of calls) {\r\n console.log(`${fn} → ${call}`);\r\n }\r\n}\r\n"],"mappings":"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","names":["ts","import_typescript","ts","program","import_typescript","program","functions","graph","ts"]}
|
|
1
|
+
{"version":3,"sources":["../src/analyzer/createProgram.ts","../src/analyzer/collectFunctions.ts","../src/analyzer/buildCallGraph.ts","../src/analyzer/purity.ts","../src/analyzer/helpers/globals.ts","../src/analyzer/helpers/ast.ts","../src/analyzer/helpers/async.ts","../src/analyzer/propagateImpurity.ts","../src/cli.ts"],"sourcesContent":["import ts from \"typescript\";\r\n\r\nexport function createTsProgram(tsconfigPath: string) {\r\n const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);\r\n\r\n if (configFile.error) {\r\n throw new Error(\r\n ts.flattenDiagnosticMessageText(configFile.error.messageText, \"\\n\")\r\n );\r\n }\r\n\r\n const parsedConfig = ts.parseJsonConfigFileContent(\r\n configFile.config,\r\n ts.sys,\r\n process.cwd()\r\n );\r\n\r\n return ts.createProgram({\r\n rootNames: parsedConfig.fileNames,\r\n options: parsedConfig.options,\r\n });\r\n}\r\n","import ts from \"typescript\";\r\nimport { FunctionNode } from \"../../libs/types\";\r\n\r\nfunction safeName(node: ts.FunctionLikeDeclaration, sf: ts.SourceFile): string {\r\n const anyNode = node as any;\r\n\r\n // Named declarations / methods: function foo() {} / class X { foo() {} }\r\n if (anyNode.name && ts.isIdentifier(anyNode.name)) return anyNode.name.text;\r\n\r\n // Methods with string/numeric names: class X { \"foo\"() {} }\r\n if (\r\n anyNode.name &&\r\n (ts.isStringLiteral(anyNode.name) || ts.isNumericLiteral(anyNode.name))\r\n ) {\r\n return String(anyNode.name.text);\r\n }\r\n\r\n // const foo = () => {} / const foo = function() {}\r\n const p = node.parent;\r\n if (p && ts.isVariableDeclaration(p) && ts.isIdentifier(p.name)) return p.name.text;\r\n\r\n // obj.foo = function() {}\r\n if (p && ts.isBinaryExpression(p) && ts.isPropertyAccessExpression(p.left)) {\r\n return p.left.name.text;\r\n }\r\n\r\n const { line, character } = sf.getLineAndCharacterOfPosition(node.getStart(sf));\r\n return `<anonymous@${line + 1}:${character + 1}>`;\r\n}\r\n\r\nexport function collectFunctions(program: ts.Program): FunctionNode[] {\r\n const out: FunctionNode[] = [];\r\n\r\n for (const sf of program.getSourceFiles()) {\r\n if (sf.isDeclarationFile) continue;\r\n if (sf.fileName.includes(\"node_modules\")) continue;\r\n\r\n ts.forEachChild(sf, function visit(n) {\r\n const isNamedFunctionDecl = ts.isFunctionDeclaration(n) && !!n.name;\r\n const isMethod = ts.isMethodDeclaration(n);\r\n\r\n const p = n.parent; //guard parent\r\n const isVarAssigned =\r\n (ts.isArrowFunction(n) || ts.isFunctionExpression(n)) &&\r\n !!p &&\r\n ts.isVariableDeclaration(p) &&\r\n ts.isIdentifier(p.name);\r\n\r\n if (isNamedFunctionDecl || isMethod || isVarAssigned) {\r\n const name = safeName(n, sf);\r\n const id = `${sf.fileName}:${n.pos}:${name}`;\r\n out.push({ id, name, file: sf.fileName, node: n });\r\n }\r\n\r\n ts.forEachChild(n, visit);\r\n });\r\n }\r\n\r\n return out;\r\n}\r\n","import ts from \"typescript\";\r\nimport { FunctionNode } from \"../../libs/types\";\r\n\r\n// callerId -> calleeIds\r\nexport type CallGraph = Map<string, Set<string>>;\r\n\r\nfunction isProjectFile(fileName: string) {\r\n return !fileName.includes(\"node_modules\") && !fileName.endsWith(\".d.ts\");\r\n}\r\n\r\nexport function buildCallGraph(\r\n program: ts.Program,\r\n functions: FunctionNode[]\r\n): CallGraph {\r\n\r\n const checker = program.getTypeChecker();\r\n const graph: CallGraph = new Map();\r\n\r\n const declToId = new Map<ts.Declaration, string>();\r\n\r\n for (const fn of functions) {\r\n graph.set(fn.id, new Set());\r\n declToId.set(fn.node, fn.id);\r\n }\r\n\r\n for (const fn of functions) {\r\n const callerId = fn.id;\r\n\r\n function visit(node: ts.Node) {\r\n if (ts.isCallExpression(node)) {\r\n const sig = checker.getResolvedSignature(node);\r\n const decl = sig?.getDeclaration();\r\n\r\n if (\r\n decl &&\r\n isProjectFile(decl.getSourceFile().fileName)\r\n ) {\r\n const calleeId = declToId.get(decl as ts.Declaration);\r\n\r\n if (calleeId) {\r\n graph.get(callerId)!.add(calleeId);\r\n }\r\n }\r\n }\r\n\r\n ts.forEachChild(node, visit);\r\n }\r\n\r\n visit(fn.node);\r\n }\r\n\r\n return graph;\r\n}\r\n","import ts from \"typescript\";\r\nimport { FunctionNode, PurityResult } from \"../../libs/types\";\r\nimport { GLOBAL_OBJECTS, MUTATING_ARRAY_METHODS } from \"./helpers/globals\";\r\nimport { isInside, rootIdentifier, isWriteOperator } from \"./helpers/ast\";\r\nimport {\r\n isPromiseLike,\r\n isAwaited,\r\n isReturned,\r\n isHandledWithThen,\r\n isAsyncForEach,\r\n isAsyncEventListener,\r\n isPromiseAll,\r\n} from \"./helpers/async\";\r\n\r\nexport function analyzePurity(\r\n program: ts.Program,\r\n functions: FunctionNode[]\r\n): PurityResult[] {\r\n const checker = program.getTypeChecker();\r\n const results: PurityResult[] = [];\r\n\r\n for (const fn of functions) {\r\n const sf = fn.node.getSourceFile();\r\n const { line, character } = sf.getLineAndCharacterOfPosition(\r\n fn.node.getStart(sf)\r\n );\r\n\r\n const reasons: string[] = [];\r\n const paramNames = new Set<string>();\r\n\r\n for (const p of fn.node.parameters) {\r\n if (ts.isIdentifier(p.name)) paramNames.add(p.name.text);\r\n }\r\n\r\n const mark = (reason: string) => {\r\n if (!reasons.includes(reason)) reasons.push(reason);\r\n };\r\n\r\n let containsAwait = false;\r\n let hasTryCatch = false;\r\n\r\n const isAsyncFunction =\r\n fn.node.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false;\r\n\r\n function visit(node: ts.Node): void {\r\n /* ---------------------------\r\n WRITE DETECTION\r\n ----------------------------*/\r\n if (ts.isBinaryExpression(node) && isWriteOperator(node.operatorToken.kind)) {\r\n const left = node.left as ts.Expression;\r\n const root = rootIdentifier(left);\r\n\r\n if (root && paramNames.has(root.text)) {\r\n mark(`Mutates parameter \\`${root.text}\\``);\r\n }\r\n\r\n if (ts.isIdentifier(left)) {\r\n const sym = checker.getSymbolAtLocation(left);\r\n const decls = sym?.getDeclarations() ?? [];\r\n const declaredInside = decls.some(d => isInside(d, fn.node));\r\n if (sym && decls.length > 0 && !declaredInside) {\r\n mark(`Writes to outer scope \\`${left.text}\\``);\r\n }\r\n }\r\n\r\n if (\r\n ts.isPropertyAccessExpression(left) &&\r\n left.expression.kind === ts.SyntaxKind.ThisKeyword\r\n ) {\r\n mark(`Mutates \\`this.${left.name.text}\\``);\r\n }\r\n }\r\n\r\n /* ---------------------------\r\n ARRAY PARAM MUTATION\r\n ----------------------------*/\r\n if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {\r\n const method = node.expression.name.text;\r\n const root = rootIdentifier(node.expression.expression);\r\n\r\n if (root && paramNames.has(root.text) && MUTATING_ARRAY_METHODS.has(method)) {\r\n mark(`Mutates parameter \\`${root.text}\\` via \\`${method}()\\``);\r\n }\r\n }\r\n\r\n /* ---------------------------\r\n GLOBAL ACCESS\r\n ----------------------------*/\r\n if (ts.isIdentifier(node)) {\r\n const sym = checker.getSymbolAtLocation(node);\r\n if (sym && GLOBAL_OBJECTS.has(sym.getName())) {\r\n mark(`Accesses global \\`${sym.getName()}\\``);\r\n }\r\n }\r\n\r\n if (ts.isPropertyAccessExpression(node)) {\r\n const root = rootIdentifier(node.expression);\r\n\r\n if (root?.text === \"process\" && node.name.text === \"env\") {\r\n mark(\"Accesses `process.env` (non-deterministic)\");\r\n }\r\n\r\n if (root?.text === \"console\") {\r\n mark(\"Writes to console\");\r\n }\r\n }\r\n\r\n /* ---------------------------\r\n CALL EXPRESSIONS\r\n ----------------------------*/\r\n if (ts.isCallExpression(node)) {\r\n const type = checker.getTypeAtLocation(node);\r\n const expr = node.expression;\r\n\r\n // Non-deterministic\r\n if (\r\n ts.isPropertyAccessExpression(expr) &&\r\n ts.isIdentifier(expr.expression) &&\r\n expr.expression.text === \"Date\" &&\r\n expr.name.text === \"now\"\r\n ) {\r\n mark(\"Calls Date.now() (non-deterministic)\");\r\n }\r\n\r\n if (\r\n ts.isPropertyAccessExpression(expr) &&\r\n ts.isIdentifier(expr.expression) &&\r\n expr.expression.text === \"Math\" &&\r\n expr.name.text === \"random\"\r\n ) {\r\n mark(\"Calls Math.random() (non-deterministic)\");\r\n }\r\n\r\n // Async rules\r\n if (isPromiseLike(type, checker)) {\r\n if (!isAwaited(node) && !isReturned(node) && !isHandledWithThen(node)) {\r\n mark(\"Floating Promise (result ignored)\");\r\n }\r\n }\r\n\r\n if (isPromiseAll(node) && !isAwaited(node) && !isReturned(node)) {\r\n mark(\"Unawaited Promise.all()\");\r\n }\r\n\r\n if (isAsyncForEach(node)) {\r\n mark(\"Async callback inside forEach (not awaited)\");\r\n }\r\n\r\n if (isAsyncEventListener(node)) {\r\n mark(\"Async event listener without error boundary\");\r\n }\r\n }\r\n\r\n /* ---------------------------\r\n NEW EXPRESSIONS\r\n ----------------------------*/\r\n if (ts.isNewExpression(node)) {\r\n if (ts.isIdentifier(node.expression) && node.expression.text === \"Date\") {\r\n mark(\"Instantiates Date (non-deterministic)\");\r\n }\r\n }\r\n\r\n /* ---------------------------\r\n AWAIT + ERROR HANDLING\r\n ----------------------------*/\r\n if (ts.isAwaitExpression(node)) {\r\n containsAwait = true;\r\n }\r\n\r\n if (ts.isTryStatement(node) && node.catchClause) {\r\n hasTryCatch = true;\r\n }\r\n\r\n if (ts.isCatchClause(node) && node.block.statements.length === 0) {\r\n mark(\"Empty catch block (error swallowed)\");\r\n }\r\n\r\n ts.forEachChild(node, visit);\r\n }\r\n\r\n visit(fn.node);\r\n\r\n if (isAsyncFunction && !containsAwait) {\r\n mark(\"Async function without await\");\r\n }\r\n\r\n if (isAsyncFunction && containsAwait && !hasTryCatch) {\r\n mark(\"Async function without error handling (no try/catch)\");\r\n }\r\n\r\n results.push({\r\n id: fn.id,\r\n name: fn.name,\r\n file: fn.file,\r\n line: line + 1,\r\n character: character + 1,\r\n isPure: reasons.length === 0,\r\n reasons,\r\n });\r\n }\r\n\r\n return results;\r\n}\r\n","export const GLOBAL_OBJECTS = new Set([\r\n \"window\",\r\n \"document\",\r\n \"global\",\r\n \"process\",\r\n \"localStorage\",\r\n \"sessionStorage\",\r\n]);\r\n\r\nexport const MUTATING_ARRAY_METHODS = new Set([\r\n \"push\",\r\n \"pop\",\r\n \"splice\",\r\n \"shift\",\r\n \"unshift\",\r\n \"sort\",\r\n \"reverse\",\r\n \"copyWithin\",\r\n \"fill\",\r\n]);\r\n","import ts from \"typescript\";\r\n\r\nexport function isInside(node: ts.Node, container: ts.Node) {\r\n return node.pos >= container.pos && node.end <= container.end;\r\n}\r\n\r\nexport function rootIdentifier(expr: ts.Expression): ts.Identifier | null {\r\n if (ts.isIdentifier(expr)) return expr;\r\n if (ts.isPropertyAccessExpression(expr)) return rootIdentifier(expr.expression);\r\n if (ts.isElementAccessExpression(expr)) return rootIdentifier(expr.expression);\r\n if (ts.isParenthesizedExpression(expr)) return rootIdentifier(expr.expression);\r\n return null;\r\n}\r\n\r\nexport function isWriteOperator(kind: ts.SyntaxKind) {\r\n return (\r\n kind === ts.SyntaxKind.EqualsToken ||\r\n kind === ts.SyntaxKind.PlusEqualsToken ||\r\n kind === ts.SyntaxKind.MinusEqualsToken ||\r\n kind === ts.SyntaxKind.AsteriskEqualsToken ||\r\n kind === ts.SyntaxKind.SlashEqualsToken ||\r\n kind === ts.SyntaxKind.PercentEqualsToken ||\r\n kind === ts.SyntaxKind.AmpersandEqualsToken ||\r\n kind === ts.SyntaxKind.BarEqualsToken ||\r\n kind === ts.SyntaxKind.CaretEqualsToken ||\r\n kind === ts.SyntaxKind.LessThanLessThanEqualsToken ||\r\n kind === ts.SyntaxKind.GreaterThanGreaterThanEqualsToken ||\r\n kind === ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken\r\n );\r\n}\r\n","import ts from \"typescript\";\r\n\r\nexport function isPromiseLike(type: ts.Type, checker: ts.TypeChecker): boolean {\r\n if (!type) return false;\r\n\r\n const thenProp = type.getProperty(\"then\");\r\n if (!thenProp) return false;\r\n\r\n const decl = thenProp.valueDeclaration ?? thenProp.declarations?.[0];\r\n if (!decl) return false;\r\n\r\n const thenType = checker.getTypeOfSymbolAtLocation(thenProp, decl);\r\n return thenType.getCallSignatures().length > 0;\r\n}\r\n\r\nexport function isAwaited(node: ts.CallExpression) {\r\n return ts.isAwaitExpression(node.parent);\r\n}\r\n\r\nexport function isReturned(node: ts.CallExpression) {\r\n return ts.isReturnStatement(node.parent);\r\n}\r\n\r\nexport function isHandledWithThen(node: ts.CallExpression) {\r\n if (!ts.isPropertyAccessExpression(node.parent)) return false;\r\n const name = node.parent.name.text;\r\n return name === \"then\" || name === \"catch\" || name === \"finally\";\r\n}\r\n\r\nexport function isAsyncForEach(node: ts.CallExpression): boolean {\r\n if (!ts.isPropertyAccessExpression(node.expression)) return false;\r\n if (node.expression.name.text !== \"forEach\") return false;\r\n\r\n const cb = node.arguments[0];\r\n if (!cb) return false;\r\n\r\n if (ts.isArrowFunction(cb) || ts.isFunctionExpression(cb)) {\r\n return cb.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false;\r\n }\r\n\r\n return false;\r\n}\r\n\r\nexport function isAsyncEventListener(node: ts.CallExpression): boolean {\r\n if (!ts.isPropertyAccessExpression(node.expression)) return false;\r\n if (node.expression.name.text !== \"addEventListener\") return false;\r\n\r\n const cb = node.arguments[1];\r\n if (!cb) return false;\r\n\r\n if (ts.isArrowFunction(cb) || ts.isFunctionExpression(cb)) {\r\n return cb.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false;\r\n }\r\n\r\n return false;\r\n}\r\n\r\nexport function isPromiseAll(node: ts.CallExpression): boolean {\r\n return (\r\n ts.isPropertyAccessExpression(node.expression) &&\r\n ts.isIdentifier(node.expression.expression) &&\r\n node.expression.expression.text === \"Promise\" &&\r\n node.expression.name.text === \"all\"\r\n );\r\n}\r\n","import { CallGraph } from \"./buildCallGraph\";\r\nimport { PurityResult } from \"../../libs/types\";\r\n\r\nexport function propagateImpurity(\r\n graph: CallGraph,\r\n purity: PurityResult[]\r\n): PurityResult[] {\r\n\r\n // Map functionId -> PurityResult\r\n const resultMap = new Map<string, PurityResult>();\r\n for (const r of purity) {\r\n resultMap.set(r.id, r);\r\n }\r\n\r\n // Build reverse graph: calleeId -> Set<callerId>\r\n const reverse = new Map<string, Set<string>>();\r\n\r\n for (const [callerId, calleeIds] of graph.entries()) {\r\n for (const calleeId of calleeIds) {\r\n if (!reverse.has(calleeId)) {\r\n reverse.set(calleeId, new Set());\r\n }\r\n reverse.get(calleeId)!.add(callerId);\r\n }\r\n }\r\n\r\n // BFS propagation\r\n const queue: string[] = [];\r\n\r\n // Seed with initially impure functions\r\n for (const r of purity) {\r\n if (!r.isPure) {\r\n queue.push(r.id);\r\n }\r\n }\r\n\r\n while (queue.length > 0) {\r\n const impureId = queue.shift()!;\r\n const callers = reverse.get(impureId);\r\n if (!callers) continue;\r\n\r\n for (const callerId of callers) {\r\n const caller = resultMap.get(callerId);\r\n if (!caller) continue;\r\n\r\n if (caller.isPure) {\r\n caller.isPure = false;\r\n caller.reasons.push(`Calls impure function`);\r\n queue.push(callerId);\r\n }\r\n }\r\n }\r\n\r\n return Array.from(resultMap.values());\r\n}\r\n","import { createTsProgram } from \"./analyzer/createProgram\";\r\nimport { collectFunctions } from \"./analyzer/collectFunctions\";\r\nimport { buildCallGraph } from \"./analyzer/buildCallGraph\";\r\nimport { analyzePurity } from \"./analyzer/purity\";\r\nimport { propagateImpurity } from \"./analyzer/propagateImpurity\";\r\n\r\nconst command = process.argv[2];\r\n\r\nif (command !== \"analyze\") {\r\n console.log(\"Usage: callograph analyze\");\r\n process.exit(1);\r\n}\r\n\r\nconst program = createTsProgram(\"tsconfig.json\");\r\nconst functions = collectFunctions(program);\r\nconst graph = buildCallGraph(program, functions);\r\n\r\nlet purity = analyzePurity(program, functions);\r\npurity = propagateImpurity(graph, purity);\r\n\r\n/* ------------------------------\r\n CALL GRAPH\r\n--------------------------------*/\r\nconsole.log(\"\\nCall Graph:\\n\");\r\n\r\nlet edgeCount = 0;\r\n\r\nfor (const [callerId, callees] of graph.entries()) {\r\n const caller = functions.find(f => f.id === callerId);\r\n const callerName = caller ? caller.name : callerId;\r\n\r\n for (const calleeId of callees) {\r\n const callee = functions.find(f => f.id === calleeId);\r\n const calleeName = callee ? callee.name : calleeId;\r\n\r\n console.log(`${callerName} → ${calleeName}`);\r\n edgeCount++;\r\n }\r\n}\r\n\r\nif (edgeCount === 0) {\r\n console.log(\"(no project-level call edges found)\");\r\n}\r\n\r\nconsole.log(`\\nTotal call edges: ${edgeCount}`);\r\n\r\n/* ------------------------------\r\n PURITY REPORT\r\n--------------------------------*/\r\nconsole.log(\"\\nPurity Report:\\n\");\r\n\r\nconst impure = purity.filter(r => !r.isPure);\r\n\r\nif (impure.length === 0) {\r\n console.log(\"All analyzed functions are pure.\");\r\n} else {\r\n for (const r of impure) {\r\n console.log(`${r.file}:${r.line}:${r.character}`);\r\n console.log(`Function: ${r.name}`);\r\n console.log(`Status: X Impure`);\r\n console.log(\"Reasons:\");\r\n for (const reason of r.reasons) {\r\n console.log(` - ${reason}`);\r\n }\r\n console.log(\"\");\r\n }\r\n}\r\n"],"mappings":"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","names":["ts","import_typescript","ts","program","import_typescript","program","functions","graph","visit","ts","import_typescript","import_typescript","ts","import_typescript","ts","program","functions","visit","ts","graph","purity"]}
|
package/dist/cli.d.cts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
|
+
export { }
|
package/dist/cli.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
|
+
export { }
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
1
|
// src/analyzer/createProgram.ts
|
|
4
2
|
import ts from "typescript";
|
|
5
3
|
function createTsProgram(tsconfigPath) {
|
|
@@ -42,7 +40,11 @@ function collectFunctions(program2) {
|
|
|
42
40
|
if (sf.isDeclarationFile) continue;
|
|
43
41
|
if (sf.fileName.includes("node_modules")) continue;
|
|
44
42
|
ts2.forEachChild(sf, function visit(n) {
|
|
45
|
-
|
|
43
|
+
const isNamedFunctionDecl = ts2.isFunctionDeclaration(n) && !!n.name;
|
|
44
|
+
const isMethod = ts2.isMethodDeclaration(n);
|
|
45
|
+
const p = n.parent;
|
|
46
|
+
const isVarAssigned = (ts2.isArrowFunction(n) || ts2.isFunctionExpression(n)) && !!p && ts2.isVariableDeclaration(p) && ts2.isIdentifier(p.name);
|
|
47
|
+
if (isNamedFunctionDecl || isMethod || isVarAssigned) {
|
|
46
48
|
const name = safeName(n, sf);
|
|
47
49
|
const id = `${sf.fileName}:${n.pos}:${name}`;
|
|
48
50
|
out.push({ id, name, file: sf.fileName, node: n });
|
|
@@ -55,33 +57,284 @@ function collectFunctions(program2) {
|
|
|
55
57
|
|
|
56
58
|
// src/analyzer/buildCallGraph.ts
|
|
57
59
|
import ts3 from "typescript";
|
|
60
|
+
function isProjectFile(fileName) {
|
|
61
|
+
return !fileName.includes("node_modules") && !fileName.endsWith(".d.ts");
|
|
62
|
+
}
|
|
58
63
|
function buildCallGraph(program2, functions2) {
|
|
59
64
|
const checker = program2.getTypeChecker();
|
|
60
65
|
const graph2 = /* @__PURE__ */ new Map();
|
|
61
|
-
const
|
|
66
|
+
const declToId = /* @__PURE__ */ new Map();
|
|
62
67
|
for (const fn of functions2) {
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
graph2.set(fn.id, /* @__PURE__ */ new Set());
|
|
69
|
+
declToId.set(fn.node, fn.id);
|
|
65
70
|
}
|
|
66
71
|
for (const fn of functions2) {
|
|
67
|
-
|
|
68
|
-
ts3.forEachChild(fn.node, function visit(node) {
|
|
72
|
+
let visit2 = function(node) {
|
|
69
73
|
if (ts3.isCallExpression(node)) {
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
if (
|
|
73
|
-
const
|
|
74
|
-
if (
|
|
75
|
-
graph2.get(
|
|
74
|
+
const sig = checker.getResolvedSignature(node);
|
|
75
|
+
const decl = sig?.getDeclaration();
|
|
76
|
+
if (decl && isProjectFile(decl.getSourceFile().fileName)) {
|
|
77
|
+
const calleeId = declToId.get(decl);
|
|
78
|
+
if (calleeId) {
|
|
79
|
+
graph2.get(callerId).add(calleeId);
|
|
76
80
|
}
|
|
77
81
|
}
|
|
78
82
|
}
|
|
79
|
-
ts3.forEachChild(node,
|
|
80
|
-
}
|
|
83
|
+
ts3.forEachChild(node, visit2);
|
|
84
|
+
};
|
|
85
|
+
var visit = visit2;
|
|
86
|
+
const callerId = fn.id;
|
|
87
|
+
visit2(fn.node);
|
|
81
88
|
}
|
|
82
89
|
return graph2;
|
|
83
90
|
}
|
|
84
91
|
|
|
92
|
+
// src/analyzer/purity.ts
|
|
93
|
+
import ts6 from "typescript";
|
|
94
|
+
|
|
95
|
+
// src/analyzer/helpers/globals.ts
|
|
96
|
+
var GLOBAL_OBJECTS = /* @__PURE__ */ new Set([
|
|
97
|
+
"window",
|
|
98
|
+
"document",
|
|
99
|
+
"global",
|
|
100
|
+
"process",
|
|
101
|
+
"localStorage",
|
|
102
|
+
"sessionStorage"
|
|
103
|
+
]);
|
|
104
|
+
var MUTATING_ARRAY_METHODS = /* @__PURE__ */ new Set([
|
|
105
|
+
"push",
|
|
106
|
+
"pop",
|
|
107
|
+
"splice",
|
|
108
|
+
"shift",
|
|
109
|
+
"unshift",
|
|
110
|
+
"sort",
|
|
111
|
+
"reverse",
|
|
112
|
+
"copyWithin",
|
|
113
|
+
"fill"
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
// src/analyzer/helpers/ast.ts
|
|
117
|
+
import ts4 from "typescript";
|
|
118
|
+
function isInside(node, container) {
|
|
119
|
+
return node.pos >= container.pos && node.end <= container.end;
|
|
120
|
+
}
|
|
121
|
+
function rootIdentifier(expr) {
|
|
122
|
+
if (ts4.isIdentifier(expr)) return expr;
|
|
123
|
+
if (ts4.isPropertyAccessExpression(expr)) return rootIdentifier(expr.expression);
|
|
124
|
+
if (ts4.isElementAccessExpression(expr)) return rootIdentifier(expr.expression);
|
|
125
|
+
if (ts4.isParenthesizedExpression(expr)) return rootIdentifier(expr.expression);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
function isWriteOperator(kind) {
|
|
129
|
+
return kind === ts4.SyntaxKind.EqualsToken || kind === ts4.SyntaxKind.PlusEqualsToken || kind === ts4.SyntaxKind.MinusEqualsToken || kind === ts4.SyntaxKind.AsteriskEqualsToken || kind === ts4.SyntaxKind.SlashEqualsToken || kind === ts4.SyntaxKind.PercentEqualsToken || kind === ts4.SyntaxKind.AmpersandEqualsToken || kind === ts4.SyntaxKind.BarEqualsToken || kind === ts4.SyntaxKind.CaretEqualsToken || kind === ts4.SyntaxKind.LessThanLessThanEqualsToken || kind === ts4.SyntaxKind.GreaterThanGreaterThanEqualsToken || kind === ts4.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/analyzer/helpers/async.ts
|
|
133
|
+
import ts5 from "typescript";
|
|
134
|
+
function isPromiseLike(type, checker) {
|
|
135
|
+
if (!type) return false;
|
|
136
|
+
const thenProp = type.getProperty("then");
|
|
137
|
+
if (!thenProp) return false;
|
|
138
|
+
const decl = thenProp.valueDeclaration ?? thenProp.declarations?.[0];
|
|
139
|
+
if (!decl) return false;
|
|
140
|
+
const thenType = checker.getTypeOfSymbolAtLocation(thenProp, decl);
|
|
141
|
+
return thenType.getCallSignatures().length > 0;
|
|
142
|
+
}
|
|
143
|
+
function isAwaited(node) {
|
|
144
|
+
return ts5.isAwaitExpression(node.parent);
|
|
145
|
+
}
|
|
146
|
+
function isReturned(node) {
|
|
147
|
+
return ts5.isReturnStatement(node.parent);
|
|
148
|
+
}
|
|
149
|
+
function isHandledWithThen(node) {
|
|
150
|
+
if (!ts5.isPropertyAccessExpression(node.parent)) return false;
|
|
151
|
+
const name = node.parent.name.text;
|
|
152
|
+
return name === "then" || name === "catch" || name === "finally";
|
|
153
|
+
}
|
|
154
|
+
function isAsyncForEach(node) {
|
|
155
|
+
if (!ts5.isPropertyAccessExpression(node.expression)) return false;
|
|
156
|
+
if (node.expression.name.text !== "forEach") return false;
|
|
157
|
+
const cb = node.arguments[0];
|
|
158
|
+
if (!cb) return false;
|
|
159
|
+
if (ts5.isArrowFunction(cb) || ts5.isFunctionExpression(cb)) {
|
|
160
|
+
return cb.modifiers?.some((m) => m.kind === ts5.SyntaxKind.AsyncKeyword) ?? false;
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
function isAsyncEventListener(node) {
|
|
165
|
+
if (!ts5.isPropertyAccessExpression(node.expression)) return false;
|
|
166
|
+
if (node.expression.name.text !== "addEventListener") return false;
|
|
167
|
+
const cb = node.arguments[1];
|
|
168
|
+
if (!cb) return false;
|
|
169
|
+
if (ts5.isArrowFunction(cb) || ts5.isFunctionExpression(cb)) {
|
|
170
|
+
return cb.modifiers?.some((m) => m.kind === ts5.SyntaxKind.AsyncKeyword) ?? false;
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
function isPromiseAll(node) {
|
|
175
|
+
return ts5.isPropertyAccessExpression(node.expression) && ts5.isIdentifier(node.expression.expression) && node.expression.expression.text === "Promise" && node.expression.name.text === "all";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/analyzer/purity.ts
|
|
179
|
+
function analyzePurity(program2, functions2) {
|
|
180
|
+
const checker = program2.getTypeChecker();
|
|
181
|
+
const results = [];
|
|
182
|
+
for (const fn of functions2) {
|
|
183
|
+
let visit2 = function(node) {
|
|
184
|
+
if (ts6.isBinaryExpression(node) && isWriteOperator(node.operatorToken.kind)) {
|
|
185
|
+
const left = node.left;
|
|
186
|
+
const root = rootIdentifier(left);
|
|
187
|
+
if (root && paramNames.has(root.text)) {
|
|
188
|
+
mark(`Mutates parameter \`${root.text}\``);
|
|
189
|
+
}
|
|
190
|
+
if (ts6.isIdentifier(left)) {
|
|
191
|
+
const sym = checker.getSymbolAtLocation(left);
|
|
192
|
+
const decls = sym?.getDeclarations() ?? [];
|
|
193
|
+
const declaredInside = decls.some((d) => isInside(d, fn.node));
|
|
194
|
+
if (sym && decls.length > 0 && !declaredInside) {
|
|
195
|
+
mark(`Writes to outer scope \`${left.text}\``);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (ts6.isPropertyAccessExpression(left) && left.expression.kind === ts6.SyntaxKind.ThisKeyword) {
|
|
199
|
+
mark(`Mutates \`this.${left.name.text}\``);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (ts6.isCallExpression(node) && ts6.isPropertyAccessExpression(node.expression)) {
|
|
203
|
+
const method = node.expression.name.text;
|
|
204
|
+
const root = rootIdentifier(node.expression.expression);
|
|
205
|
+
if (root && paramNames.has(root.text) && MUTATING_ARRAY_METHODS.has(method)) {
|
|
206
|
+
mark(`Mutates parameter \`${root.text}\` via \`${method}()\``);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (ts6.isIdentifier(node)) {
|
|
210
|
+
const sym = checker.getSymbolAtLocation(node);
|
|
211
|
+
if (sym && GLOBAL_OBJECTS.has(sym.getName())) {
|
|
212
|
+
mark(`Accesses global \`${sym.getName()}\``);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (ts6.isPropertyAccessExpression(node)) {
|
|
216
|
+
const root = rootIdentifier(node.expression);
|
|
217
|
+
if (root?.text === "process" && node.name.text === "env") {
|
|
218
|
+
mark("Accesses `process.env` (non-deterministic)");
|
|
219
|
+
}
|
|
220
|
+
if (root?.text === "console") {
|
|
221
|
+
mark("Writes to console");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (ts6.isCallExpression(node)) {
|
|
225
|
+
const type = checker.getTypeAtLocation(node);
|
|
226
|
+
const expr = node.expression;
|
|
227
|
+
if (ts6.isPropertyAccessExpression(expr) && ts6.isIdentifier(expr.expression) && expr.expression.text === "Date" && expr.name.text === "now") {
|
|
228
|
+
mark("Calls Date.now() (non-deterministic)");
|
|
229
|
+
}
|
|
230
|
+
if (ts6.isPropertyAccessExpression(expr) && ts6.isIdentifier(expr.expression) && expr.expression.text === "Math" && expr.name.text === "random") {
|
|
231
|
+
mark("Calls Math.random() (non-deterministic)");
|
|
232
|
+
}
|
|
233
|
+
if (isPromiseLike(type, checker)) {
|
|
234
|
+
if (!isAwaited(node) && !isReturned(node) && !isHandledWithThen(node)) {
|
|
235
|
+
mark("Floating Promise (result ignored)");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (isPromiseAll(node) && !isAwaited(node) && !isReturned(node)) {
|
|
239
|
+
mark("Unawaited Promise.all()");
|
|
240
|
+
}
|
|
241
|
+
if (isAsyncForEach(node)) {
|
|
242
|
+
mark("Async callback inside forEach (not awaited)");
|
|
243
|
+
}
|
|
244
|
+
if (isAsyncEventListener(node)) {
|
|
245
|
+
mark("Async event listener without error boundary");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (ts6.isNewExpression(node)) {
|
|
249
|
+
if (ts6.isIdentifier(node.expression) && node.expression.text === "Date") {
|
|
250
|
+
mark("Instantiates Date (non-deterministic)");
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (ts6.isAwaitExpression(node)) {
|
|
254
|
+
containsAwait = true;
|
|
255
|
+
}
|
|
256
|
+
if (ts6.isTryStatement(node) && node.catchClause) {
|
|
257
|
+
hasTryCatch = true;
|
|
258
|
+
}
|
|
259
|
+
if (ts6.isCatchClause(node) && node.block.statements.length === 0) {
|
|
260
|
+
mark("Empty catch block (error swallowed)");
|
|
261
|
+
}
|
|
262
|
+
ts6.forEachChild(node, visit2);
|
|
263
|
+
};
|
|
264
|
+
var visit = visit2;
|
|
265
|
+
const sf = fn.node.getSourceFile();
|
|
266
|
+
const { line, character } = sf.getLineAndCharacterOfPosition(
|
|
267
|
+
fn.node.getStart(sf)
|
|
268
|
+
);
|
|
269
|
+
const reasons = [];
|
|
270
|
+
const paramNames = /* @__PURE__ */ new Set();
|
|
271
|
+
for (const p of fn.node.parameters) {
|
|
272
|
+
if (ts6.isIdentifier(p.name)) paramNames.add(p.name.text);
|
|
273
|
+
}
|
|
274
|
+
const mark = (reason) => {
|
|
275
|
+
if (!reasons.includes(reason)) reasons.push(reason);
|
|
276
|
+
};
|
|
277
|
+
let containsAwait = false;
|
|
278
|
+
let hasTryCatch = false;
|
|
279
|
+
const isAsyncFunction = fn.node.modifiers?.some((m) => m.kind === ts6.SyntaxKind.AsyncKeyword) ?? false;
|
|
280
|
+
visit2(fn.node);
|
|
281
|
+
if (isAsyncFunction && !containsAwait) {
|
|
282
|
+
mark("Async function without await");
|
|
283
|
+
}
|
|
284
|
+
if (isAsyncFunction && containsAwait && !hasTryCatch) {
|
|
285
|
+
mark("Async function without error handling (no try/catch)");
|
|
286
|
+
}
|
|
287
|
+
results.push({
|
|
288
|
+
id: fn.id,
|
|
289
|
+
name: fn.name,
|
|
290
|
+
file: fn.file,
|
|
291
|
+
line: line + 1,
|
|
292
|
+
character: character + 1,
|
|
293
|
+
isPure: reasons.length === 0,
|
|
294
|
+
reasons
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
return results;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/analyzer/propagateImpurity.ts
|
|
301
|
+
function propagateImpurity(graph2, purity2) {
|
|
302
|
+
const resultMap = /* @__PURE__ */ new Map();
|
|
303
|
+
for (const r of purity2) {
|
|
304
|
+
resultMap.set(r.id, r);
|
|
305
|
+
}
|
|
306
|
+
const reverse = /* @__PURE__ */ new Map();
|
|
307
|
+
for (const [callerId, calleeIds] of graph2.entries()) {
|
|
308
|
+
for (const calleeId of calleeIds) {
|
|
309
|
+
if (!reverse.has(calleeId)) {
|
|
310
|
+
reverse.set(calleeId, /* @__PURE__ */ new Set());
|
|
311
|
+
}
|
|
312
|
+
reverse.get(calleeId).add(callerId);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const queue = [];
|
|
316
|
+
for (const r of purity2) {
|
|
317
|
+
if (!r.isPure) {
|
|
318
|
+
queue.push(r.id);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
while (queue.length > 0) {
|
|
322
|
+
const impureId = queue.shift();
|
|
323
|
+
const callers = reverse.get(impureId);
|
|
324
|
+
if (!callers) continue;
|
|
325
|
+
for (const callerId of callers) {
|
|
326
|
+
const caller = resultMap.get(callerId);
|
|
327
|
+
if (!caller) continue;
|
|
328
|
+
if (caller.isPure) {
|
|
329
|
+
caller.isPure = false;
|
|
330
|
+
caller.reasons.push(`Calls impure function`);
|
|
331
|
+
queue.push(callerId);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return Array.from(resultMap.values());
|
|
336
|
+
}
|
|
337
|
+
|
|
85
338
|
// src/cli.ts
|
|
86
339
|
var command = process.argv[2];
|
|
87
340
|
if (command !== "analyze") {
|
|
@@ -91,10 +344,39 @@ if (command !== "analyze") {
|
|
|
91
344
|
var program = createTsProgram("tsconfig.json");
|
|
92
345
|
var functions = collectFunctions(program);
|
|
93
346
|
var graph = buildCallGraph(program, functions);
|
|
347
|
+
var purity = analyzePurity(program, functions);
|
|
348
|
+
purity = propagateImpurity(graph, purity);
|
|
94
349
|
console.log("\nCall Graph:\n");
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
350
|
+
var edgeCount = 0;
|
|
351
|
+
for (const [callerId, callees] of graph.entries()) {
|
|
352
|
+
const caller = functions.find((f) => f.id === callerId);
|
|
353
|
+
const callerName = caller ? caller.name : callerId;
|
|
354
|
+
for (const calleeId of callees) {
|
|
355
|
+
const callee = functions.find((f) => f.id === calleeId);
|
|
356
|
+
const calleeName = callee ? callee.name : calleeId;
|
|
357
|
+
console.log(`${callerName} \u2192 ${calleeName}`);
|
|
358
|
+
edgeCount++;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (edgeCount === 0) {
|
|
362
|
+
console.log("(no project-level call edges found)");
|
|
363
|
+
}
|
|
364
|
+
console.log(`
|
|
365
|
+
Total call edges: ${edgeCount}`);
|
|
366
|
+
console.log("\nPurity Report:\n");
|
|
367
|
+
var impure = purity.filter((r) => !r.isPure);
|
|
368
|
+
if (impure.length === 0) {
|
|
369
|
+
console.log("All analyzed functions are pure.");
|
|
370
|
+
} else {
|
|
371
|
+
for (const r of impure) {
|
|
372
|
+
console.log(`${r.file}:${r.line}:${r.character}`);
|
|
373
|
+
console.log(`Function: ${r.name}`);
|
|
374
|
+
console.log(`Status: X Impure`);
|
|
375
|
+
console.log("Reasons:");
|
|
376
|
+
for (const reason of r.reasons) {
|
|
377
|
+
console.log(` - ${reason}`);
|
|
378
|
+
}
|
|
379
|
+
console.log("");
|
|
98
380
|
}
|
|
99
381
|
}
|
|
100
382
|
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/analyzer/createProgram.ts","../src/analyzer/collectFunctions.ts","../src/analyzer/buildCallGraph.ts","../src/cli.ts"],"sourcesContent":["import ts from \"typescript\";\r\n\r\nexport function createTsProgram(tsconfigPath: string) {\r\n const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);\r\n\r\n if (configFile.error) {\r\n throw new Error(\r\n ts.flattenDiagnosticMessageText(configFile.error.messageText, \"\\n\")\r\n );\r\n }\r\n\r\n const parsedConfig = ts.parseJsonConfigFileContent(\r\n configFile.config,\r\n ts.sys,\r\n process.cwd()\r\n );\r\n\r\n return ts.createProgram({\r\n rootNames: parsedConfig.fileNames,\r\n options: parsedConfig.options,\r\n });\r\n}\r\n","import ts from \"typescript\";\r\n\r\nexport interface FunctionNode {\r\n id: string;\r\n name: string;\r\n file: string;\r\n node: ts.FunctionLikeDeclaration;\r\n}\r\n\r\nfunction safeName(node: ts.FunctionLikeDeclaration, sf: ts.SourceFile): string {\r\n const anyNode = node as any;\r\n\r\n // Named declarations / methods: function foo() {} / class X { foo() {} }\r\n if (anyNode.name && ts.isIdentifier(anyNode.name)) return anyNode.name.text;\r\n\r\n // Methods with string/numeric names: class X { \"foo\"() {} }\r\n if (anyNode.name && (ts.isStringLiteral(anyNode.name) || ts.isNumericLiteral(anyNode.name))) {\r\n return String(anyNode.name.text);\r\n }\r\n\r\n // const foo = () => {} / const foo = function() {}\r\n const p = node.parent;\r\n if (p && ts.isVariableDeclaration(p) && ts.isIdentifier(p.name)) return p.name.text;\r\n\r\n // obj.foo = function() {}\r\n if (p && ts.isBinaryExpression(p) && ts.isPropertyAccessExpression(p.left)) {\r\n return p.left.name.text;\r\n }\r\n\r\n const { line, character } = sf.getLineAndCharacterOfPosition(node.getStart(sf));\r\n return `<anonymous@${line + 1}:${character + 1}>`;\r\n}\r\n\r\nexport function collectFunctions(program: ts.Program): FunctionNode[] {\r\n const out: FunctionNode[] = [];\r\n\r\n for (const sf of program.getSourceFiles()) {\r\n if (sf.isDeclarationFile) continue;\r\n if (sf.fileName.includes(\"node_modules\")) continue;\r\n\r\n ts.forEachChild(sf, function visit(n) {\r\n if (\r\n ts.isFunctionDeclaration(n) ||\r\n ts.isMethodDeclaration(n) ||\r\n ts.isArrowFunction(n) ||\r\n ts.isFunctionExpression(n)\r\n ) {\r\n const name = safeName(n, sf);\r\n const id = `${sf.fileName}:${n.pos}:${name}`;\r\n out.push({ id, name, file: sf.fileName, node: n });\r\n }\r\n ts.forEachChild(n, visit);\r\n });\r\n }\r\n\r\n return out;\r\n}\r\n","import ts from \"typescript\";\r\nimport { FunctionNode } from \"./collectFunctions\";\r\n\r\nexport type CallGraph = Map<string, Set<string>>;\r\n\r\nexport function buildCallGraph(\r\n program: ts.Program,\r\n functions: FunctionNode[]\r\n): CallGraph {\r\n const checker = program.getTypeChecker();\r\n const graph: CallGraph = new Map();\r\n\r\n const functionMap = new Map<string, ts.FunctionLikeDeclaration>();\r\n\r\n for (const fn of functions) {\r\n functionMap.set(fn.name, fn.node);\r\n graph.set(fn.name, new Set());\r\n }\r\n\r\n for (const fn of functions) {\r\n const fnName = fn.name;\r\n\r\n ts.forEachChild(fn.node, function visit(node) {\r\n if (ts.isCallExpression(node)) {\r\n const expression = node.expression;\r\n const symbol = checker.getSymbolAtLocation(expression);\r\n\r\n if (symbol) {\r\n const name = symbol.getName();\r\n if (graph.has(fnName)) {\r\n graph.get(fnName)!.add(name);\r\n }\r\n }\r\n }\r\n\r\n ts.forEachChild(node, visit);\r\n });\r\n }\r\n\r\n return graph;\r\n}\r\n","#!/usr/bin/env node\r\nimport { createTsProgram } from \"./analyzer/createProgram\";\r\nimport { collectFunctions } from \"./analyzer/collectFunctions\";\r\nimport { buildCallGraph } from \"./analyzer/buildCallGraph\";\r\n\r\nconst command = process.argv[2];\r\n\r\nif (command !== \"analyze\") {\r\n console.log(\"Usage: callograph analyze\");\r\n process.exit(1);\r\n}\r\n\r\nconst program = createTsProgram(\"tsconfig.json\");\r\nconst functions = collectFunctions(program);\r\nconst graph = buildCallGraph(program, functions);\r\n\r\nconsole.log(\"\\nCall Graph:\\n\");\r\n\r\nfor (const [fn, calls] of graph.entries()) {\r\n for (const call of calls) {\r\n console.log(`${fn} → ${call}`);\r\n }\r\n}\r\n"],"mappings":"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","names":["ts","program","ts","program","functions","graph"]}
|
|
1
|
+
{"version":3,"sources":["../src/analyzer/createProgram.ts","../src/analyzer/collectFunctions.ts","../src/analyzer/buildCallGraph.ts","../src/analyzer/purity.ts","../src/analyzer/helpers/globals.ts","../src/analyzer/helpers/ast.ts","../src/analyzer/helpers/async.ts","../src/analyzer/propagateImpurity.ts","../src/cli.ts"],"sourcesContent":["import ts from \"typescript\";\r\n\r\nexport function createTsProgram(tsconfigPath: string) {\r\n const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);\r\n\r\n if (configFile.error) {\r\n throw new Error(\r\n ts.flattenDiagnosticMessageText(configFile.error.messageText, \"\\n\")\r\n );\r\n }\r\n\r\n const parsedConfig = ts.parseJsonConfigFileContent(\r\n configFile.config,\r\n ts.sys,\r\n process.cwd()\r\n );\r\n\r\n return ts.createProgram({\r\n rootNames: parsedConfig.fileNames,\r\n options: parsedConfig.options,\r\n });\r\n}\r\n","import ts from \"typescript\";\r\nimport { FunctionNode } from \"../../libs/types\";\r\n\r\nfunction safeName(node: ts.FunctionLikeDeclaration, sf: ts.SourceFile): string {\r\n const anyNode = node as any;\r\n\r\n // Named declarations / methods: function foo() {} / class X { foo() {} }\r\n if (anyNode.name && ts.isIdentifier(anyNode.name)) return anyNode.name.text;\r\n\r\n // Methods with string/numeric names: class X { \"foo\"() {} }\r\n if (\r\n anyNode.name &&\r\n (ts.isStringLiteral(anyNode.name) || ts.isNumericLiteral(anyNode.name))\r\n ) {\r\n return String(anyNode.name.text);\r\n }\r\n\r\n // const foo = () => {} / const foo = function() {}\r\n const p = node.parent;\r\n if (p && ts.isVariableDeclaration(p) && ts.isIdentifier(p.name)) return p.name.text;\r\n\r\n // obj.foo = function() {}\r\n if (p && ts.isBinaryExpression(p) && ts.isPropertyAccessExpression(p.left)) {\r\n return p.left.name.text;\r\n }\r\n\r\n const { line, character } = sf.getLineAndCharacterOfPosition(node.getStart(sf));\r\n return `<anonymous@${line + 1}:${character + 1}>`;\r\n}\r\n\r\nexport function collectFunctions(program: ts.Program): FunctionNode[] {\r\n const out: FunctionNode[] = [];\r\n\r\n for (const sf of program.getSourceFiles()) {\r\n if (sf.isDeclarationFile) continue;\r\n if (sf.fileName.includes(\"node_modules\")) continue;\r\n\r\n ts.forEachChild(sf, function visit(n) {\r\n const isNamedFunctionDecl = ts.isFunctionDeclaration(n) && !!n.name;\r\n const isMethod = ts.isMethodDeclaration(n);\r\n\r\n const p = n.parent; //guard parent\r\n const isVarAssigned =\r\n (ts.isArrowFunction(n) || ts.isFunctionExpression(n)) &&\r\n !!p &&\r\n ts.isVariableDeclaration(p) &&\r\n ts.isIdentifier(p.name);\r\n\r\n if (isNamedFunctionDecl || isMethod || isVarAssigned) {\r\n const name = safeName(n, sf);\r\n const id = `${sf.fileName}:${n.pos}:${name}`;\r\n out.push({ id, name, file: sf.fileName, node: n });\r\n }\r\n\r\n ts.forEachChild(n, visit);\r\n });\r\n }\r\n\r\n return out;\r\n}\r\n","import ts from \"typescript\";\r\nimport { FunctionNode } from \"../../libs/types\";\r\n\r\n// callerId -> calleeIds\r\nexport type CallGraph = Map<string, Set<string>>;\r\n\r\nfunction isProjectFile(fileName: string) {\r\n return !fileName.includes(\"node_modules\") && !fileName.endsWith(\".d.ts\");\r\n}\r\n\r\nexport function buildCallGraph(\r\n program: ts.Program,\r\n functions: FunctionNode[]\r\n): CallGraph {\r\n\r\n const checker = program.getTypeChecker();\r\n const graph: CallGraph = new Map();\r\n\r\n const declToId = new Map<ts.Declaration, string>();\r\n\r\n for (const fn of functions) {\r\n graph.set(fn.id, new Set());\r\n declToId.set(fn.node, fn.id);\r\n }\r\n\r\n for (const fn of functions) {\r\n const callerId = fn.id;\r\n\r\n function visit(node: ts.Node) {\r\n if (ts.isCallExpression(node)) {\r\n const sig = checker.getResolvedSignature(node);\r\n const decl = sig?.getDeclaration();\r\n\r\n if (\r\n decl &&\r\n isProjectFile(decl.getSourceFile().fileName)\r\n ) {\r\n const calleeId = declToId.get(decl as ts.Declaration);\r\n\r\n if (calleeId) {\r\n graph.get(callerId)!.add(calleeId);\r\n }\r\n }\r\n }\r\n\r\n ts.forEachChild(node, visit);\r\n }\r\n\r\n visit(fn.node);\r\n }\r\n\r\n return graph;\r\n}\r\n","import ts from \"typescript\";\r\nimport { FunctionNode, PurityResult } from \"../../libs/types\";\r\nimport { GLOBAL_OBJECTS, MUTATING_ARRAY_METHODS } from \"./helpers/globals\";\r\nimport { isInside, rootIdentifier, isWriteOperator } from \"./helpers/ast\";\r\nimport {\r\n isPromiseLike,\r\n isAwaited,\r\n isReturned,\r\n isHandledWithThen,\r\n isAsyncForEach,\r\n isAsyncEventListener,\r\n isPromiseAll,\r\n} from \"./helpers/async\";\r\n\r\nexport function analyzePurity(\r\n program: ts.Program,\r\n functions: FunctionNode[]\r\n): PurityResult[] {\r\n const checker = program.getTypeChecker();\r\n const results: PurityResult[] = [];\r\n\r\n for (const fn of functions) {\r\n const sf = fn.node.getSourceFile();\r\n const { line, character } = sf.getLineAndCharacterOfPosition(\r\n fn.node.getStart(sf)\r\n );\r\n\r\n const reasons: string[] = [];\r\n const paramNames = new Set<string>();\r\n\r\n for (const p of fn.node.parameters) {\r\n if (ts.isIdentifier(p.name)) paramNames.add(p.name.text);\r\n }\r\n\r\n const mark = (reason: string) => {\r\n if (!reasons.includes(reason)) reasons.push(reason);\r\n };\r\n\r\n let containsAwait = false;\r\n let hasTryCatch = false;\r\n\r\n const isAsyncFunction =\r\n fn.node.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false;\r\n\r\n function visit(node: ts.Node): void {\r\n /* ---------------------------\r\n WRITE DETECTION\r\n ----------------------------*/\r\n if (ts.isBinaryExpression(node) && isWriteOperator(node.operatorToken.kind)) {\r\n const left = node.left as ts.Expression;\r\n const root = rootIdentifier(left);\r\n\r\n if (root && paramNames.has(root.text)) {\r\n mark(`Mutates parameter \\`${root.text}\\``);\r\n }\r\n\r\n if (ts.isIdentifier(left)) {\r\n const sym = checker.getSymbolAtLocation(left);\r\n const decls = sym?.getDeclarations() ?? [];\r\n const declaredInside = decls.some(d => isInside(d, fn.node));\r\n if (sym && decls.length > 0 && !declaredInside) {\r\n mark(`Writes to outer scope \\`${left.text}\\``);\r\n }\r\n }\r\n\r\n if (\r\n ts.isPropertyAccessExpression(left) &&\r\n left.expression.kind === ts.SyntaxKind.ThisKeyword\r\n ) {\r\n mark(`Mutates \\`this.${left.name.text}\\``);\r\n }\r\n }\r\n\r\n /* ---------------------------\r\n ARRAY PARAM MUTATION\r\n ----------------------------*/\r\n if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {\r\n const method = node.expression.name.text;\r\n const root = rootIdentifier(node.expression.expression);\r\n\r\n if (root && paramNames.has(root.text) && MUTATING_ARRAY_METHODS.has(method)) {\r\n mark(`Mutates parameter \\`${root.text}\\` via \\`${method}()\\``);\r\n }\r\n }\r\n\r\n /* ---------------------------\r\n GLOBAL ACCESS\r\n ----------------------------*/\r\n if (ts.isIdentifier(node)) {\r\n const sym = checker.getSymbolAtLocation(node);\r\n if (sym && GLOBAL_OBJECTS.has(sym.getName())) {\r\n mark(`Accesses global \\`${sym.getName()}\\``);\r\n }\r\n }\r\n\r\n if (ts.isPropertyAccessExpression(node)) {\r\n const root = rootIdentifier(node.expression);\r\n\r\n if (root?.text === \"process\" && node.name.text === \"env\") {\r\n mark(\"Accesses `process.env` (non-deterministic)\");\r\n }\r\n\r\n if (root?.text === \"console\") {\r\n mark(\"Writes to console\");\r\n }\r\n }\r\n\r\n /* ---------------------------\r\n CALL EXPRESSIONS\r\n ----------------------------*/\r\n if (ts.isCallExpression(node)) {\r\n const type = checker.getTypeAtLocation(node);\r\n const expr = node.expression;\r\n\r\n // Non-deterministic\r\n if (\r\n ts.isPropertyAccessExpression(expr) &&\r\n ts.isIdentifier(expr.expression) &&\r\n expr.expression.text === \"Date\" &&\r\n expr.name.text === \"now\"\r\n ) {\r\n mark(\"Calls Date.now() (non-deterministic)\");\r\n }\r\n\r\n if (\r\n ts.isPropertyAccessExpression(expr) &&\r\n ts.isIdentifier(expr.expression) &&\r\n expr.expression.text === \"Math\" &&\r\n expr.name.text === \"random\"\r\n ) {\r\n mark(\"Calls Math.random() (non-deterministic)\");\r\n }\r\n\r\n // Async rules\r\n if (isPromiseLike(type, checker)) {\r\n if (!isAwaited(node) && !isReturned(node) && !isHandledWithThen(node)) {\r\n mark(\"Floating Promise (result ignored)\");\r\n }\r\n }\r\n\r\n if (isPromiseAll(node) && !isAwaited(node) && !isReturned(node)) {\r\n mark(\"Unawaited Promise.all()\");\r\n }\r\n\r\n if (isAsyncForEach(node)) {\r\n mark(\"Async callback inside forEach (not awaited)\");\r\n }\r\n\r\n if (isAsyncEventListener(node)) {\r\n mark(\"Async event listener without error boundary\");\r\n }\r\n }\r\n\r\n /* ---------------------------\r\n NEW EXPRESSIONS\r\n ----------------------------*/\r\n if (ts.isNewExpression(node)) {\r\n if (ts.isIdentifier(node.expression) && node.expression.text === \"Date\") {\r\n mark(\"Instantiates Date (non-deterministic)\");\r\n }\r\n }\r\n\r\n /* ---------------------------\r\n AWAIT + ERROR HANDLING\r\n ----------------------------*/\r\n if (ts.isAwaitExpression(node)) {\r\n containsAwait = true;\r\n }\r\n\r\n if (ts.isTryStatement(node) && node.catchClause) {\r\n hasTryCatch = true;\r\n }\r\n\r\n if (ts.isCatchClause(node) && node.block.statements.length === 0) {\r\n mark(\"Empty catch block (error swallowed)\");\r\n }\r\n\r\n ts.forEachChild(node, visit);\r\n }\r\n\r\n visit(fn.node);\r\n\r\n if (isAsyncFunction && !containsAwait) {\r\n mark(\"Async function without await\");\r\n }\r\n\r\n if (isAsyncFunction && containsAwait && !hasTryCatch) {\r\n mark(\"Async function without error handling (no try/catch)\");\r\n }\r\n\r\n results.push({\r\n id: fn.id,\r\n name: fn.name,\r\n file: fn.file,\r\n line: line + 1,\r\n character: character + 1,\r\n isPure: reasons.length === 0,\r\n reasons,\r\n });\r\n }\r\n\r\n return results;\r\n}\r\n","export const GLOBAL_OBJECTS = new Set([\r\n \"window\",\r\n \"document\",\r\n \"global\",\r\n \"process\",\r\n \"localStorage\",\r\n \"sessionStorage\",\r\n]);\r\n\r\nexport const MUTATING_ARRAY_METHODS = new Set([\r\n \"push\",\r\n \"pop\",\r\n \"splice\",\r\n \"shift\",\r\n \"unshift\",\r\n \"sort\",\r\n \"reverse\",\r\n \"copyWithin\",\r\n \"fill\",\r\n]);\r\n","import ts from \"typescript\";\r\n\r\nexport function isInside(node: ts.Node, container: ts.Node) {\r\n return node.pos >= container.pos && node.end <= container.end;\r\n}\r\n\r\nexport function rootIdentifier(expr: ts.Expression): ts.Identifier | null {\r\n if (ts.isIdentifier(expr)) return expr;\r\n if (ts.isPropertyAccessExpression(expr)) return rootIdentifier(expr.expression);\r\n if (ts.isElementAccessExpression(expr)) return rootIdentifier(expr.expression);\r\n if (ts.isParenthesizedExpression(expr)) return rootIdentifier(expr.expression);\r\n return null;\r\n}\r\n\r\nexport function isWriteOperator(kind: ts.SyntaxKind) {\r\n return (\r\n kind === ts.SyntaxKind.EqualsToken ||\r\n kind === ts.SyntaxKind.PlusEqualsToken ||\r\n kind === ts.SyntaxKind.MinusEqualsToken ||\r\n kind === ts.SyntaxKind.AsteriskEqualsToken ||\r\n kind === ts.SyntaxKind.SlashEqualsToken ||\r\n kind === ts.SyntaxKind.PercentEqualsToken ||\r\n kind === ts.SyntaxKind.AmpersandEqualsToken ||\r\n kind === ts.SyntaxKind.BarEqualsToken ||\r\n kind === ts.SyntaxKind.CaretEqualsToken ||\r\n kind === ts.SyntaxKind.LessThanLessThanEqualsToken ||\r\n kind === ts.SyntaxKind.GreaterThanGreaterThanEqualsToken ||\r\n kind === ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken\r\n );\r\n}\r\n","import ts from \"typescript\";\r\n\r\nexport function isPromiseLike(type: ts.Type, checker: ts.TypeChecker): boolean {\r\n if (!type) return false;\r\n\r\n const thenProp = type.getProperty(\"then\");\r\n if (!thenProp) return false;\r\n\r\n const decl = thenProp.valueDeclaration ?? thenProp.declarations?.[0];\r\n if (!decl) return false;\r\n\r\n const thenType = checker.getTypeOfSymbolAtLocation(thenProp, decl);\r\n return thenType.getCallSignatures().length > 0;\r\n}\r\n\r\nexport function isAwaited(node: ts.CallExpression) {\r\n return ts.isAwaitExpression(node.parent);\r\n}\r\n\r\nexport function isReturned(node: ts.CallExpression) {\r\n return ts.isReturnStatement(node.parent);\r\n}\r\n\r\nexport function isHandledWithThen(node: ts.CallExpression) {\r\n if (!ts.isPropertyAccessExpression(node.parent)) return false;\r\n const name = node.parent.name.text;\r\n return name === \"then\" || name === \"catch\" || name === \"finally\";\r\n}\r\n\r\nexport function isAsyncForEach(node: ts.CallExpression): boolean {\r\n if (!ts.isPropertyAccessExpression(node.expression)) return false;\r\n if (node.expression.name.text !== \"forEach\") return false;\r\n\r\n const cb = node.arguments[0];\r\n if (!cb) return false;\r\n\r\n if (ts.isArrowFunction(cb) || ts.isFunctionExpression(cb)) {\r\n return cb.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false;\r\n }\r\n\r\n return false;\r\n}\r\n\r\nexport function isAsyncEventListener(node: ts.CallExpression): boolean {\r\n if (!ts.isPropertyAccessExpression(node.expression)) return false;\r\n if (node.expression.name.text !== \"addEventListener\") return false;\r\n\r\n const cb = node.arguments[1];\r\n if (!cb) return false;\r\n\r\n if (ts.isArrowFunction(cb) || ts.isFunctionExpression(cb)) {\r\n return cb.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false;\r\n }\r\n\r\n return false;\r\n}\r\n\r\nexport function isPromiseAll(node: ts.CallExpression): boolean {\r\n return (\r\n ts.isPropertyAccessExpression(node.expression) &&\r\n ts.isIdentifier(node.expression.expression) &&\r\n node.expression.expression.text === \"Promise\" &&\r\n node.expression.name.text === \"all\"\r\n );\r\n}\r\n","import { CallGraph } from \"./buildCallGraph\";\r\nimport { PurityResult } from \"../../libs/types\";\r\n\r\nexport function propagateImpurity(\r\n graph: CallGraph,\r\n purity: PurityResult[]\r\n): PurityResult[] {\r\n\r\n // Map functionId -> PurityResult\r\n const resultMap = new Map<string, PurityResult>();\r\n for (const r of purity) {\r\n resultMap.set(r.id, r);\r\n }\r\n\r\n // Build reverse graph: calleeId -> Set<callerId>\r\n const reverse = new Map<string, Set<string>>();\r\n\r\n for (const [callerId, calleeIds] of graph.entries()) {\r\n for (const calleeId of calleeIds) {\r\n if (!reverse.has(calleeId)) {\r\n reverse.set(calleeId, new Set());\r\n }\r\n reverse.get(calleeId)!.add(callerId);\r\n }\r\n }\r\n\r\n // BFS propagation\r\n const queue: string[] = [];\r\n\r\n // Seed with initially impure functions\r\n for (const r of purity) {\r\n if (!r.isPure) {\r\n queue.push(r.id);\r\n }\r\n }\r\n\r\n while (queue.length > 0) {\r\n const impureId = queue.shift()!;\r\n const callers = reverse.get(impureId);\r\n if (!callers) continue;\r\n\r\n for (const callerId of callers) {\r\n const caller = resultMap.get(callerId);\r\n if (!caller) continue;\r\n\r\n if (caller.isPure) {\r\n caller.isPure = false;\r\n caller.reasons.push(`Calls impure function`);\r\n queue.push(callerId);\r\n }\r\n }\r\n }\r\n\r\n return Array.from(resultMap.values());\r\n}\r\n","import { createTsProgram } from \"./analyzer/createProgram\";\r\nimport { collectFunctions } from \"./analyzer/collectFunctions\";\r\nimport { buildCallGraph } from \"./analyzer/buildCallGraph\";\r\nimport { analyzePurity } from \"./analyzer/purity\";\r\nimport { propagateImpurity } from \"./analyzer/propagateImpurity\";\r\n\r\nconst command = process.argv[2];\r\n\r\nif (command !== \"analyze\") {\r\n console.log(\"Usage: callograph analyze\");\r\n process.exit(1);\r\n}\r\n\r\nconst program = createTsProgram(\"tsconfig.json\");\r\nconst functions = collectFunctions(program);\r\nconst graph = buildCallGraph(program, functions);\r\n\r\nlet purity = analyzePurity(program, functions);\r\npurity = propagateImpurity(graph, purity);\r\n\r\n/* ------------------------------\r\n CALL GRAPH\r\n--------------------------------*/\r\nconsole.log(\"\\nCall Graph:\\n\");\r\n\r\nlet edgeCount = 0;\r\n\r\nfor (const [callerId, callees] of graph.entries()) {\r\n const caller = functions.find(f => f.id === callerId);\r\n const callerName = caller ? caller.name : callerId;\r\n\r\n for (const calleeId of callees) {\r\n const callee = functions.find(f => f.id === calleeId);\r\n const calleeName = callee ? callee.name : calleeId;\r\n\r\n console.log(`${callerName} → ${calleeName}`);\r\n edgeCount++;\r\n }\r\n}\r\n\r\nif (edgeCount === 0) {\r\n console.log(\"(no project-level call edges found)\");\r\n}\r\n\r\nconsole.log(`\\nTotal call edges: ${edgeCount}`);\r\n\r\n/* ------------------------------\r\n PURITY REPORT\r\n--------------------------------*/\r\nconsole.log(\"\\nPurity Report:\\n\");\r\n\r\nconst impure = purity.filter(r => !r.isPure);\r\n\r\nif (impure.length === 0) {\r\n console.log(\"All analyzed functions are pure.\");\r\n} else {\r\n for (const r of impure) {\r\n console.log(`${r.file}:${r.line}:${r.character}`);\r\n console.log(`Function: ${r.name}`);\r\n console.log(`Status: X Impure`);\r\n console.log(\"Reasons:\");\r\n for (const reason of r.reasons) {\r\n console.log(` - ${reason}`);\r\n }\r\n console.log(\"\");\r\n }\r\n}\r\n"],"mappings":"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","names":["ts","program","ts","program","functions","graph","visit","ts","ts","ts","program","functions","visit","ts","graph","purity"]}
|
package/dist/index.cjs
CHANGED
|
@@ -23,7 +23,7 @@ __export(index_exports, {
|
|
|
23
23
|
version: () => version
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(index_exports);
|
|
26
|
-
var version = "0.0.
|
|
26
|
+
var version = "0.0.2";
|
|
27
27
|
// Annotate the CommonJS export names for ESM import in node:
|
|
28
28
|
0 && (module.exports = {
|
|
29
29
|
version
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export const version = \"0.0.
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export const version = \"0.0.2\";"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAO,IAAM,UAAU;","names":[]}
|
package/dist/index.d.cts
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export const version = \"0.0.
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export const version = \"0.0.2\";"],"mappings":";AAAO,IAAM,UAAU;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "callograph",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "TypeScript call graph and side-effect analyzer.",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"main": "./dist/index.cjs",
|
|
8
|
-
"module": "./dist/index.js",
|
|
9
|
-
"types": "./dist/index.d.ts",
|
|
10
|
-
"files": [
|
|
11
|
-
"dist",
|
|
12
|
-
"README.md",
|
|
13
|
-
"LICENSE"
|
|
14
|
-
],
|
|
15
|
-
"scripts": {
|
|
16
|
-
"build": "tsup",
|
|
17
|
-
"dev": "tsup --watch",
|
|
18
|
-
"prepublishOnly": "npm run build"
|
|
19
|
-
},
|
|
20
|
-
"devDependencies": {
|
|
21
|
-
"@types/node": "^25.2.3",
|
|
22
|
-
"tsup": "^8.5.1",
|
|
23
|
-
"typescript": "^5.9.3"
|
|
24
|
-
},
|
|
25
|
-
"bin": {
|
|
26
|
-
"callograph": "./dist/cli.cjs"
|
|
27
|
-
},
|
|
28
|
-
"repository": {
|
|
29
|
-
"type": "git",
|
|
30
|
-
"url": "git+https://github.com
|
|
31
|
-
},
|
|
32
|
-
"bugs": {
|
|
33
|
-
"url": "https://github.com
|
|
34
|
-
},
|
|
35
|
-
"homepage": "https://github.com
|
|
36
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "callograph",
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"description": "TypeScript call graph and side-effect analyzer.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsup",
|
|
17
|
+
"dev": "tsup --watch",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^25.2.3",
|
|
22
|
+
"tsup": "^8.5.1",
|
|
23
|
+
"typescript": "^5.9.3"
|
|
24
|
+
},
|
|
25
|
+
"bin": {
|
|
26
|
+
"callograph": "./dist/cli.cjs"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/miraclemarcel/callograph.git"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/miraclemarcel/callograph/issues"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/miraclemarcel/callograph#readme"
|
|
36
|
+
}
|