callograph 0.0.2 → 0.0.4

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 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 beyond lint rules by analyzing symbol resolution and cross-file call relationships using the TypeScript Compiler API.
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.1.0)
24
+ ## Core Capabilities (v0.0.4)
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.1.0)
234
+ ## Limitations (v0.0.4)
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,5 @@
1
1
  #!/usr/bin/env node
2
+ #!/usr/bin/env node
2
3
  "use strict";
3
4
  var __create = Object.create;
4
5
  var __defProp = Object.defineProperty;
@@ -65,7 +66,11 @@ function collectFunctions(program2) {
65
66
  if (sf.isDeclarationFile) continue;
66
67
  if (sf.fileName.includes("node_modules")) continue;
67
68
  import_typescript2.default.forEachChild(sf, function visit(n) {
68
- if (import_typescript2.default.isFunctionDeclaration(n) || import_typescript2.default.isMethodDeclaration(n) || import_typescript2.default.isArrowFunction(n) || import_typescript2.default.isFunctionExpression(n)) {
69
+ const isNamedFunctionDecl = import_typescript2.default.isFunctionDeclaration(n) && !!n.name;
70
+ const isMethod = import_typescript2.default.isMethodDeclaration(n);
71
+ const p = n.parent;
72
+ const isVarAssigned = (import_typescript2.default.isArrowFunction(n) || import_typescript2.default.isFunctionExpression(n)) && !!p && import_typescript2.default.isVariableDeclaration(p) && import_typescript2.default.isIdentifier(p.name);
73
+ if (isNamedFunctionDecl || isMethod || isVarAssigned) {
69
74
  const name = safeName(n, sf);
70
75
  const id = `${sf.fileName}:${n.pos}:${name}`;
71
76
  out.push({ id, name, file: sf.fileName, node: n });
@@ -78,33 +83,284 @@ function collectFunctions(program2) {
78
83
 
79
84
  // src/analyzer/buildCallGraph.ts
80
85
  var import_typescript3 = __toESM(require("typescript"), 1);
86
+ function isProjectFile(fileName) {
87
+ return !fileName.includes("node_modules") && !fileName.endsWith(".d.ts");
88
+ }
81
89
  function buildCallGraph(program2, functions2) {
82
90
  const checker = program2.getTypeChecker();
83
91
  const graph2 = /* @__PURE__ */ new Map();
84
- const functionMap = /* @__PURE__ */ new Map();
92
+ const declToId = /* @__PURE__ */ new Map();
85
93
  for (const fn of functions2) {
86
- functionMap.set(fn.name, fn.node);
87
- graph2.set(fn.name, /* @__PURE__ */ new Set());
94
+ graph2.set(fn.id, /* @__PURE__ */ new Set());
95
+ declToId.set(fn.node, fn.id);
88
96
  }
89
97
  for (const fn of functions2) {
90
- const fnName = fn.name;
91
- import_typescript3.default.forEachChild(fn.node, function visit(node) {
98
+ let visit2 = function(node) {
92
99
  if (import_typescript3.default.isCallExpression(node)) {
93
- const expression = node.expression;
94
- const symbol = checker.getSymbolAtLocation(expression);
95
- if (symbol) {
96
- const name = symbol.getName();
97
- if (graph2.has(fnName)) {
98
- graph2.get(fnName).add(name);
100
+ const sig = checker.getResolvedSignature(node);
101
+ const decl = sig?.getDeclaration();
102
+ if (decl && isProjectFile(decl.getSourceFile().fileName)) {
103
+ const calleeId = declToId.get(decl);
104
+ if (calleeId) {
105
+ graph2.get(callerId).add(calleeId);
99
106
  }
100
107
  }
101
108
  }
102
- import_typescript3.default.forEachChild(node, visit);
103
- });
109
+ import_typescript3.default.forEachChild(node, visit2);
110
+ };
111
+ var visit = visit2;
112
+ const callerId = fn.id;
113
+ visit2(fn.node);
104
114
  }
105
115
  return graph2;
106
116
  }
107
117
 
118
+ // src/analyzer/purity.ts
119
+ var import_typescript6 = __toESM(require("typescript"), 1);
120
+
121
+ // src/analyzer/helpers/globals.ts
122
+ var GLOBAL_OBJECTS = /* @__PURE__ */ new Set([
123
+ "window",
124
+ "document",
125
+ "global",
126
+ "process",
127
+ "localStorage",
128
+ "sessionStorage"
129
+ ]);
130
+ var MUTATING_ARRAY_METHODS = /* @__PURE__ */ new Set([
131
+ "push",
132
+ "pop",
133
+ "splice",
134
+ "shift",
135
+ "unshift",
136
+ "sort",
137
+ "reverse",
138
+ "copyWithin",
139
+ "fill"
140
+ ]);
141
+
142
+ // src/analyzer/helpers/ast.ts
143
+ var import_typescript4 = __toESM(require("typescript"), 1);
144
+ function isInside(node, container) {
145
+ return node.pos >= container.pos && node.end <= container.end;
146
+ }
147
+ function rootIdentifier(expr) {
148
+ if (import_typescript4.default.isIdentifier(expr)) return expr;
149
+ if (import_typescript4.default.isPropertyAccessExpression(expr)) return rootIdentifier(expr.expression);
150
+ if (import_typescript4.default.isElementAccessExpression(expr)) return rootIdentifier(expr.expression);
151
+ if (import_typescript4.default.isParenthesizedExpression(expr)) return rootIdentifier(expr.expression);
152
+ return null;
153
+ }
154
+ function isWriteOperator(kind) {
155
+ 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;
156
+ }
157
+
158
+ // src/analyzer/helpers/async.ts
159
+ var import_typescript5 = __toESM(require("typescript"), 1);
160
+ function isPromiseLike(type, checker) {
161
+ if (!type) return false;
162
+ const thenProp = type.getProperty("then");
163
+ if (!thenProp) return false;
164
+ const decl = thenProp.valueDeclaration ?? thenProp.declarations?.[0];
165
+ if (!decl) return false;
166
+ const thenType = checker.getTypeOfSymbolAtLocation(thenProp, decl);
167
+ return thenType.getCallSignatures().length > 0;
168
+ }
169
+ function isAwaited(node) {
170
+ return import_typescript5.default.isAwaitExpression(node.parent);
171
+ }
172
+ function isReturned(node) {
173
+ return import_typescript5.default.isReturnStatement(node.parent);
174
+ }
175
+ function isHandledWithThen(node) {
176
+ if (!import_typescript5.default.isPropertyAccessExpression(node.parent)) return false;
177
+ const name = node.parent.name.text;
178
+ return name === "then" || name === "catch" || name === "finally";
179
+ }
180
+ function isAsyncForEach(node) {
181
+ if (!import_typescript5.default.isPropertyAccessExpression(node.expression)) return false;
182
+ if (node.expression.name.text !== "forEach") return false;
183
+ const cb = node.arguments[0];
184
+ if (!cb) return false;
185
+ if (import_typescript5.default.isArrowFunction(cb) || import_typescript5.default.isFunctionExpression(cb)) {
186
+ return cb.modifiers?.some((m) => m.kind === import_typescript5.default.SyntaxKind.AsyncKeyword) ?? false;
187
+ }
188
+ return false;
189
+ }
190
+ function isAsyncEventListener(node) {
191
+ if (!import_typescript5.default.isPropertyAccessExpression(node.expression)) return false;
192
+ if (node.expression.name.text !== "addEventListener") return false;
193
+ const cb = node.arguments[1];
194
+ if (!cb) return false;
195
+ if (import_typescript5.default.isArrowFunction(cb) || import_typescript5.default.isFunctionExpression(cb)) {
196
+ return cb.modifiers?.some((m) => m.kind === import_typescript5.default.SyntaxKind.AsyncKeyword) ?? false;
197
+ }
198
+ return false;
199
+ }
200
+ function isPromiseAll(node) {
201
+ return import_typescript5.default.isPropertyAccessExpression(node.expression) && import_typescript5.default.isIdentifier(node.expression.expression) && node.expression.expression.text === "Promise" && node.expression.name.text === "all";
202
+ }
203
+
204
+ // src/analyzer/purity.ts
205
+ function analyzePurity(program2, functions2) {
206
+ const checker = program2.getTypeChecker();
207
+ const results = [];
208
+ for (const fn of functions2) {
209
+ let visit2 = function(node) {
210
+ if (import_typescript6.default.isBinaryExpression(node) && isWriteOperator(node.operatorToken.kind)) {
211
+ const left = node.left;
212
+ const root = rootIdentifier(left);
213
+ if (root && paramNames.has(root.text)) {
214
+ mark(`Mutates parameter \`${root.text}\``);
215
+ }
216
+ if (import_typescript6.default.isIdentifier(left)) {
217
+ const sym = checker.getSymbolAtLocation(left);
218
+ const decls = sym?.getDeclarations() ?? [];
219
+ const declaredInside = decls.some((d) => isInside(d, fn.node));
220
+ if (sym && decls.length > 0 && !declaredInside) {
221
+ mark(`Writes to outer scope \`${left.text}\``);
222
+ }
223
+ }
224
+ if (import_typescript6.default.isPropertyAccessExpression(left) && left.expression.kind === import_typescript6.default.SyntaxKind.ThisKeyword) {
225
+ mark(`Mutates \`this.${left.name.text}\``);
226
+ }
227
+ }
228
+ if (import_typescript6.default.isCallExpression(node) && import_typescript6.default.isPropertyAccessExpression(node.expression)) {
229
+ const method = node.expression.name.text;
230
+ const root = rootIdentifier(node.expression.expression);
231
+ if (root && paramNames.has(root.text) && MUTATING_ARRAY_METHODS.has(method)) {
232
+ mark(`Mutates parameter \`${root.text}\` via \`${method}()\``);
233
+ }
234
+ }
235
+ if (import_typescript6.default.isIdentifier(node)) {
236
+ const sym = checker.getSymbolAtLocation(node);
237
+ if (sym && GLOBAL_OBJECTS.has(sym.getName())) {
238
+ mark(`Accesses global \`${sym.getName()}\``);
239
+ }
240
+ }
241
+ if (import_typescript6.default.isPropertyAccessExpression(node)) {
242
+ const root = rootIdentifier(node.expression);
243
+ if (root?.text === "process" && node.name.text === "env") {
244
+ mark("Accesses `process.env` (non-deterministic)");
245
+ }
246
+ if (root?.text === "console") {
247
+ mark("Writes to console");
248
+ }
249
+ }
250
+ if (import_typescript6.default.isCallExpression(node)) {
251
+ const type = checker.getTypeAtLocation(node);
252
+ const expr = node.expression;
253
+ if (import_typescript6.default.isPropertyAccessExpression(expr) && import_typescript6.default.isIdentifier(expr.expression) && expr.expression.text === "Date" && expr.name.text === "now") {
254
+ mark("Calls Date.now() (non-deterministic)");
255
+ }
256
+ if (import_typescript6.default.isPropertyAccessExpression(expr) && import_typescript6.default.isIdentifier(expr.expression) && expr.expression.text === "Math" && expr.name.text === "random") {
257
+ mark("Calls Math.random() (non-deterministic)");
258
+ }
259
+ if (isPromiseLike(type, checker)) {
260
+ if (!isAwaited(node) && !isReturned(node) && !isHandledWithThen(node)) {
261
+ mark("Floating Promise (result ignored)");
262
+ }
263
+ }
264
+ if (isPromiseAll(node) && !isAwaited(node) && !isReturned(node)) {
265
+ mark("Unawaited Promise.all()");
266
+ }
267
+ if (isAsyncForEach(node)) {
268
+ mark("Async callback inside forEach (not awaited)");
269
+ }
270
+ if (isAsyncEventListener(node)) {
271
+ mark("Async event listener without error boundary");
272
+ }
273
+ }
274
+ if (import_typescript6.default.isNewExpression(node)) {
275
+ if (import_typescript6.default.isIdentifier(node.expression) && node.expression.text === "Date") {
276
+ mark("Instantiates Date (non-deterministic)");
277
+ }
278
+ }
279
+ if (import_typescript6.default.isAwaitExpression(node)) {
280
+ containsAwait = true;
281
+ }
282
+ if (import_typescript6.default.isTryStatement(node) && node.catchClause) {
283
+ hasTryCatch = true;
284
+ }
285
+ if (import_typescript6.default.isCatchClause(node) && node.block.statements.length === 0) {
286
+ mark("Empty catch block (error swallowed)");
287
+ }
288
+ import_typescript6.default.forEachChild(node, visit2);
289
+ };
290
+ var visit = visit2;
291
+ const sf = fn.node.getSourceFile();
292
+ const { line, character } = sf.getLineAndCharacterOfPosition(
293
+ fn.node.getStart(sf)
294
+ );
295
+ const reasons = [];
296
+ const paramNames = /* @__PURE__ */ new Set();
297
+ for (const p of fn.node.parameters) {
298
+ if (import_typescript6.default.isIdentifier(p.name)) paramNames.add(p.name.text);
299
+ }
300
+ const mark = (reason) => {
301
+ if (!reasons.includes(reason)) reasons.push(reason);
302
+ };
303
+ let containsAwait = false;
304
+ let hasTryCatch = false;
305
+ const isAsyncFunction = fn.node.modifiers?.some((m) => m.kind === import_typescript6.default.SyntaxKind.AsyncKeyword) ?? false;
306
+ visit2(fn.node);
307
+ if (isAsyncFunction && !containsAwait) {
308
+ mark("Async function without await");
309
+ }
310
+ if (isAsyncFunction && containsAwait && !hasTryCatch) {
311
+ mark("Async function without error handling (no try/catch)");
312
+ }
313
+ results.push({
314
+ id: fn.id,
315
+ name: fn.name,
316
+ file: fn.file,
317
+ line: line + 1,
318
+ character: character + 1,
319
+ isPure: reasons.length === 0,
320
+ reasons
321
+ });
322
+ }
323
+ return results;
324
+ }
325
+
326
+ // src/analyzer/propagateImpurity.ts
327
+ function propagateImpurity(graph2, purity2) {
328
+ const resultMap = /* @__PURE__ */ new Map();
329
+ for (const r of purity2) {
330
+ resultMap.set(r.id, r);
331
+ }
332
+ const reverse = /* @__PURE__ */ new Map();
333
+ for (const [callerId, calleeIds] of graph2.entries()) {
334
+ for (const calleeId of calleeIds) {
335
+ if (!reverse.has(calleeId)) {
336
+ reverse.set(calleeId, /* @__PURE__ */ new Set());
337
+ }
338
+ reverse.get(calleeId).add(callerId);
339
+ }
340
+ }
341
+ const queue = [];
342
+ for (const r of purity2) {
343
+ if (!r.isPure) {
344
+ queue.push(r.id);
345
+ }
346
+ }
347
+ while (queue.length > 0) {
348
+ const impureId = queue.shift();
349
+ const callers = reverse.get(impureId);
350
+ if (!callers) continue;
351
+ for (const callerId of callers) {
352
+ const caller = resultMap.get(callerId);
353
+ if (!caller) continue;
354
+ if (caller.isPure) {
355
+ caller.isPure = false;
356
+ caller.reasons.push(`Calls impure function`);
357
+ queue.push(callerId);
358
+ }
359
+ }
360
+ }
361
+ return Array.from(resultMap.values());
362
+ }
363
+
108
364
  // src/cli.ts
109
365
  var command = process.argv[2];
110
366
  if (command !== "analyze") {
@@ -114,10 +370,39 @@ if (command !== "analyze") {
114
370
  var program = createTsProgram("tsconfig.json");
115
371
  var functions = collectFunctions(program);
116
372
  var graph = buildCallGraph(program, functions);
373
+ var purity = analyzePurity(program, functions);
374
+ purity = propagateImpurity(graph, purity);
117
375
  console.log("\nCall Graph:\n");
118
- for (const [fn, calls] of graph.entries()) {
119
- for (const call of calls) {
120
- console.log(`${fn} \u2192 ${call}`);
376
+ var edgeCount = 0;
377
+ for (const [callerId, callees] of graph.entries()) {
378
+ const caller = functions.find((f) => f.id === callerId);
379
+ const callerName = caller ? caller.name : callerId;
380
+ for (const calleeId of callees) {
381
+ const callee = functions.find((f) => f.id === calleeId);
382
+ const calleeName = callee ? callee.name : calleeId;
383
+ console.log(`${callerName} \u2192 ${calleeName}`);
384
+ edgeCount++;
385
+ }
386
+ }
387
+ if (edgeCount === 0) {
388
+ console.log("(no project-level call edges found)");
389
+ }
390
+ console.log(`
391
+ Total call edges: ${edgeCount}`);
392
+ console.log("\nPurity Report:\n");
393
+ var impure = purity.filter((r) => !r.isPure);
394
+ if (impure.length === 0) {
395
+ console.log("All analyzed functions are pure.");
396
+ } else {
397
+ for (const r of impure) {
398
+ console.log(`${r.file}:${r.line}:${r.character}`);
399
+ console.log(`Function: ${r.name}`);
400
+ console.log(`Status: X Impure`);
401
+ console.log("Reasons:");
402
+ for (const reason of r.reasons) {
403
+ console.log(` - ${reason}`);
404
+ }
405
+ console.log("");
121
406
  }
122
407
  }
123
408
  //# 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","#!/usr/bin/env node\r\nimport { 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.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ #!/usr/bin/env node
2
3
 
3
4
  // src/analyzer/createProgram.ts
4
5
  import ts from "typescript";
@@ -42,7 +43,11 @@ function collectFunctions(program2) {
42
43
  if (sf.isDeclarationFile) continue;
43
44
  if (sf.fileName.includes("node_modules")) continue;
44
45
  ts2.forEachChild(sf, function visit(n) {
45
- if (ts2.isFunctionDeclaration(n) || ts2.isMethodDeclaration(n) || ts2.isArrowFunction(n) || ts2.isFunctionExpression(n)) {
46
+ const isNamedFunctionDecl = ts2.isFunctionDeclaration(n) && !!n.name;
47
+ const isMethod = ts2.isMethodDeclaration(n);
48
+ const p = n.parent;
49
+ const isVarAssigned = (ts2.isArrowFunction(n) || ts2.isFunctionExpression(n)) && !!p && ts2.isVariableDeclaration(p) && ts2.isIdentifier(p.name);
50
+ if (isNamedFunctionDecl || isMethod || isVarAssigned) {
46
51
  const name = safeName(n, sf);
47
52
  const id = `${sf.fileName}:${n.pos}:${name}`;
48
53
  out.push({ id, name, file: sf.fileName, node: n });
@@ -55,33 +60,284 @@ function collectFunctions(program2) {
55
60
 
56
61
  // src/analyzer/buildCallGraph.ts
57
62
  import ts3 from "typescript";
63
+ function isProjectFile(fileName) {
64
+ return !fileName.includes("node_modules") && !fileName.endsWith(".d.ts");
65
+ }
58
66
  function buildCallGraph(program2, functions2) {
59
67
  const checker = program2.getTypeChecker();
60
68
  const graph2 = /* @__PURE__ */ new Map();
61
- const functionMap = /* @__PURE__ */ new Map();
69
+ const declToId = /* @__PURE__ */ new Map();
62
70
  for (const fn of functions2) {
63
- functionMap.set(fn.name, fn.node);
64
- graph2.set(fn.name, /* @__PURE__ */ new Set());
71
+ graph2.set(fn.id, /* @__PURE__ */ new Set());
72
+ declToId.set(fn.node, fn.id);
65
73
  }
66
74
  for (const fn of functions2) {
67
- const fnName = fn.name;
68
- ts3.forEachChild(fn.node, function visit(node) {
75
+ let visit2 = function(node) {
69
76
  if (ts3.isCallExpression(node)) {
70
- const expression = node.expression;
71
- const symbol = checker.getSymbolAtLocation(expression);
72
- if (symbol) {
73
- const name = symbol.getName();
74
- if (graph2.has(fnName)) {
75
- graph2.get(fnName).add(name);
77
+ const sig = checker.getResolvedSignature(node);
78
+ const decl = sig?.getDeclaration();
79
+ if (decl && isProjectFile(decl.getSourceFile().fileName)) {
80
+ const calleeId = declToId.get(decl);
81
+ if (calleeId) {
82
+ graph2.get(callerId).add(calleeId);
76
83
  }
77
84
  }
78
85
  }
79
- ts3.forEachChild(node, visit);
80
- });
86
+ ts3.forEachChild(node, visit2);
87
+ };
88
+ var visit = visit2;
89
+ const callerId = fn.id;
90
+ visit2(fn.node);
81
91
  }
82
92
  return graph2;
83
93
  }
84
94
 
95
+ // src/analyzer/purity.ts
96
+ import ts6 from "typescript";
97
+
98
+ // src/analyzer/helpers/globals.ts
99
+ var GLOBAL_OBJECTS = /* @__PURE__ */ new Set([
100
+ "window",
101
+ "document",
102
+ "global",
103
+ "process",
104
+ "localStorage",
105
+ "sessionStorage"
106
+ ]);
107
+ var MUTATING_ARRAY_METHODS = /* @__PURE__ */ new Set([
108
+ "push",
109
+ "pop",
110
+ "splice",
111
+ "shift",
112
+ "unshift",
113
+ "sort",
114
+ "reverse",
115
+ "copyWithin",
116
+ "fill"
117
+ ]);
118
+
119
+ // src/analyzer/helpers/ast.ts
120
+ import ts4 from "typescript";
121
+ function isInside(node, container) {
122
+ return node.pos >= container.pos && node.end <= container.end;
123
+ }
124
+ function rootIdentifier(expr) {
125
+ if (ts4.isIdentifier(expr)) return expr;
126
+ if (ts4.isPropertyAccessExpression(expr)) return rootIdentifier(expr.expression);
127
+ if (ts4.isElementAccessExpression(expr)) return rootIdentifier(expr.expression);
128
+ if (ts4.isParenthesizedExpression(expr)) return rootIdentifier(expr.expression);
129
+ return null;
130
+ }
131
+ function isWriteOperator(kind) {
132
+ 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;
133
+ }
134
+
135
+ // src/analyzer/helpers/async.ts
136
+ import ts5 from "typescript";
137
+ function isPromiseLike(type, checker) {
138
+ if (!type) return false;
139
+ const thenProp = type.getProperty("then");
140
+ if (!thenProp) return false;
141
+ const decl = thenProp.valueDeclaration ?? thenProp.declarations?.[0];
142
+ if (!decl) return false;
143
+ const thenType = checker.getTypeOfSymbolAtLocation(thenProp, decl);
144
+ return thenType.getCallSignatures().length > 0;
145
+ }
146
+ function isAwaited(node) {
147
+ return ts5.isAwaitExpression(node.parent);
148
+ }
149
+ function isReturned(node) {
150
+ return ts5.isReturnStatement(node.parent);
151
+ }
152
+ function isHandledWithThen(node) {
153
+ if (!ts5.isPropertyAccessExpression(node.parent)) return false;
154
+ const name = node.parent.name.text;
155
+ return name === "then" || name === "catch" || name === "finally";
156
+ }
157
+ function isAsyncForEach(node) {
158
+ if (!ts5.isPropertyAccessExpression(node.expression)) return false;
159
+ if (node.expression.name.text !== "forEach") return false;
160
+ const cb = node.arguments[0];
161
+ if (!cb) return false;
162
+ if (ts5.isArrowFunction(cb) || ts5.isFunctionExpression(cb)) {
163
+ return cb.modifiers?.some((m) => m.kind === ts5.SyntaxKind.AsyncKeyword) ?? false;
164
+ }
165
+ return false;
166
+ }
167
+ function isAsyncEventListener(node) {
168
+ if (!ts5.isPropertyAccessExpression(node.expression)) return false;
169
+ if (node.expression.name.text !== "addEventListener") return false;
170
+ const cb = node.arguments[1];
171
+ if (!cb) return false;
172
+ if (ts5.isArrowFunction(cb) || ts5.isFunctionExpression(cb)) {
173
+ return cb.modifiers?.some((m) => m.kind === ts5.SyntaxKind.AsyncKeyword) ?? false;
174
+ }
175
+ return false;
176
+ }
177
+ function isPromiseAll(node) {
178
+ return ts5.isPropertyAccessExpression(node.expression) && ts5.isIdentifier(node.expression.expression) && node.expression.expression.text === "Promise" && node.expression.name.text === "all";
179
+ }
180
+
181
+ // src/analyzer/purity.ts
182
+ function analyzePurity(program2, functions2) {
183
+ const checker = program2.getTypeChecker();
184
+ const results = [];
185
+ for (const fn of functions2) {
186
+ let visit2 = function(node) {
187
+ if (ts6.isBinaryExpression(node) && isWriteOperator(node.operatorToken.kind)) {
188
+ const left = node.left;
189
+ const root = rootIdentifier(left);
190
+ if (root && paramNames.has(root.text)) {
191
+ mark(`Mutates parameter \`${root.text}\``);
192
+ }
193
+ if (ts6.isIdentifier(left)) {
194
+ const sym = checker.getSymbolAtLocation(left);
195
+ const decls = sym?.getDeclarations() ?? [];
196
+ const declaredInside = decls.some((d) => isInside(d, fn.node));
197
+ if (sym && decls.length > 0 && !declaredInside) {
198
+ mark(`Writes to outer scope \`${left.text}\``);
199
+ }
200
+ }
201
+ if (ts6.isPropertyAccessExpression(left) && left.expression.kind === ts6.SyntaxKind.ThisKeyword) {
202
+ mark(`Mutates \`this.${left.name.text}\``);
203
+ }
204
+ }
205
+ if (ts6.isCallExpression(node) && ts6.isPropertyAccessExpression(node.expression)) {
206
+ const method = node.expression.name.text;
207
+ const root = rootIdentifier(node.expression.expression);
208
+ if (root && paramNames.has(root.text) && MUTATING_ARRAY_METHODS.has(method)) {
209
+ mark(`Mutates parameter \`${root.text}\` via \`${method}()\``);
210
+ }
211
+ }
212
+ if (ts6.isIdentifier(node)) {
213
+ const sym = checker.getSymbolAtLocation(node);
214
+ if (sym && GLOBAL_OBJECTS.has(sym.getName())) {
215
+ mark(`Accesses global \`${sym.getName()}\``);
216
+ }
217
+ }
218
+ if (ts6.isPropertyAccessExpression(node)) {
219
+ const root = rootIdentifier(node.expression);
220
+ if (root?.text === "process" && node.name.text === "env") {
221
+ mark("Accesses `process.env` (non-deterministic)");
222
+ }
223
+ if (root?.text === "console") {
224
+ mark("Writes to console");
225
+ }
226
+ }
227
+ if (ts6.isCallExpression(node)) {
228
+ const type = checker.getTypeAtLocation(node);
229
+ const expr = node.expression;
230
+ if (ts6.isPropertyAccessExpression(expr) && ts6.isIdentifier(expr.expression) && expr.expression.text === "Date" && expr.name.text === "now") {
231
+ mark("Calls Date.now() (non-deterministic)");
232
+ }
233
+ if (ts6.isPropertyAccessExpression(expr) && ts6.isIdentifier(expr.expression) && expr.expression.text === "Math" && expr.name.text === "random") {
234
+ mark("Calls Math.random() (non-deterministic)");
235
+ }
236
+ if (isPromiseLike(type, checker)) {
237
+ if (!isAwaited(node) && !isReturned(node) && !isHandledWithThen(node)) {
238
+ mark("Floating Promise (result ignored)");
239
+ }
240
+ }
241
+ if (isPromiseAll(node) && !isAwaited(node) && !isReturned(node)) {
242
+ mark("Unawaited Promise.all()");
243
+ }
244
+ if (isAsyncForEach(node)) {
245
+ mark("Async callback inside forEach (not awaited)");
246
+ }
247
+ if (isAsyncEventListener(node)) {
248
+ mark("Async event listener without error boundary");
249
+ }
250
+ }
251
+ if (ts6.isNewExpression(node)) {
252
+ if (ts6.isIdentifier(node.expression) && node.expression.text === "Date") {
253
+ mark("Instantiates Date (non-deterministic)");
254
+ }
255
+ }
256
+ if (ts6.isAwaitExpression(node)) {
257
+ containsAwait = true;
258
+ }
259
+ if (ts6.isTryStatement(node) && node.catchClause) {
260
+ hasTryCatch = true;
261
+ }
262
+ if (ts6.isCatchClause(node) && node.block.statements.length === 0) {
263
+ mark("Empty catch block (error swallowed)");
264
+ }
265
+ ts6.forEachChild(node, visit2);
266
+ };
267
+ var visit = visit2;
268
+ const sf = fn.node.getSourceFile();
269
+ const { line, character } = sf.getLineAndCharacterOfPosition(
270
+ fn.node.getStart(sf)
271
+ );
272
+ const reasons = [];
273
+ const paramNames = /* @__PURE__ */ new Set();
274
+ for (const p of fn.node.parameters) {
275
+ if (ts6.isIdentifier(p.name)) paramNames.add(p.name.text);
276
+ }
277
+ const mark = (reason) => {
278
+ if (!reasons.includes(reason)) reasons.push(reason);
279
+ };
280
+ let containsAwait = false;
281
+ let hasTryCatch = false;
282
+ const isAsyncFunction = fn.node.modifiers?.some((m) => m.kind === ts6.SyntaxKind.AsyncKeyword) ?? false;
283
+ visit2(fn.node);
284
+ if (isAsyncFunction && !containsAwait) {
285
+ mark("Async function without await");
286
+ }
287
+ if (isAsyncFunction && containsAwait && !hasTryCatch) {
288
+ mark("Async function without error handling (no try/catch)");
289
+ }
290
+ results.push({
291
+ id: fn.id,
292
+ name: fn.name,
293
+ file: fn.file,
294
+ line: line + 1,
295
+ character: character + 1,
296
+ isPure: reasons.length === 0,
297
+ reasons
298
+ });
299
+ }
300
+ return results;
301
+ }
302
+
303
+ // src/analyzer/propagateImpurity.ts
304
+ function propagateImpurity(graph2, purity2) {
305
+ const resultMap = /* @__PURE__ */ new Map();
306
+ for (const r of purity2) {
307
+ resultMap.set(r.id, r);
308
+ }
309
+ const reverse = /* @__PURE__ */ new Map();
310
+ for (const [callerId, calleeIds] of graph2.entries()) {
311
+ for (const calleeId of calleeIds) {
312
+ if (!reverse.has(calleeId)) {
313
+ reverse.set(calleeId, /* @__PURE__ */ new Set());
314
+ }
315
+ reverse.get(calleeId).add(callerId);
316
+ }
317
+ }
318
+ const queue = [];
319
+ for (const r of purity2) {
320
+ if (!r.isPure) {
321
+ queue.push(r.id);
322
+ }
323
+ }
324
+ while (queue.length > 0) {
325
+ const impureId = queue.shift();
326
+ const callers = reverse.get(impureId);
327
+ if (!callers) continue;
328
+ for (const callerId of callers) {
329
+ const caller = resultMap.get(callerId);
330
+ if (!caller) continue;
331
+ if (caller.isPure) {
332
+ caller.isPure = false;
333
+ caller.reasons.push(`Calls impure function`);
334
+ queue.push(callerId);
335
+ }
336
+ }
337
+ }
338
+ return Array.from(resultMap.values());
339
+ }
340
+
85
341
  // src/cli.ts
86
342
  var command = process.argv[2];
87
343
  if (command !== "analyze") {
@@ -91,10 +347,39 @@ if (command !== "analyze") {
91
347
  var program = createTsProgram("tsconfig.json");
92
348
  var functions = collectFunctions(program);
93
349
  var graph = buildCallGraph(program, functions);
350
+ var purity = analyzePurity(program, functions);
351
+ purity = propagateImpurity(graph, purity);
94
352
  console.log("\nCall Graph:\n");
95
- for (const [fn, calls] of graph.entries()) {
96
- for (const call of calls) {
97
- console.log(`${fn} \u2192 ${call}`);
353
+ var edgeCount = 0;
354
+ for (const [callerId, callees] of graph.entries()) {
355
+ const caller = functions.find((f) => f.id === callerId);
356
+ const callerName = caller ? caller.name : callerId;
357
+ for (const calleeId of callees) {
358
+ const callee = functions.find((f) => f.id === calleeId);
359
+ const calleeName = callee ? callee.name : calleeId;
360
+ console.log(`${callerName} \u2192 ${calleeName}`);
361
+ edgeCount++;
362
+ }
363
+ }
364
+ if (edgeCount === 0) {
365
+ console.log("(no project-level call edges found)");
366
+ }
367
+ console.log(`
368
+ Total call edges: ${edgeCount}`);
369
+ console.log("\nPurity Report:\n");
370
+ var impure = purity.filter((r) => !r.isPure);
371
+ if (impure.length === 0) {
372
+ console.log("All analyzed functions are pure.");
373
+ } else {
374
+ for (const r of impure) {
375
+ console.log(`${r.file}:${r.line}:${r.character}`);
376
+ console.log(`Function: ${r.name}`);
377
+ console.log(`Status: X Impure`);
378
+ console.log("Reasons:");
379
+ for (const reason of r.reasons) {
380
+ console.log(` - ${reason}`);
381
+ }
382
+ console.log("");
98
383
  }
99
384
  }
100
385
  //# 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","#!/usr/bin/env node\r\nimport { 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
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  "use strict";
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -23,7 +24,7 @@ __export(index_exports, {
23
24
  version: () => version
24
25
  });
25
26
  module.exports = __toCommonJS(index_exports);
26
- var version = "0.0.1";
27
+ var version = "0.0.2";
27
28
  // Annotate the CommonJS export names for ESM import in node:
28
29
  0 && (module.exports = {
29
30
  version
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export const version = \"0.0.1\";"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAO,IAAM,UAAU;","names":[]}
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
@@ -1,3 +1,3 @@
1
- declare const version = "0.0.1";
1
+ declare const version = "0.0.2";
2
2
 
3
3
  export { version };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- declare const version = "0.0.1";
1
+ declare const version = "0.0.2";
2
2
 
3
3
  export { version };
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
+ #!/usr/bin/env node
2
+
1
3
  // src/index.ts
2
- var version = "0.0.1";
4
+ var version = "0.0.2";
3
5
  export {
4
6
  version
5
7
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export const version = \"0.0.1\";"],"mappings":";AAAO,IAAM,UAAU;","names":[]}
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.2",
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/<your-username>/callograph.git"
31
- },
32
- "bugs": {
33
- "url": "https://github.com/<your-username>/callograph/issues"
34
- },
35
- "homepage": "https://github.com/<your-username>/callograph#readme"
36
- }
1
+ {
2
+ "name": "callograph",
3
+ "version": "0.0.4",
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
+ }