callograph 0.0.8 → 1.0.0

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
@@ -1,40 +1,49 @@
1
+
2
+ ---
3
+
1
4
  # Callograph
2
5
 
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.
6
+ **Callograph** is a structural static analysis engine for TypeScript that builds a full program-level call graph and performs interprocedural purity and async safety analysis.
4
7
 
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.
8
+ It goes beyond lint rules by analyzing symbol resolution, cross-file relationships, and effect propagation using the TypeScript Compiler API.
9
+
10
+ Callograph is built for engineers who want architectural clarity — not stylistic warnings.
6
11
 
7
12
  ---
8
13
 
9
- ## Why Callograph?
14
+ ## 🚀 What Callograph Does
10
15
 
11
- Modern TypeScript codebases suffer from:
16
+ Callograph analyzes your entire TypeScript program and answers questions like:
12
17
 
13
- * Hidden side effects
14
- * Implicit mutation
15
- * Unintended global state access
16
- * Async misuse
17
- * Impure utilities leaking into shared modules
18
- * Lack of deterministic boundaries
18
+ * Which functions call which?
19
+ * Where do side effects originate?
20
+ * How does impurity propagate?
21
+ * Are promises safely handled?
22
+ * Are async boundaries enforced?
23
+ * Are errors swallowed?
24
+ * Are async effects escaping function boundaries?
19
25
 
20
- Callograph provides structural visibility into function relationships and propagates impurity across the call graph to surface real architectural risks.
26
+ It provides structural reasoning about your system.
21
27
 
22
28
  ---
23
29
 
24
- ## Core Capabilities (v0.0.8)
30
+ # Core Capabilities (v1.0.0)
25
31
 
26
- ### 1. Call Graph Construction
32
+ ---
27
33
 
28
- Builds a full program-level call graph from your `tsconfig.json`.
34
+ ## 1. Program-Level Call Graph Construction
29
35
 
30
- It identifies:
36
+ Callograph builds a complete call graph from your `tsconfig.json`.
37
+
38
+ It detects:
31
39
 
32
40
  * Function → function relationships
33
41
  * Cross-file calls
34
- * Imported symbol calls
35
- * Method calls (where statically resolvable)
42
+ * Imported symbol resolution
43
+ * Method calls (when statically resolvable)
44
+ * Interprocedural call edges
36
45
 
37
- Example output:
46
+ Example:
38
47
 
39
48
  ```
40
49
  Call Graph:
@@ -44,38 +53,64 @@ applyDiscount → fetchTaxRate
44
53
  fetchTaxRate → Date.now
45
54
  ```
46
55
 
56
+ This graph becomes the foundation for structural effect analysis.
57
+
47
58
  ---
48
59
 
49
- ### 2. Function Purity Detection
60
+ ## 2. Function Purity Analysis
61
+
62
+ Callograph determines whether a function is pure or impure based on structural rules.
50
63
 
51
- Callograph analyzes whether a function is:
64
+ It detects:
52
65
 
53
- * Mutating its parameters
54
- * Writing to outer scope variables
55
- * Accessing global state
56
- * Calling impure functions
66
+ ### Parameter Mutation
67
+
68
+ * Direct reassignment
69
+ * Deep property mutation
70
+ * Array mutator calls (`push`, `splice`, etc.)
57
71
 
58
72
  Example:
59
73
 
60
74
  ```
61
- src/utils/calc.ts:42
62
- Function: calculateTotal
63
- Status: ❌ Impure
75
+ ❌ Mutates parameter `items`
76
+ ```
77
+
78
+ ---
79
+
80
+ ### Outer Scope Writes
81
+
82
+ Detects writes to variables declared outside the function.
64
83
 
65
- Reasons:
66
- - Mutates parameter `items`
67
- - Calls impure function `fetchTaxRate`
84
+ ```
85
+ Writes to outer scope `cache`
68
86
  ```
69
87
 
70
88
  ---
71
89
 
72
- ### 3. Impurity Propagation
90
+ ### Global State Access
91
+
92
+ Flags access to:
93
+
94
+ * `process`
95
+ * `process.env`
96
+ * `window`
97
+ * `global`
98
+ * `document`
99
+ * Console writes
100
+ * `Date.now()`
101
+ * `Math.random()`
102
+
103
+ These are marked as non-deterministic or impure operations.
104
+
105
+ ---
106
+
107
+ ## 3. Impurity Propagation (Interprocedural)
73
108
 
74
109
  If:
75
110
 
76
111
  ```
77
112
  A → calls B
78
- B impure
113
+ B is impure
79
114
  ```
80
115
 
81
116
  Then:
@@ -84,194 +119,314 @@ Then:
84
119
  A becomes impure
85
120
  ```
86
121
 
87
- This propagation makes Callograph significantly more powerful than simple AST pattern scanning.
122
+ Callograph propagates impurity across the call graph using reverse-graph traversal and fixed-point propagation.
88
123
 
89
- It reasons about effect chains.
124
+ This makes impurity structural, not local.
90
125
 
91
126
  ---
92
127
 
93
- ### 4. Global State & Non-Determinism Detection
128
+ ## 4. Enterprise Async Safety Engine
94
129
 
95
- Flags usage of:
130
+ Callograph includes a dedicated async analysis engine, separate from purity.
96
131
 
97
- * `process.env`
98
- * `Date.now()`
99
- * `Math.random()`
100
- * `global`
101
- * `window`
102
- * Console writes (optional strict mode)
132
+ It performs structural async inference.
103
133
 
104
- Useful for teams enforcing deterministic or pure-core architectures.
134
+ ---
135
+
136
+ ### Floating Promise Detection
137
+
138
+ Flags:
139
+
140
+ * Unawaited promises
141
+ * Fire-and-forget async calls
142
+ * Promises ignored inside expressions
143
+ * Promises ignored inside loops (loop-aware escalation)
144
+
145
+ Example:
146
+
147
+ ```
148
+ ❌ Floating promise
149
+ ⚠️ Floating promise inside loop
150
+ ```
105
151
 
106
152
  ---
107
153
 
108
- ### 5. Parameter Mutation Detection
154
+ ### Promise.all Structural Analysis
109
155
 
110
156
  Detects:
111
157
 
112
- * Direct reassignment
113
- * Deep property mutation
114
- * Array mutation (`push`, `splice`, etc.)
158
+ * Unawaited `Promise.all`
159
+ * Promise arrays containing unhandled async calls
160
+ * Deep inspection of array elements
161
+
162
+ ---
163
+
164
+ ### Async Callback Detection
165
+
166
+ Flags async callbacks in:
167
+
168
+ * `forEach`
169
+ * `map`
170
+ * `filter`
171
+ * `reduce`
172
+ * `addEventListener`
115
173
 
116
174
  Example:
117
175
 
118
176
  ```
