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 +276 -121
- package/dist/cli.cjs +21 -5
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/package.json +1 -11
package/README.md
CHANGED
|
@@ -1,40 +1,49 @@
|
|
|
1
|
+
|
|
2
|
+
---
|
|
3
|
+
|
|
1
4
|
# Callograph
|
|
2
5
|
|
|
3
|
-
**Callograph** is a static analysis
|
|
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
|
|
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
|
-
##
|
|
14
|
+
## 🚀 What Callograph Does
|
|
10
15
|
|
|
11
|
-
|
|
16
|
+
Callograph analyzes your entire TypeScript program and answers questions like:
|
|
12
17
|
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
-
|
|
26
|
+
It provides structural reasoning about your system.
|
|
21
27
|
|
|
22
28
|
---
|
|
23
29
|
|
|
24
|
-
|
|
30
|
+
# Core Capabilities (v1.0.0)
|
|
25
31
|
|
|
26
|
-
|
|
32
|
+
---
|
|
27
33
|
|
|
28
|
-
|
|
34
|
+
## 1. Program-Level Call Graph Construction
|
|
29
35
|
|
|
30
|
-
|
|
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
|
|
35
|
-
* Method calls (
|
|
42
|
+
* Imported symbol resolution
|
|
43
|
+
* Method calls (when statically resolvable)
|
|
44
|
+
* Interprocedural call edges
|
|
36
45
|
|
|
37
|
-
Example
|
|
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
|
-
|
|
60
|
+
## 2. Function Purity Analysis
|
|
61
|
+
|
|
62
|
+
Callograph determines whether a function is pure or impure based on structural rules.
|
|
50
63
|
|
|
51
|
-
|
|
64
|
+
It detects:
|
|
52
65
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
*
|
|
56
|
-
*
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
- Calls impure function `fetchTaxRate`
|
|
84
|
+
```
|
|
85
|
+
❌ Writes to outer scope `cache`
|
|
68
86
|
```
|
|
69
87
|
|
|
70
88
|
---
|
|
71
89
|
|
|
72
|
-
###
|
|
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
|
|
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
|
-
|
|
122
|
+
Callograph propagates impurity across the call graph using reverse-graph traversal and fixed-point propagation.
|
|
88
123
|
|
|
89
|
-
|
|
124
|
+
This makes impurity structural, not local.
|
|
90
125
|
|
|
91
126
|
---
|
|
92
127
|
|
|
93
|
-
|
|
128
|
+
## 4. Enterprise Async Safety Engine
|
|
94
129
|
|
|
95
|
-
|
|
130
|
+
Callograph includes a dedicated async analysis engine, separate from purity.
|
|
96
131
|
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
154
|
+
### Promise.all Structural Analysis
|
|
109
155
|
|
|
110
156
|
Detects:
|
|
111
157
|
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
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
|
-
|
|
120
|
-
updateUser(user) → user.name = "new"
|
|
177
|
+
⚠️ Async callback inside array method
|
|
121
178
|
```
|
|
122
179
|
|
|
123
180
|
---
|
|
124
181
|
|
|
125
|
-
###
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
###
|
|
235
|
+
### Severity Model
|
|
236
|
+
|
|
237
|
+
Async issues are categorized:
|
|
238
|
+
|
|
239
|
+
* `error`
|
|
240
|
+
* `warning`
|
|
241
|
+
* `info`
|
|
136
242
|
|
|
137
|
-
|
|
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
|
-
|
|
261
|
+
You can enforce architectural constraints in pipelines.
|
|
144
262
|
|
|
145
263
|
---
|
|
146
264
|
|
|
147
|
-
##
|
|
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
|
-
|
|
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
|
-
```
|
|
156
|
-
|
|
291
|
+
```
|
|
292
|
+
--ignore=src/generated,dist
|
|
157
293
|
```
|
|
158
294
|
|
|
159
295
|
---
|
|
160
296
|
|
|
161
|
-
|
|
297
|
+
# CLI Usage
|
|
162
298
|
|
|
163
|
-
Analyze project
|
|
299
|
+
Analyze project:
|
|
164
300
|
|
|
165
|
-
```
|
|
301
|
+
```
|
|
166
302
|
callograph analyze
|
|
167
303
|
```
|
|
168
304
|
|
|
169
|
-
|
|
305
|
+
Custom tsconfig:
|
|
170
306
|
|
|
171
|
-
```
|
|
172
|
-
callograph analyze
|
|
307
|
+
```
|
|
308
|
+
callograph analyze tsconfig.build.json
|
|
173
309
|
```
|
|
174
310
|
|
|
175
|
-
Fail
|
|
311
|
+
Fail on impurity:
|
|
176
312
|
|
|
177
|
-
```
|
|
313
|
+
```
|
|
178
314
|
callograph analyze --fail-on-impure
|
|
179
315
|
```
|
|
180
316
|
|
|
181
|
-
|
|
317
|
+
Fail on async violations:
|
|
182
318
|
|
|
183
|
-
```
|
|
184
|
-
callograph analyze --
|
|
319
|
+
```
|
|
320
|
+
callograph analyze --fail-on-async
|
|
185
321
|
```
|
|
186
322
|
|
|
187
|
-
|
|
323
|
+
Adjust async severity threshold:
|
|
188
324
|
|
|
189
|
-
|
|
325
|
+
```
|
|
326
|
+
callograph analyze --async-severity=warning
|
|
327
|
+
```
|
|
190
328
|
|
|
191
|
-
|
|
329
|
+
JSON output:
|
|
192
330
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
* AST traversal via `ts.forEachChild`
|
|
197
|
-
* Impurity propagation via call graph traversal
|
|
331
|
+
```
|
|
332
|
+
callograph analyze --json
|
|
333
|
+
```
|
|
198
334
|
|
|
199
|
-
|
|
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
|
-
|
|
348
|
+
Build call graph (TypeChecker-based resolution)
|
|
207
349
|
↓
|
|
208
|
-
|
|
350
|
+
Purity analysis
|
|
209
351
|
↓
|
|
210
|
-
|
|
352
|
+
Impurity propagation
|
|
211
353
|
↓
|
|
212
|
-
|
|
354
|
+
Async flow analysis
|
|
213
355
|
↓
|
|
214
|
-
|
|
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
|
-
|
|
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
|
-
|
|
369
|
+
Callograph is designed for:
|
|
225
370
|
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
* Clear reasoning in output
|
|
371
|
+
* Structural clarity
|
|
372
|
+
* Deterministic reasoning
|
|
229
373
|
* Minimal false positives
|
|
230
|
-
*
|
|
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
|
-
|
|
384
|
+
# Current Limitations
|
|
235
385
|
|
|
236
|
-
* Dynamic runtime dispatch cannot always be resolved
|
|
237
|
-
* Higher-order
|
|
238
|
-
*
|
|
239
|
-
* No
|
|
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
|
-
|
|
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
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
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
|
|
407
|
+
* Monorepo project reference support
|
|
252
408
|
|
|
253
409
|
---
|
|
254
410
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
MIT
|
|
411
|
+
# Versioning
|
|
258
412
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
## Contributing
|
|
413
|
+
The current release introduces:
|
|
262
414
|
|
|
263
|
-
|
|
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
|
-
|
|
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
|
-
|
|
425
|
+
|
|
426
|
+
# Vision
|
|
272
427
|
|
|
273
428
|
Callograph aims to become:
|
|
274
429
|
|
|
275
|
-
> A structural analysis
|
|
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
|
|
3
|
-
`));let s=
|
|
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
|
|
6
|
-
Total call edges: ${
|
|
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
|
|
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
|
|
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
package/package.json
CHANGED
|
@@ -1,51 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "callograph",
|
|
3
|
-
"version": "0.0
|
|
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
|
}
|