119
- Parameter mutation detected:
120
- updateUser(user) → user.name = "new"
177
+ ⚠️ Async callback inside array method
121
178
  ```
122
179
 
123
180
  ---
124
181
 
125
- ### 6. Async Safety Checks
182
+ ### Error Swallowing Detection
183
+
184
+ Detects:
185
+
186
+ * Empty `.catch()` blocks
187
+ * Swallowed promise rejections
188
+
189
+ Example:
190
+
191
+ ```
192
+ ❌ Error swallowed in .catch()
193
+ ```
194
+
195
+ ---
196
+
197
+ ### Async Boundary Enforcement
126
198
 
127
199
  Flags:
128
200
 
129
- * Unawaited promises
130
- * Fire-and-forget async calls
131
- * Async functions lacking error handling (basic detection)
201
+ ```
202
+ async function foo() {
203
+ await fetchData();
204
+ }
205
+ ```
206
+
207
+ If no `try/catch` boundary exists:
208
+
209
+ ```
210
+ ⚠️ Async function contains await but has no error boundary
211
+ ```
212
+
213
+ ---
214
+
215
+ ### Interprocedural Async Propagation
216
+
217
+ If:
218
+
219
+ ```
220
+ A calls B
221
+ B returns a promise
222
+ A does not await B
223
+ ```
224
+
225
+ Then:
226
+
227
+ ```
228
+ A is async-unsafe
229
+ ```
230
+
231
+ Callograph propagates async risk across the call graph.
132
232
 
133
233
  ---
134
234
 
135
- ### 7. CI Integration
235
+ ### Severity Model
236
+
237
+ Async issues are categorized:
238
+
239
+ * `error`
240
+ * `warning`
241
+ * `info`
136
242
 
137
- Callograph can fail CI if impure functions are detected.
243
+ You can configure thresholds:
244
+
245
+ ```
246
+ --async-severity=warning
247
+ --fail-on-async
248
+ ```
249
+
250
+ ---
251
+
252
+ ## 5. CI Integration
253
+
254
+ Callograph supports CI enforcement:
138
255
 
139
256
  ```
140
257
  callograph analyze --fail-on-impure
258
+ callograph analyze --fail-on-async
141
259
  ```
142
260
 
143
- This allows enforcement of architectural constraints.
261
+ You can enforce architectural constraints in pipelines.
144
262
 
145
263
  ---
146
264
 
147
- ## Installation
265
+ ## 6. JSON Output Mode
266
+
267
+ Structured JSON output for automation:
148
268
 
149
- ```bash
150
- npm install --save-dev callograph
151
269
  ```
270
+ callograph analyze --json
271
+ ```
272
+
273
+ Useful for:
274
+
275
+ * CI dashboards
276
+ * Custom reporting
277
+ * PR bots
278
+ * IDE integrations
279
+
280
+ ---
152
281
 
153
- Or use directly:
282
+ ## 7. Smart Ignore System
283
+
284
+ Callograph automatically:
285
+
286
+ * Ignores `node_modules`
287
+ * Detects and ignores its own source when analyzing itself
288
+
289
+ You can manually ignore paths:
154
290
 
155
- ```bash
156
- npx callograph analyze
291
+ ```
292
+ --ignore=src/generated,dist
157
293
  ```
158
294
 
159
295
  ---
160
296
 
161
- ## Usage
297
+ # CLI Usage
162
298
 
163
- Analyze project based on `tsconfig.json`:
299
+ Analyze project:
164
300
 
165
- ```bash
301
+ ```
166
302
  callograph analyze
167
303
  ```
168
304
 
169
- JSON output:
305
+ Custom tsconfig:
170
306
 
171
- ```bash
172
- callograph analyze --json
307
+ ```
308
+ callograph analyze tsconfig.build.json
173
309
  ```
174
310
 
175
- Fail CI if impurity detected:
311
+ Fail on impurity:
176
312
 
177
- ```bash
313
+ ```
178
314
  callograph analyze --fail-on-impure
179
315
  ```
180
316
 
181
- Export DOT graph:
317
+ Fail on async violations:
182
318
 
183
- ```bash
184
- callograph analyze --dot
319
+ ```
320
+ callograph analyze --fail-on-async
185
321
  ```
186
322
 
187
- ---
323
+ Adjust async severity threshold:
188
324
 
189
- ## Architecture
325
+ ```
326
+ callograph analyze --async-severity=warning
327
+ ```
190
328
 
191
- Callograph uses:
329
+ JSON output:
192
330
 
193
- * TypeScript Compiler API
194
- * Program-level symbol resolution
195
- * TypeChecker-based call resolution
196
- * AST traversal via `ts.forEachChild`
197
- * Impurity propagation via call graph traversal
331
+ ```
332
+ callograph analyze --json
333
+ ```
198
334
 
199
- Pipeline:
335
+ ---
336
+
337
+ # Architecture
338
+
339
+ Callograph pipeline:
200
340
 
201
341
  ```
202
342
  Load tsconfig
203
343
 
204
- Create Program
344
+ Create TypeScript Program
345
+
346
+ Collect function declarations (ignore-aware)
205
347
 
206
- Extract function symbols
348
+ Build call graph (TypeChecker-based resolution)
207
349
 
208
- Build call graph
350
+ Purity analysis
209
351
 
210
- Detect side effects
352
+ Impurity propagation
211
353
 
212
- Propagate impurity
354
+ Async flow analysis
213
355
 
214
- Report results
356
+ Async return propagation
357
+
358
+ Async risk propagation
359
+
360
+ Severity filtering
361
+
362
+ Report (Text or JSON)
215
363
  ```
216
364
 
217
365
  ---
218
366
 
219
- ## Design Philosophy
220
-
221
- Callograph does not attempt perfect purity inference.
222
- It aims to provide practical, high-signal diagnostics with minimal noise.
367
+ # Design Philosophy
223
368
 
224
- It prioritizes:
369
+ Callograph is designed for:
225
370
 
226
- * Deterministic analysis
227
- * CI compatibility
228
- * Clear reasoning in output
371
+ * Structural clarity
372
+ * Deterministic reasoning
229
373
  * Minimal false positives
230
- * Performance (<2s on mid-sized codebases)
374
+ * Interprocedural analysis
375
+ * CI enforcement
376
+ * Performance on mid-sized codebases
377
+
378
+ It does not aim for perfect theoretical purity inference.
379
+
380
+ It aims for high-signal architectural diagnostics.
231
381
 
232
382
  ---
233
383
 
234
- ## Limitations (v0.0.8)
384
+ # Current Limitations
235
385
 
236
- * Dynamic runtime dispatch cannot always be resolved.
237
- * Higher-order functions are partially supported.
238
- * Deep framework-specific analysis is not included.
239
- * No IDE integration yet.
386
+ * Dynamic runtime dispatch cannot always be resolved
387
+ * Higher-order functional inference is partial
388
+ * No race-condition modeling (sync/async timing hazards)
389
+ * No async escape-boundary graph modeling yet
390
+ * No IDE plugin yet
240
391
 
241
392
  ---
242
393
 
243
- ## Roadmap
394
+ # Roadmap
244
395
 
396
+ Planned enhancements:
397
+
398
+ * DOT graph export
399
+ * Circular dependency detection
245
400
  * Shared mutable singleton detection
246
401
  * Closure-captured state analysis
247
- * Circular call detection
248
- * Effect scoring per module
249
- * Interactive HTML graph visualization
402
+ * Module-level effect scoring
403
+ * Async escape-boundary modeling
404
+ * Mixed sync/async race detection
405
+ * HTML graph visualization
250
406
  * GitHub PR comment bot
251
- * Monorepo support with project references
407
+ * Monorepo project reference support
252
408
 
253
409
  ---
254
410
 
255
- ## License
256
-
257
- MIT
411
+ # Versioning
258
412
 
259
- ---
260
-
261
- ## Contributing
413
+ The current release introduces:
262
414
 
263
- Contributions are welcome.
415
+ * Dedicated async engine
416
+ * Interprocedural async propagation
417
+ * Severity modeling
418
+ * JSON output
419
+ * Smart ignore system
420
+ * Enterprise boundary detection
264
421
 
265
- * Open an issue to discuss architecture changes.
266
- * Keep changes deterministic and type-safe.
267
- * Add tests for new analysis rules.
422
+ Callograph is evolving toward a structural effect analysis engine for TypeScript.
268
423
 
269
424
  ---
270
-
271
- ## Vision
425
+
426
+ # Vision
272
427
 
273
428
  Callograph aims to become:
274
429
 
275
- > A structural analysis tool that gives engineers clarity over side effects, purity, and call relationships in large TypeScript systems.
430
+ > A structural analysis engine that provides deep visibility into side effects, purity, async safety, and architectural boundaries in large TypeScript systems.
276
431
 
277
- ---
432
+ ---
package/dist/cli.cjs CHANGED
@@ -1,8 +1,24 @@
1
1
  #!/usr/bin/env node
2
- "use strict";var se=Object.create;var G=Object.defineProperty;var ne=Object.getOwnPropertyDescriptor;var ie=Object.getOwnPropertyNames;var re=Object.getPrototypeOf,oe=Object.prototype.hasOwnProperty;var ae=(e,t,s,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of ie(t))!oe.call(e,i)&&i!==s&&G(e,i,{get:()=>t[i],enumerable:!(r=ne(t,i))||r.enumerable});return e};var S=(e,t,s)=>(s=e!=null?se(re(e)):{},ae(t||!e||!e.__esModule?G(s,"default",{value:e,enumerable:!0}):s,e));var y=S(require("typescript"),1);function R(e){let t=y.default.readConfigFile(e,y.default.sys.readFile);if(t.error)throw new Error(y.default.flattenDiagnosticMessageText(t.error.messageText,`
3
- `));let s=y.default.parseJsonConfigFileContent(t.config,y.default.sys,process.cwd());return y.default.createProgram({rootNames:s.fileNames,options:s.options})}var m=S(require("typescript"),1);function ce(e,t){let s=e;if(s.name&&m.default.isIdentifier(s.name))return s.name.text;if(s.name&&(m.default.isStringLiteral(s.name)||m.default.isNumericLiteral(s.name)))return String(s.name.text);let r=e.parent;if(r&&m.default.isVariableDeclaration(r)&&m.default.isIdentifier(r.name))return r.name.text;if(r&&m.default.isBinaryExpression(r)&&m.default.isPropertyAccessExpression(r.left))return r.left.name.text;let{line:i,character:o}=t.getLineAndCharacterOfPosition(e.getStart(t));return`<anonymous@${i+1}:${o+1}>`}function q(e){let t=[];for(let s of e.getSourceFiles())s.isDeclarationFile||s.fileName.includes("node_modules")||m.default.forEachChild(s,function r(i){let o=m.default.isFunctionDeclaration(i)&&!!i.name,f=m.default.isMethodDeclaration(i),x=i.parent,g=(m.default.isArrowFunction(i)||m.default.isFunctionExpression(i))&&!!x&&m.default.isVariableDeclaration(x)&&m.default.isIdentifier(x.name);if(o||f||g){let h=ce(i,s),A=`${s.fileName}:${i.pos}:${h}`;t.push({id:A,name:h,file:s.fileName,node:i})}m.default.forEachChild(i,r)});return t}var w=S(require("typescript"),1);function le(e){return!e.includes("node_modules")&&!e.endsWith(".d.ts")}function O(e,t){let s=e.getTypeChecker(),r=new Map,i=new Map;for(let f of t)r.set(f.id,new Set),i.set(f.node,f.id);for(let f of t){let g=function(h){if(w.default.isCallExpression(h)){let c=s.getResolvedSignature(h)?.getDeclaration();if(c&&le(c.getSourceFile().fileName)){let P=i.get(c);P&&r.get(x).add(P)}}w.default.forEachChild(h,g)};var o=g;let x=f.id;g(f.node)}return r}var a=S(require("typescript"),1);var W=new Set(["window","document","global","process","localStorage","sessionStorage"]),_=new Set(["push","pop","splice","shift","unshift","sort","reverse","copyWithin","fill"]);var p=S(require("typescript"),1);function B(e,t){return e.pos>=t.pos&&e.end<=t.end}function E(e){return p.default.isIdentifier(e)?e:p.default.isPropertyAccessExpression(e)||p.default.isElementAccessExpression(e)||p.default.isParenthesizedExpression(e)?E(e.expression):null}function z(e){return e===p.default.SyntaxKind.EqualsToken||e===p.default.SyntaxKind.PlusEqualsToken||e===p.default.SyntaxKind.MinusEqualsToken||e===p.default.SyntaxKind.AsteriskEqualsToken||e===p.default.SyntaxKind.SlashEqualsToken||e===p.default.SyntaxKind.PercentEqualsToken||e===p.default.SyntaxKind.AmpersandEqualsToken||e===p.default.SyntaxKind.BarEqualsToken||e===p.default.SyntaxKind.CaretEqualsToken||e===p.default.SyntaxKind.LessThanLessThanEqualsToken||e===p.default.SyntaxKind.GreaterThanGreaterThanEqualsToken||e===p.default.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken}var d=S(require("typescript"),1);function H(e,t){if(!e)return!1;let s=e.getProperty("then");if(!s)return!1;let r=s.valueDeclaration??s.declarations?.[0];return r?t.getTypeOfSymbolAtLocation(s,r).getCallSignatures().length>0:!1}function I(e){return d.default.isAwaitExpression(e.parent)}function F(e){return d.default.isReturnStatement(e.parent)}function U(e){if(!d.default.isPropertyAccessExpression(e.parent))return!1;let t=e.parent.name.text;return t==="then"||t==="catch"||t==="finally"}function j(e){if(!d.default.isPropertyAccessExpression(e.expression)||e.expression.name.text!=="forEach")return!1;let t=e.arguments[0];return t&&(d.default.isArrowFunction(t)||d.default.isFunctionExpression(t))?t.modifiers?.some(s=>s.kind===d.default.SyntaxKind.AsyncKeyword)??!1:!1}function J(e){if(!d.default.isPropertyAccessExpression(e.expression)||e.expression.name.text!=="addEventListener")return!1;let t=e.arguments[1];return t&&(d.default.isArrowFunction(t)||d.default.isFunctionExpression(t))?t.modifiers?.some(s=>s.kind===d.default.SyntaxKind.AsyncKeyword)??!1:!1}function V(e){return d.default.isPropertyAccessExpression(e.expression)&&d.default.isIdentifier(e.expression.expression)&&e.expression.expression.text==="Promise"&&e.expression.name.text==="all"}function Y(e,t){let s=e.getTypeChecker(),r=[];for(let o of t){let C=function(n){if(a.default.isBinaryExpression(n)&&z(n.operatorToken.kind)){let l=n.left,u=E(l);if(u&&A.has(u.text)&&c(`Mutates parameter \`${u.text}\``),a.default.isIdentifier(l)){let $=s.getSymbolAtLocation(l),v=$?.getDeclarations()??[],ee=v.some(te=>B(te,o.node));$&&v.length>0&&!ee&&c(`Writes to outer scope \`${l.text}\``)}a.default.isPropertyAccessExpression(l)&&l.expression.kind===a.default.SyntaxKind.ThisKeyword&&c(`Mutates \`this.${l.name.text}\``)}if(a.default.isCallExpression(n)&&a.default.isPropertyAccessExpression(n.expression)){let l=n.expression.name.text,u=E(n.expression.expression);u&&A.has(u.text)&&_.has(l)&&c(`Mutates parameter \`${u.text}\` via \`${l}()\``)}if(a.default.isIdentifier(n)){let l=s.getSymbolAtLocation(n);l&&W.has(l.getName())&&c(`Accesses global \`${l.getName()}\``)}if(a.default.isPropertyAccessExpression(n)){let l=E(n.expression);l?.text==="process"&&n.name.text==="env"&&c("Accesses `process.env` (non-deterministic)"),l?.text==="console"&&c("Writes to console")}if(a.default.isCallExpression(n)){let l=s.getTypeAtLocation(n),u=n.expression;a.default.isPropertyAccessExpression(u)&&a.default.isIdentifier(u.expression)&&u.expression.text==="Date"&&u.name.text==="now"&&c("Calls Date.now() (non-deterministic)"),a.default.isPropertyAccessExpression(u)&&a.default.isIdentifier(u.expression)&&u.expression.text==="Math"&&u.name.text==="random"&&c("Calls Math.random() (non-deterministic)"),H(l,s)&&!I(n)&&!F(n)&&!U(n)&&c("Floating Promise (result ignored)"),V(n)&&!I(n)&&!F(n)&&c("Unawaited Promise.all()"),j(n)&&c("Async callback inside forEach (not awaited)"),J(n)&&c("Async event listener without error boundary")}a.default.isNewExpression(n)&&a.default.isIdentifier(n.expression)&&n.expression.text==="Date"&&c("Instantiates Date (non-deterministic)"),a.default.isAwaitExpression(n)&&(P=!0),a.default.isTryStatement(n)&&n.catchClause&&(L=!0),a.default.isCatchClause(n)&&n.block.statements.length===0&&c("Empty catch block (error swallowed)"),a.default.forEachChild(n,C)};var i=C;let f=o.node.getSourceFile(),{line:x,character:g}=f.getLineAndCharacterOfPosition(o.node.getStart(f)),h=[],A=new Set;for(let n of o.node.parameters)a.default.isIdentifier(n.name)&&A.add(n.name.text);let c=n=>{h.includes(n)||h.push(n)},P=!1,L=!1,D=o.node.modifiers?.some(n=>n.kind===a.default.SyntaxKind.AsyncKeyword)??!1;C(o.node),D&&!P&&c("Async function without await"),D&&P&&!L&&c("Async function without error handling (no try/catch)"),r.push({id:o.id,name:o.name,file:o.file,line:x+1,character:g+1,isPure:h.length===0,reasons:h})}return r}function X(e,t){let s=new Map;for(let o of t)s.set(o.id,o);let r=new Map;for(let[o,f]of e.entries())for(let x of f)r.has(x)||r.set(x,new Set),r.get(x).add(o);let i=[];for(let o of t)o.isPure||i.push(o.id);for(;i.length>0;){let o=i.shift(),f=r.get(o);if(f)for(let x of f){let g=s.get(x);g&&g.isPure&&(g.isPure=!1,g.reasons.push("Calls impure function"),i.push(x))}}return Array.from(s.values())}var T=process.argv.slice(2),fe=T[0];fe!=="analyze"&&(console.log("Usage: callograph analyze [tsconfigPath] [--fail-on-impure]"),process.exit(1));var pe=T[1]&&!T[1].startsWith("--")?T[1]:"tsconfig.json",ue=T.includes("--fail-on-impure"),M=R(pe),b=q(M),Z=O(M,b),N=Y(M,b);N=X(Z,N);var Q=new Map(b.map(e=>[e.id,e]));console.log(`
2
+ "use strict";var pe=Object.create;var q=Object.defineProperty;var me=Object.getOwnPropertyDescriptor;var ye=Object.getOwnPropertyNames;var ge=Object.getPrototypeOf,de=Object.prototype.hasOwnProperty;var he=(e,t,s,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let a of ye(t))!de.call(e,a)&&a!==s&&q(e,a,{get:()=>t[a],enumerable:!(n=me(t,a))||n.enumerable});return e};var C=(e,t,s)=>(s=e!=null?pe(ge(e)):{},he(t||!e||!e.__esModule?q(s,"default",{value:e,enumerable:!0}):s,e));var D=C(require("fs"),1),oe=C(require("path"),1);var P=C(require("typescript"),1);function z(e){let t=P.default.readConfigFile(e,P.default.sys.readFile);if(t.error)throw new Error(P.default.flattenDiagnosticMessageText(t.error.messageText,`
3
+ `));let s=P.default.parseJsonConfigFileContent(t.config,P.default.sys,process.cwd());return P.default.createProgram({rootNames:s.fileNames,options:s.options})}var h=C(require("typescript"),1);function xe(e,t){let s=e;if(s.name&&h.default.isIdentifier(s.name))return s.name.text;if(s.name&&(h.default.isStringLiteral(s.name)||h.default.isNumericLiteral(s.name)))return String(s.name.text);let n=e.parent;if(n&&h.default.isVariableDeclaration(n)&&h.default.isIdentifier(n.name))return n.name.text;if(n&&h.default.isBinaryExpression(n)&&h.default.isPropertyAccessExpression(n.left))return n.left.name.text;let{line:a,character:o}=t.getLineAndCharacterOfPosition(e.getStart(t));return`<anonymous@${a+1}:${o+1}>`}function W(e,t=[]){let s=[],n=t.map(a=>a.replace(/\\/g,"/"));for(let a of e.getSourceFiles()){if(a.isDeclarationFile)continue;let o=a.fileName.replace(/\\/g,"/");n.some(l=>o.includes(l))||h.default.forEachChild(a,function l(c){let p=h.default.isFunctionDeclaration(c)&&!!c.name,m=h.default.isMethodDeclaration(c),g=c.parent,r=(h.default.isArrowFunction(c)||h.default.isFunctionExpression(c))&&!!g&&h.default.isVariableDeclaration(g)&&h.default.isIdentifier(g.name);if(p||m||r){let x=xe(c,a),i=`${o}:${c.pos}:${x}`;s.push({id:i,name:x,file:o,node:c})}h.default.forEachChild(c,l)})}return s}var $=C(require("typescript"),1);function Ae(e){return!e.includes("node_modules")&&!e.endsWith(".d.ts")}function _(e,t){let s=e.getTypeChecker(),n=new Map,a=new Map;for(let l of t)n.set(l.id,new Set),a.set(l.node,l.id);for(let l of t){let p=function(m){if($.default.isCallExpression(m)){let r=s.getResolvedSignature(m)?.getDeclaration();if(r&&Ae(r.getSourceFile().fileName)){let x=a.get(r);x&&n.get(c).add(x)}}$.default.forEachChild(m,p)};var o=p;let c=l.id;p(l.node)}return n}var y=C(require("typescript"),1);var B=new Set(["window","document","global","process","localStorage","sessionStorage"]),H=new Set(["push","pop","splice","shift","unshift","sort","reverse","copyWithin","fill"]);var d=C(require("typescript"),1);function j(e,t){return e.pos>=t.pos&&e.end<=t.end}function F(e){return d.default.isIdentifier(e)?e:d.default.isPropertyAccessExpression(e)||d.default.isElementAccessExpression(e)||d.default.isParenthesizedExpression(e)?F(e.expression):null}function J(e){return e===d.default.SyntaxKind.EqualsToken||e===d.default.SyntaxKind.PlusEqualsToken||e===d.default.SyntaxKind.MinusEqualsToken||e===d.default.SyntaxKind.AsteriskEqualsToken||e===d.default.SyntaxKind.SlashEqualsToken||e===d.default.SyntaxKind.PercentEqualsToken||e===d.default.SyntaxKind.AmpersandEqualsToken||e===d.default.SyntaxKind.BarEqualsToken||e===d.default.SyntaxKind.CaretEqualsToken||e===d.default.SyntaxKind.LessThanLessThanEqualsToken||e===d.default.SyntaxKind.GreaterThanGreaterThanEqualsToken||e===d.default.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken}function U(e,t){let s=e.getTypeChecker(),n=[];for(let o of t){let x=function(i){if(y.default.isBinaryExpression(i)&&J(i.operatorToken.kind)){let f=i.left,S=F(f);if(S&&g.has(S.text)&&r(`Mutates parameter \`${S.text}\``),y.default.isIdentifier(f)){let E=s.getSymbolAtLocation(f),v=E?.getDeclarations()??[],fe=v.some(ue=>j(ue,o.node));E&&v.length>0&&!fe&&r(`Writes to outer scope \`${f.text}\``)}y.default.isPropertyAccessExpression(f)&&f.expression.kind===y.default.SyntaxKind.ThisKeyword&&r(`Mutates \`this.${f.name.text}\``)}if(y.default.isCallExpression(i)&&y.default.isPropertyAccessExpression(i.expression)){let f=i.expression.name.text,S=F(i.expression.expression);S&&g.has(S.text)&&H.has(f)&&r(`Mutates parameter \`${S.text}\` via \`${f}()\``)}if(y.default.isIdentifier(i)){let f=s.getSymbolAtLocation(i);f&&B.has(f.getName())&&r(`Accesses global \`${f.getName()}\``)}if(y.default.isPropertyAccessExpression(i)){let f=F(i.expression);f?.text==="process"&&i.name.text==="env"&&r("Accesses `process.env` (non-deterministic)"),f?.text==="console"&&r("Writes to console")}if(y.default.isCallExpression(i)){let f=i.expression;y.default.isPropertyAccessExpression(f)&&y.default.isIdentifier(f.expression)&&f.expression.text==="Date"&&f.name.text==="now"&&r("Calls Date.now() (non-deterministic)"),y.default.isPropertyAccessExpression(f)&&y.default.isIdentifier(f.expression)&&f.expression.text==="Math"&&f.name.text==="random"&&r("Calls Math.random() (non-deterministic)")}y.default.isNewExpression(i)&&y.default.isIdentifier(i.expression)&&i.expression.text==="Date"&&r("Instantiates Date (non-deterministic)"),y.default.isCatchClause(i)&&i.block.statements.length===0&&r("Empty catch block (error swallowed)"),y.default.forEachChild(i,x)};var a=x;let l=o.node.getSourceFile(),{line:c,character:p}=l.getLineAndCharacterOfPosition(o.node.getStart(l)),m=[],g=new Set;for(let i of o.node.parameters)y.default.isIdentifier(i.name)&&g.add(i.name.text);let r=i=>{m.includes(i)||m.push(i)};x(o.node),n.push({id:o.id,name:o.name,file:o.file,line:c+1,character:p+1,isPure:m.length===0,reasons:m})}return n}function V(e,t){let s=new Map;for(let o of t)s.set(o.id,o);let n=new Map;for(let[o,l]of e.entries())for(let c of l)n.has(c)||n.set(c,new Set),n.get(c).add(o);let a=[];for(let o of t)o.isPure||a.push(o.id);for(;a.length>0;){let o=a.shift(),l=n.get(o);if(l)for(let c of l){let p=s.get(c);p&&p.isPure&&(p.isPure=!1,p.reasons.push("Calls impure function"),a.push(c))}}return Array.from(s.values())}var A=C(require("typescript"),1);var u=C(require("typescript"),1);function M(e,t){if(!e)return!1;let s=e.getProperty("then");if(!s)return!1;let n=s.valueDeclaration??s.declarations?.[0];return n?t.getTypeOfSymbolAtLocation(s,n).getCallSignatures().length>0:!1}function Se(e){let t=e.parent;if(!u.default.isPropertyAccessExpression(t))return!1;let s=t.name.text;return s==="then"||s==="catch"||s==="finally"}function R(e){let t=e.parent;return!!(u.default.isAwaitExpression(t)||u.default.isReturnStatement(t)||u.default.isVariableDeclaration(t)||u.default.isBinaryExpression(t)||u.default.isCallExpression(t)||Se(e))}var Ee=new Set(["forEach","map","filter","reduce"]);function Y(e){if(!u.default.isPropertyAccessExpression(e.expression))return!1;let t=e.expression.name.text;if(!Ee.has(t))return!1;let s=e.arguments[0];return s&&(u.default.isArrowFunction(s)||u.default.isFunctionExpression(s))?s.modifiers?.some(n=>n.kind===u.default.SyntaxKind.AsyncKeyword)??!1:!1}function Q(e){if(!u.default.isPropertyAccessExpression(e.expression)||e.expression.name.text!=="addEventListener")return!1;let t=e.arguments[1];return t&&(u.default.isArrowFunction(t)||u.default.isFunctionExpression(t))?t.modifiers?.some(s=>s.kind===u.default.SyntaxKind.AsyncKeyword)??!1:!1}function X(e){return u.default.isPropertyAccessExpression(e.expression)&&u.default.isIdentifier(e.expression.expression)&&e.expression.expression.text==="Promise"&&e.expression.name.text==="all"}function Z(e){return u.default.isForStatement(e)||u.default.isForOfStatement(e)||u.default.isForInStatement(e)||u.default.isWhileStatement(e)||u.default.isDoStatement(e)}function ee(e,t){let s=t.getTypeChecker(),n=[],a=[],o=!1,l=!1,c=!1,p=e.node.modifiers?.some(r=>r.kind===A.default.SyntaxKind.AsyncKeyword)??!1,m=(r,x)=>{n.push({message:r,severity:x})};function g(r,x=!1){if(Z(r)){A.default.forEachChild(r,i=>g(i,!0));return}if(A.default.isAwaitExpression(r)&&(l=!0),A.default.isTryStatement(r)&&r.catchClause&&(c=!0),A.default.isCallExpression(r)){let i=s.getTypeAtLocation(r);if(M(i,s)){o=!0,R(r)||m(x?"Floating promise inside loop":"Floating promise",x?"warning":"error");let E=s.getResolvedSignature(r)?.getDeclaration();if(E&&E.getSourceFile().fileName.includes(process.cwd())){let v=`${E.getSourceFile().fileName}:${E.pos}`;a.push(v)}}if(X(r)){R(r)||m("Unawaited Promise.all()","error");let f=r.arguments[0];if(f&&A.default.isArrayLiteralExpression(f))for(let S of f.elements){let E=s.getTypeAtLocation(S);M(E,s)&&(R(r)||m("Promise.all contains unhandled promise","error"))}}Y(r)&&m("Async callback inside array method","warning"),Q(r)&&m("Async event listener without boundary","warning")}if(A.default.isPropertyAccessExpression(r)&&r.name.text==="catch"&&A.default.isCallExpression(r.parent)){let i=r.parent.arguments[0];i&&(A.default.isArrowFunction(i)||A.default.isFunctionExpression(i))&&i.body&&A.default.isBlock(i.body)&&i.body.statements.length===0&&m("Error swallowed in .catch()","error")}A.default.forEachChild(r,i=>g(i,x))}return g(e.node),p&&l&&!c&&m("Async function contains await but has no error boundary (try/catch)","warning"),{issues:n,returnsPromise:o,callsReturningPromise:a}}function se(e,t){let s=[];for(let n of t){let a=n.node.getSourceFile(),{line:o,character:l}=a.getLineAndCharacterOfPosition(n.node.getStart()),c=ee(n,e);s.push({id:n.id,name:n.name,file:n.file,line:o+1,character:l+1,issues:c.issues})}return s}function te(e,t){let s=new Map(t.map(a=>[a.id,a])),n=!0;for(;n;){n=!1;for(let[a,o]of e.entries()){let l=s.get(a);if(l)for(let c of o){let p=s.get(c);if(!p)continue;p.issues.some(g=>g.message.includes("Floating")||g.message.includes("Promise"))&&(l.issues.some(r=>r.message==="Calls promise-returning function without await")||(l.issues.push({message:"Calls promise-returning function without await",severity:"error"}),n=!0))}}}return Array.from(s.values())}function ne(e,t){let s=new Map(t.map(o=>[o.id,o])),n=new Map;for(let[o,l]of e.entries())for(let c of l)n.has(c)||n.set(c,new Set),n.get(c).add(o);let a=t.filter(o=>o.issues.some(l=>l.severity==="error")).map(o=>o.id);for(;a.length;){let o=a.shift(),l=n.get(o);if(l)for(let c of l){let p=s.get(c);if(!p)continue;p.issues.some(g=>g.message==="Calls async-unsafe function")||(p.issues.push({message:"Calls async-unsafe function",severity:"error"}),a.push(c))}}return Array.from(s.values())}var w=process.argv.slice(2),L=w[0];function ie(){console.log(`
4
+ Callograph \u2014 Structural TypeScript Analyzer
5
+
6
+ Usage:
7
+ callograph analyze [tsconfigPath]
8
+
9
+ Options:
10
+ --fail-on-impure Exit with code 1 if impurity detected
11
+ --fail-on-async Exit with code 1 if async issues detected
12
+ --async-severity=error|warning|info
13
+ Minimum async severity to report (default: error)
14
+ --ignore=pattern1,pattern2 Comma-separated path substrings to ignore
15
+ --json Output structured JSON
16
+ --help Show this help message
17
+ `)}(!L||L==="--help")&&(ie(),process.exit(0));L!=="analyze"&&(ie(),process.exit(1));var Ce=w[1]&&!w[1].startsWith("--")?w[1]:"tsconfig.json",we=w.includes("--fail-on-impure"),Pe=w.includes("--fail-on-async"),Fe=w.includes("--json"),Te=w.find(e=>e.startsWith("--async-severity=")),ae=Te?.split("=")[1]??"error",b={error:3,warning:2,info:1},re=w.find(e=>e.startsWith("--ignore=")),Ie=re?re.split("=")[1].split(","):[],ce=["node_modules"];try{let e=oe.default.join(process.cwd(),"package.json");D.default.existsSync(e)&&JSON.parse(D.default.readFileSync(e,"utf-8")).name==="callograph"&&ce.push("src/analyzer","src/cli","dist")}catch{}var ve=[...ce,...Ie],k=z(Ce),K=W(k).filter(e=>!ve.some(t=>e.file.includes(t))),I=_(k,K),N=U(k,K);N=V(I,N);var T=se(k,K);T=te(I,T);T=ne(I,T);if(Fe){let e={callGraph:Array.from(I.entries()).map(([t,s])=>({caller:t,callees:Array.from(s)})),purity:N,async:T};console.log(JSON.stringify(e,null,2)),process.exit(0)}console.log(`
4
18
  Call Graph:
5
- `);var k=0;for(let[e,t]of Z.entries()){let s=Q.get(e),r=s?s.name:e;for(let i of t){let o=Q.get(i),f=o?o.name:i;console.log(`${r} \u2192 ${f}`),k++}}k===0&&console.log("(no project-level call edges found)");console.log(`
6
- Total call edges: ${k}`);console.log(`
19
+ `);var le=0;for(let[e,t]of I.entries())for(let s of t)console.log(`${e} \u2192 ${s}`),le++;console.log(`
20
+ Total call edges: ${le}`);console.log(`
7
21
  Purity Report:
8
- `);var K=N.filter(e=>!e.isPure);if(K.length===0)console.log("All analyzed functions are pure.");else for(let e of K){console.log(`${e.file}:${e.line}:${e.character}`),console.log(`Function: ${e.name}`),console.log("Status: X Impure"),console.log("Reasons:");for(let t of e.reasons)console.log(` - ${t}`);console.log("")}ue&&K.length>0&&process.exit(1);
22
+ `);var G=N.filter(e=>!e.isPure);if(G.length===0)console.log("All analyzed functions are pure.");else for(let e of G){console.log(`${e.file}:${e.line}:${e.character}`),console.log(`Function: ${e.name}`);for(let t of e.reasons)console.log(` - ${t}`);console.log("")}we&&G.length>0&&process.exit(1);function Re(e){let t=new Map;for(let s of e){let n=`${s.severity}|${s.message}`;t.has(n)?t.get(n).count++:t.set(n,{severity:s.severity,count:1})}return Array.from(t.entries()).map(([s,n])=>{let[,a]=s.split("|");return{message:a,severity:n.severity,count:n.count}})}console.log(`
23
+ Async Safety Report:
24
+ `);var O=T.filter(e=>e.issues.some(t=>b[t.severity]>=b[ae]));if(O.length===0)console.log("No async issues detected.");else for(let e of O){console.log(`${e.file}:${e.line}:${e.character}`),console.log(`Function: ${e.name}`);let t=Re(e.issues);for(let s of t)if(b[s.severity]>=b[ae]){let n=s.count>1?` (${s.count} occurrences)`:"";console.log(` [${s.severity.toUpperCase()}] ${s.message}${n}`)}console.log("")}Pe&&O.length>0&&process.exit(1);
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";var s=Object.defineProperty;var c=Object.getOwnPropertyDescriptor;var i=Object.getOwnPropertyNames;var p=Object.prototype.hasOwnProperty;var v=(e,o)=>{for(var r in o)s(e,r,{get:o[r],enumerable:!0})},x=(e,o,r,t)=>{if(o&&typeof o=="object"||typeof o=="function")for(let n of i(o))!p.call(e,n)&&n!==r&&s(e,n,{get:()=>o[n],enumerable:!(t=c(o,n))||t.enumerable});return e};var a=e=>x(s({},"__esModule",{value:!0}),e);var d={};v(d,{version:()=>b});module.exports=a(d);var b="0.0.2";0&&(module.exports={version});
1
+ "use strict";var s=Object.defineProperty;var c=Object.getOwnPropertyDescriptor;var i=Object.getOwnPropertyNames;var p=Object.prototype.hasOwnProperty;var v=(e,o)=>{for(var r in o)s(e,r,{get:o[r],enumerable:!0})},x=(e,o,r,t)=>{if(o&&typeof o=="object"||typeof o=="function")for(let n of i(o))!p.call(e,n)&&n!==r&&s(e,n,{get:()=>o[n],enumerable:!(t=c(o,n))||t.enumerable});return e};var a=e=>x(s({},"__esModule",{value:!0}),e);var d={};v(d,{version:()=>b});module.exports=a(d);var b="1.0.0";0&&(module.exports={version});
package/dist/index.d.cts CHANGED
@@ -1,3 +1,3 @@
1
- declare const version = "0.0.2";
1
+ declare const version = "1.0.0";
2
2
 
3
3
  export { version };
package/package.json CHANGED
@@ -1,51 +1,41 @@
1
1
  {
2
2
  "name": "callograph",
3
- "version": "0.0.8",
3
+ "version": "1.0.0",
4
4
  "description": "TypeScript call graph and side-effect analyzer.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
-
8
7
  "main": "./dist/index.cjs",
9
8
  "module": "./dist/index.js",
10
9
  "types": "./dist/index.d.ts",
11
-
12
10
  "files": [
13
11
  "dist",
14
12
  "README.md",
15
13
  "LICENSE"
16
14
  ],
17
-
18
15
  "scripts": {
19
16
  "build": "tsup",
20
17
  "dev": "tsup --watch",
21
18
  "prepublishOnly": "npm run build"
22
19
  },
23
-
24
20
  "dependencies": {
25
21
  "typescript": "^5.9.3"
26
22
  },
27
-
28
23
  "devDependencies": {
29
24
  "@types/node": "^25.2.3",
30
25
  "tsup": "^8.5.1"
31
26
  },
32
-
33
27
  "bin": {
34
28
  "callograph": "dist/cli.cjs"
35
29
  },
36
-
37
30
  "engines": {
38
31
  "node": ">=18"
39
32
  },
40
-
41
33
  "repository": {
42
34
  "type": "git",
43
35
  "url": "git+https://github.com/miraclemarcel/callograph.git"
44
36
  },
45
-
46
37
  "bugs": {
47
38
  "url": "https://github.com/miraclemarcel/callograph/issues"
48
39
  },
49
-
50
40
  "homepage": "https://github.com/miraclemarcel/callograph#readme"
51
41
  }