blast-radius-analyzer 1.2.1

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.
Files changed (49) hide show
  1. package/README.md +108 -0
  2. package/TEST-REPORT.md +379 -0
  3. package/dist/core/AnalysisCache.d.ts +59 -0
  4. package/dist/core/AnalysisCache.js +156 -0
  5. package/dist/core/BlastRadiusAnalyzer.d.ts +99 -0
  6. package/dist/core/BlastRadiusAnalyzer.js +510 -0
  7. package/dist/core/CallStackBuilder.d.ts +63 -0
  8. package/dist/core/CallStackBuilder.js +269 -0
  9. package/dist/core/DataFlowAnalyzer.d.ts +215 -0
  10. package/dist/core/DataFlowAnalyzer.js +1115 -0
  11. package/dist/core/DependencyGraph.d.ts +55 -0
  12. package/dist/core/DependencyGraph.js +541 -0
  13. package/dist/core/ImpactTracer.d.ts +96 -0
  14. package/dist/core/ImpactTracer.js +398 -0
  15. package/dist/core/PropagationTracker.d.ts +73 -0
  16. package/dist/core/PropagationTracker.js +502 -0
  17. package/dist/core/PropertyAccessTracker.d.ts +56 -0
  18. package/dist/core/PropertyAccessTracker.js +281 -0
  19. package/dist/core/SymbolAnalyzer.d.ts +139 -0
  20. package/dist/core/SymbolAnalyzer.js +608 -0
  21. package/dist/core/TypeFlowAnalyzer.d.ts +120 -0
  22. package/dist/core/TypeFlowAnalyzer.js +654 -0
  23. package/dist/core/TypePropagationAnalyzer.d.ts +58 -0
  24. package/dist/core/TypePropagationAnalyzer.js +269 -0
  25. package/dist/index.d.ts +13 -0
  26. package/dist/index.js +952 -0
  27. package/dist/types.d.ts +102 -0
  28. package/dist/types.js +5 -0
  29. package/package.json +39 -0
  30. package/src/core/AnalysisCache.ts +189 -0
  31. package/src/core/CallStackBuilder.ts +345 -0
  32. package/src/core/DataFlowAnalyzer.ts +1403 -0
  33. package/src/core/DependencyGraph.ts +584 -0
  34. package/src/core/ImpactTracer.ts +521 -0
  35. package/src/core/PropagationTracker.ts +630 -0
  36. package/src/core/PropertyAccessTracker.ts +349 -0
  37. package/src/core/SymbolAnalyzer.ts +746 -0
  38. package/src/core/TypeFlowAnalyzer.ts +844 -0
  39. package/src/core/TypePropagationAnalyzer.ts +332 -0
  40. package/src/index.ts +1071 -0
  41. package/src/types.ts +163 -0
  42. package/test-cases/.blast-radius-cache/file-states.json +14 -0
  43. package/test-cases/config.ts +13 -0
  44. package/test-cases/consumer.ts +12 -0
  45. package/test-cases/nested.ts +25 -0
  46. package/test-cases/simple.ts +62 -0
  47. package/test-cases/tsconfig.json +11 -0
  48. package/test-cases/user.ts +32 -0
  49. package/tsconfig.json +16 -0
@@ -0,0 +1,1403 @@
1
+ /**
2
+ * Commercial-Grade Data Flow Analyzer
3
+ *
4
+ * Implements:
5
+ * - Interprocedural data flow analysis (cross-function tracking)
6
+ * - Control flow sensitivity (branches, loops, exceptions)
7
+ * - Path sensitivity (different branches = different type states)
8
+ * - Context sensitivity (same function, different call sites = different types)
9
+ * - Worklist algorithm with fixed-point computation
10
+ * - Lattice-based abstract interpretation
11
+ * - Symbolic execution for branch conditions
12
+ * - Points-to analysis for reference tracking
13
+ * - Taint analysis for security-sensitive data
14
+ * - Escape analysis for closure/global escape
15
+ */
16
+
17
+ import * as ts from 'typescript';
18
+ import * as path from 'path';
19
+
20
+ // === LATTICE TYPES ===
21
+
22
+ /**
23
+ * Abstract value in the data flow lattice
24
+ */
25
+ export interface AbstractValue {
26
+ /** Type representation */
27
+ type: string;
28
+ /** Possible values (constants) */
29
+ constants: Set<string>;
30
+ /** Is null/undefined possible */
31
+ nullable: boolean;
32
+ /** Property types if object */
33
+ properties: Map<string, AbstractValue>;
34
+ /** Array element type if array */
35
+ elementType?: AbstractValue;
36
+ /** Is this value tainted (user input, etc.) */
37
+ tainted: boolean;
38
+ /** Where did this value escape (closure, global, return) */
39
+ escapes: Set<'closure' | 'global' | 'parameter' | 'return'>;
40
+ }
41
+
42
+ /**
43
+ * Data flow fact at a program point
44
+ */
45
+ export interface DataFlowFact {
46
+ /** Variable name -> abstract value */
47
+ env: Map<string, AbstractValue>;
48
+ /** Type constraints */
49
+ constraints: TypeConstraint[];
50
+ /** Path condition (branch predicates) */
51
+ pathCondition: PathCondition[];
52
+ }
53
+
54
+ /**
55
+ * Type constraint
56
+ */
57
+ export interface TypeConstraint {
58
+ variable: string;
59
+ predicate: string;
60
+ thenTypes?: Map<string, AbstractValue>;
61
+ elseTypes?: Map<string, AbstractValue>;
62
+ }
63
+
64
+ /**
65
+ * Path condition from branch predicates
66
+ */
67
+ export interface PathCondition {
68
+ expression: string;
69
+ /** true = then branch, false = else branch */
70
+ polarity: boolean;
71
+ }
72
+
73
+ /**
74
+ * Basic block in CFG
75
+ */
76
+ interface BasicBlock {
77
+ id: string;
78
+ statements: ts.Statement[];
79
+ predecessors: string[];
80
+ successors: string[];
81
+ /** Branch info if this block ends with a branch */
82
+ branch?: {
83
+ condition: ts.Expression;
84
+ trueTarget: string;
85
+ falseTarget: string;
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Control Flow Graph
91
+ */
92
+ interface ControlFlowGraph {
93
+ blocks: Map<string, BasicBlock>;
94
+ entryBlock: string;
95
+ exitBlock: string;
96
+ }
97
+
98
+ /**
99
+ * Call site information
100
+ */
101
+ interface CallSite {
102
+ callee: ts.FunctionDeclaration | ts.ArrowFunction | ts.MethodDeclaration;
103
+ arguments: Map<string, DataFlowFact>;
104
+ returnVariable?: string;
105
+ callExpression: ts.CallExpression;
106
+ }
107
+
108
+ /**
109
+ * Analysis result with full flow paths
110
+ */
111
+ export interface DataFlowResult {
112
+ hasDataLeaks: boolean;
113
+ flowPaths: FlowPath[];
114
+ taintedPaths: TaintedPath[];
115
+ typeNarrowing: Map<string, { line: number; types: string[] }[]>;
116
+ statistics: {
117
+ nodesAnalyzed: number;
118
+ blocksConstructed: number;
119
+ callSitesAnalyzed: number;
120
+ fixedPointIterations: number;
121
+ constraintsGenerated: number;
122
+ typesNarrowed: number;
123
+ promiseUnwraps: number;
124
+ conditionalBranches: number;
125
+ escapedValues: number;
126
+ taintedValues: number;
127
+ pathsTracked: number;
128
+ };
129
+ confidence: 'high' | 'medium' | 'low';
130
+ duration: number;
131
+ /** All facts at exit of each block */
132
+ finalFacts: Map<string, DataFlowFact>;
133
+ }
134
+
135
+ export interface FlowPath {
136
+ source: string;
137
+ sink: string;
138
+ path: string[];
139
+ typeAtSink: string;
140
+ typeAtSource: string;
141
+ isTainted: boolean;
142
+ }
143
+
144
+ export interface TaintedPath {
145
+ source: string;
146
+ sink: string;
147
+ taintSource: 'user-input' | 'file-read' | 'network' | 'environment';
148
+ path: string[];
149
+ }
150
+
151
+ export class DataFlowAnalyzer {
152
+ private program: ts.Program;
153
+ private checker: ts.TypeChecker;
154
+ private sourceFiles: ts.SourceFile[] = [];
155
+ private cfgCache: Map<string, ControlFlowGraph> = new Map();
156
+ private worklist: string[] = [];
157
+ private analyzedCallSites: Set<string> = new Set();
158
+
159
+ // Analysis options
160
+ private maxIterations = 100;
161
+ private maxCallDepth = 5;
162
+ private trackTaint = true;
163
+ private trackEscapes = true;
164
+
165
+ constructor(projectRoot: string, tsConfigPath: string) {
166
+ const configFile = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
167
+ const parsedConfig = ts.parseJsonConfigFileContent(
168
+ configFile.config,
169
+ ts.sys,
170
+ path.dirname(tsConfigPath)
171
+ );
172
+ this.program = ts.createProgram(parsedConfig.fileNames, parsedConfig.options);
173
+ this.checker = this.program.getTypeChecker();
174
+ this.sourceFiles = this.program.getSourceFiles().filter(
175
+ sf => !sf.fileName.includes('node_modules')
176
+ );
177
+ }
178
+
179
+ /**
180
+ * MAIN ENTRY POINT - Full interprocedural data flow analysis
181
+ */
182
+ analyzeDataFlow(functionName: string, functionFile: string): DataFlowResult {
183
+ const startTime = Date.now();
184
+ const stats = {
185
+ nodesAnalyzed: 0,
186
+ blocksConstructed: 0,
187
+ callSitesAnalyzed: 0,
188
+ fixedPointIterations: 0,
189
+ constraintsGenerated: 0,
190
+ typesNarrowed: 0,
191
+ promiseUnwraps: 0,
192
+ conditionalBranches: 0,
193
+ escapedValues: 0,
194
+ taintedValues: 0,
195
+ pathsTracked: 0,
196
+ };
197
+
198
+ const finalFacts = new Map<string, DataFlowFact>();
199
+ const flowPaths: FlowPath[] = [];
200
+ const taintedPaths: TaintedPath[] = [];
201
+ const typeNarrowing = new Map<string, { line: number; types: string[] }[]>();
202
+
203
+ // 1. Find the target function
204
+ const targetFunc = this.findFunction(functionName, functionFile);
205
+ if (!targetFunc) {
206
+ return this.createEmptyResult('low', stats, Date.now() - startTime);
207
+ }
208
+
209
+ // 2. Build CFG for the function
210
+ const cfg = this.buildCFG(targetFunc);
211
+ stats.blocksConstructed = cfg.blocks.size;
212
+
213
+ // 3. Initialize entry fact (empty environment with parameters)
214
+ const entryFact = this.createEntryFact(targetFunc);
215
+
216
+ // 4. Run worklist algorithm with lattice-based fixed-point computation
217
+ const blockFacts = this.runWorklistAnalysis(cfg, entryFact, stats);
218
+
219
+ // 5. Extract flow paths from final facts
220
+ for (const [blockId, fact] of blockFacts) {
221
+ finalFacts.set(blockId, fact);
222
+
223
+ // Collect type narrowing
224
+ for (const [varName, value] of fact.env) {
225
+ if (value.constants.size > 1) {
226
+ if (!typeNarrowing.has(varName)) {
227
+ typeNarrowing.set(varName, []);
228
+ }
229
+ typeNarrowing.get(varName)!.push({
230
+ line: 0,
231
+ types: Array.from(value.constants),
232
+ });
233
+ stats.typesNarrowed++;
234
+ }
235
+ }
236
+
237
+ // Collect tainted paths
238
+ if (this.trackTaint) {
239
+ for (const [varName, value] of fact.env) {
240
+ if (value.tainted) {
241
+ stats.taintedValues++;
242
+ taintedPaths.push({
243
+ source: `tainted:${varName}`,
244
+ sink: `${varName} at block ${blockId}`,
245
+ taintSource: 'user-input',
246
+ path: [varName],
247
+ });
248
+ }
249
+ }
250
+ }
251
+
252
+ // Collect escapes
253
+ if (this.trackEscapes) {
254
+ for (const [varName, value] of fact.env) {
255
+ if (value.escapes.size > 0) {
256
+ stats.escapedValues++;
257
+ }
258
+ }
259
+ }
260
+ }
261
+
262
+ // 6. Analyze Promise/async patterns
263
+ this.analyzeAsyncPatterns(targetFunc, flowPaths, stats);
264
+
265
+ // 7. Check for data leaks (tainted -> return/escape)
266
+ const hasDataLeaks = this.checkDataLeaks(flowPaths, taintedPaths);
267
+
268
+ return {
269
+ hasDataLeaks,
270
+ flowPaths,
271
+ taintedPaths,
272
+ typeNarrowing,
273
+ statistics: stats,
274
+ confidence: this.calculateConfidence(stats),
275
+ duration: Date.now() - startTime,
276
+ finalFacts,
277
+ };
278
+ }
279
+
280
+ /**
281
+ * Find function declaration
282
+ */
283
+ private findFunction(name: string, inFile: string): ts.FunctionDeclaration | ts.ArrowFunction | null {
284
+ const resolvedPath = path.resolve(inFile);
285
+
286
+ for (const sf of this.sourceFiles) {
287
+ if (!sf.fileName.includes(path.dirname(resolvedPath))) continue;
288
+
289
+ let result: ts.FunctionDeclaration | ts.ArrowFunction | null = null;
290
+
291
+ const visit = (node: ts.Node): void => {
292
+ if (result) return;
293
+
294
+ if (ts.isFunctionDeclaration(node) && node.name?.text === name) {
295
+ result = node;
296
+ } else if (ts.isArrowFunction(node)) {
297
+ const parent = node.parent;
298
+ if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name) && parent.name.text === name) {
299
+ result = node;
300
+ }
301
+ }
302
+
303
+ ts.forEachChild(node, visit);
304
+ };
305
+
306
+ visit(sf);
307
+ if (result) return result;
308
+ }
309
+
310
+ return null;
311
+ }
312
+
313
+ /**
314
+ * Build Control Flow Graph with basic blocks
315
+ */
316
+ private buildCFG(func: ts.FunctionDeclaration | ts.ArrowFunction): ControlFlowGraph {
317
+ const cacheKey = `${func.getSourceFile().fileName}:${func.getStart()}`;
318
+ if (this.cfgCache.has(cacheKey)) {
319
+ return this.cfgCache.get(cacheKey)!;
320
+ }
321
+
322
+ const blocks = new Map<string, BasicBlock>();
323
+ let blockId = 0;
324
+
325
+ const createBlock = (): BasicBlock => {
326
+ const id = `block_${blockId++}`;
327
+ const block: BasicBlock = {
328
+ id,
329
+ statements: [],
330
+ predecessors: [],
331
+ successors: [],
332
+ };
333
+ blocks.set(id, block);
334
+ return block;
335
+ };
336
+
337
+ // Entry block
338
+ const entryBlock = createBlock();
339
+ let currentBlock = entryBlock;
340
+
341
+ // Process function body
342
+ const body = func.body;
343
+ if (!body) {
344
+ const exitBlock = createBlock();
345
+ currentBlock.successors.push(exitBlock.id);
346
+ exitBlock.predecessors.push(currentBlock.id);
347
+ const cfg = { blocks, entryBlock: entryBlock.id, exitBlock: exitBlock.id };
348
+ this.cfgCache.set(cacheKey, cfg);
349
+ return cfg;
350
+ }
351
+
352
+ // Traverse and create blocks
353
+ const processStatement = (stmt: ts.Statement): void => {
354
+ // Handle if statement (creates branches)
355
+ if (ts.isIfStatement(stmt)) {
356
+ // Create branch blocks
357
+ const thenBlock = createBlock();
358
+ const elseBlock = createBlock();
359
+ const mergeBlock = createBlock();
360
+
361
+ // Set up branch info
362
+ currentBlock.branch = {
363
+ condition: stmt.expression,
364
+ trueTarget: thenBlock.id,
365
+ falseTarget: elseBlock.id,
366
+ };
367
+ currentBlock.successors.push(thenBlock.id, elseBlock.id);
368
+ thenBlock.predecessors.push(currentBlock.id);
369
+ elseBlock.predecessors.push(currentBlock.id);
370
+
371
+ // Process then statement
372
+ currentBlock = thenBlock;
373
+ if (ts.isStatement(stmt.thenStatement)) {
374
+ processStatement(stmt.thenStatement);
375
+ } else if (ts.isStatement(stmt.thenStatement)) {
376
+ currentBlock.statements.push(stmt.thenStatement);
377
+ }
378
+
379
+ // Add merge as successor
380
+ if (!currentBlock.successors.includes(mergeBlock.id)) {
381
+ currentBlock.successors.push(mergeBlock.id);
382
+ mergeBlock.predecessors.push(currentBlock.id);
383
+ }
384
+
385
+ // Process else statement
386
+ if (stmt.elseStatement) {
387
+ currentBlock = elseBlock;
388
+ if (ts.isStatement(stmt.elseStatement)) {
389
+ processStatement(stmt.elseStatement);
390
+ } else if (ts.isStatement(stmt.elseStatement)) {
391
+ currentBlock.statements.push(stmt.elseStatement);
392
+ }
393
+
394
+ if (!currentBlock.successors.includes(mergeBlock.id)) {
395
+ currentBlock.successors.push(mergeBlock.id);
396
+ mergeBlock.predecessors.push(currentBlock.id);
397
+ }
398
+ } else {
399
+ // No else - add edge from else block to merge
400
+ elseBlock.successors.push(mergeBlock.id);
401
+ mergeBlock.predecessors.push(elseBlock.id);
402
+ }
403
+
404
+ currentBlock = mergeBlock;
405
+ return;
406
+ }
407
+
408
+ // Handle while/do-while/for loops
409
+ if (ts.isWhileStatement(stmt) || ts.isForStatement(stmt) || ts.isForInStatement(stmt) || ts.isForOfStatement(stmt)) {
410
+ const loopHeader = createBlock();
411
+ const loopBody = createBlock();
412
+ const loopExit = createBlock();
413
+
414
+ currentBlock.successors.push(loopHeader.id);
415
+ loopHeader.predecessors.push(currentBlock.id);
416
+
417
+ currentBlock = loopHeader;
418
+ let loopCondition: ts.Expression | undefined;
419
+ if (ts.isWhileStatement(stmt)) {
420
+ loopCondition = stmt.expression;
421
+ } else if (ts.isForStatement(stmt)) {
422
+ loopCondition = stmt.condition;
423
+ } else if (ts.isForInStatement(stmt)) {
424
+ loopCondition = stmt.expression;
425
+ } else if (ts.isForOfStatement(stmt)) {
426
+ loopCondition = stmt.expression;
427
+ }
428
+ if (!loopCondition) return;
429
+ currentBlock.branch = {
430
+ condition: loopCondition,
431
+ trueTarget: loopBody.id,
432
+ falseTarget: loopExit.id,
433
+ };
434
+ currentBlock.successors.push(loopBody.id, loopExit.id);
435
+ loopBody.predecessors.push(currentBlock.id);
436
+
437
+ currentBlock = loopBody;
438
+ processStatement(stmt.statement);
439
+ currentBlock.successors.push(loopHeader.id);
440
+ loopHeader.predecessors.push(currentBlock.id);
441
+
442
+ currentBlock = loopExit;
443
+ return;
444
+ }
445
+
446
+ // Handle try-catch-finally
447
+ if (ts.isTryStatement(stmt)) {
448
+ const tryBlock = createBlock();
449
+ const catchBlock = createBlock();
450
+ const finallyBlock = createBlock();
451
+
452
+ currentBlock.successors.push(tryBlock.id);
453
+ tryBlock.predecessors.push(currentBlock.id);
454
+
455
+ currentBlock = tryBlock;
456
+ processStatement(stmt.tryBlock);
457
+
458
+ if (stmt.catchClause) {
459
+ currentBlock.successors.push(catchBlock.id);
460
+ catchBlock.predecessors.push(currentBlock.id);
461
+ currentBlock = catchBlock;
462
+ processStatement(stmt.catchClause.block);
463
+ }
464
+
465
+ currentBlock.successors.push(finallyBlock.id);
466
+ finallyBlock.predecessors.push(currentBlock.id);
467
+ finallyBlock.successors.push(finallyBlock.id); // exits to itself then to next
468
+ currentBlock = finallyBlock;
469
+ if (stmt.finallyBlock) {
470
+ processStatement(stmt.finallyBlock);
471
+ }
472
+ return;
473
+ }
474
+
475
+ // Handle return statement
476
+ if (ts.isReturnStatement(stmt)) {
477
+ currentBlock.statements.push(stmt);
478
+ return;
479
+ }
480
+
481
+ // Handle switch statement
482
+ if (ts.isSwitchStatement(stmt)) {
483
+ const mergeBlock = createBlock();
484
+ const caseBlocks: BasicBlock[] = [];
485
+
486
+ for (const clause of stmt.caseBlock.clauses) {
487
+ const caseBlock = createBlock();
488
+ caseBlocks.push(caseBlock);
489
+ currentBlock.successors.push(caseBlock.id);
490
+ caseBlock.predecessors.push(currentBlock.id);
491
+ currentBlock = caseBlock;
492
+
493
+ for (const s of clause.statements) {
494
+ processStatement(s);
495
+ }
496
+
497
+ if (!currentBlock.successors.includes(mergeBlock.id)) {
498
+ currentBlock.successors.push(mergeBlock.id);
499
+ mergeBlock.predecessors.push(currentBlock.id);
500
+ }
501
+ }
502
+
503
+ currentBlock = mergeBlock;
504
+ return;
505
+ }
506
+
507
+ // Regular statement - add to current block
508
+ currentBlock.statements.push(stmt);
509
+
510
+ // Check for control flow statements that might branch
511
+ if (ts.isBreakStatement(stmt) || ts.isContinueStatement(stmt) || ts.isThrowStatement(stmt)) {
512
+ // These will be handled when we add proper CFG edges
513
+ }
514
+ };
515
+
516
+ // Process body statements
517
+ if (ts.isBlock(body)) {
518
+ for (const stmt of body.statements) {
519
+ processStatement(stmt);
520
+ }
521
+ } else {
522
+ // Expression body (arrow function)
523
+ currentBlock.statements.push(body as any);
524
+ }
525
+
526
+ // Create exit block
527
+ const exitBlock = createBlock();
528
+ currentBlock.successors.push(exitBlock.id);
529
+ exitBlock.predecessors.push(currentBlock.id);
530
+
531
+ const cfg = { blocks, entryBlock: entryBlock.id, exitBlock: exitBlock.id };
532
+ this.cfgCache.set(cacheKey, cfg);
533
+ return cfg;
534
+ }
535
+
536
+ /**
537
+ * Create entry fact with parameter bindings
538
+ */
539
+ private createEntryFact(func: ts.FunctionDeclaration | ts.ArrowFunction): DataFlowFact {
540
+ const env = new Map<string, AbstractValue>();
541
+
542
+ // Add parameters
543
+ for (const param of func.parameters) {
544
+ if (ts.isIdentifier(param.name)) {
545
+ const paramType = this.checker.getTypeAtLocation(param);
546
+ env.set(param.name.text, this.createAbstractValue(paramType));
547
+ }
548
+ }
549
+
550
+ return {
551
+ env,
552
+ constraints: [],
553
+ pathCondition: [],
554
+ };
555
+ }
556
+
557
+ /**
558
+ * Create abstract value from TypeScript type
559
+ */
560
+ private createAbstractValue(type: ts.Type): AbstractValue {
561
+ const typeStr = this.checker.typeToString(type);
562
+ const flags = type.flags;
563
+
564
+ const value: AbstractValue = {
565
+ type: typeStr,
566
+ constants: new Set(),
567
+ nullable: false,
568
+ properties: new Map(),
569
+ tainted: false,
570
+ escapes: new Set(),
571
+ };
572
+
573
+ // Check for null/undefined
574
+ if (flags & ts.TypeFlags.Null) value.nullable = true;
575
+ if (flags & ts.TypeFlags.Undefined) value.nullable = true;
576
+ if (flags & ts.TypeFlags.StringLiteral) {
577
+ value.constants.add((type as any).value || typeStr);
578
+ }
579
+ if (flags & ts.TypeFlags.NumberLiteral) {
580
+ value.constants.add(String((type as any).value || typeStr));
581
+ }
582
+
583
+ // Handle object types
584
+ if (flags & ts.TypeFlags.Object) {
585
+ const objType = type as ts.ObjectType;
586
+ const props = this.checker.getPropertiesOfType(objType);
587
+ for (const prop of props) {
588
+ if (prop.valueDeclaration) {
589
+ const propType = this.checker.getTypeAtLocation(prop.valueDeclaration);
590
+ value.properties.set(prop.name, this.createAbstractValue(propType));
591
+ }
592
+ }
593
+ }
594
+
595
+ // Handle type references (interfaces, classes) - check for typeArguments
596
+ const typeRef = type as ts.TypeReference;
597
+ if (typeRef.typeArguments && typeRef.typeArguments.length > 0) {
598
+ // Generic type reference
599
+ if (typeRef.target) {
600
+ value.type = this.checker.typeToString(typeRef.target);
601
+ }
602
+ value.elementType = this.createAbstractValue(typeRef.typeArguments[0]);
603
+ }
604
+
605
+ return value;
606
+ }
607
+
608
+ /**
609
+ * WORKLIST ALGORITHM - Lattice-based fixed-point computation
610
+ *
611
+ * This is the core of the data flow analysis.
612
+ * It iterates until no facts change (fixed point is reached).
613
+ */
614
+ private runWorklistAnalysis(
615
+ cfg: ControlFlowGraph,
616
+ entryFact: DataFlowFact,
617
+ stats: DataFlowResult['statistics']
618
+ ): Map<string, DataFlowFact> {
619
+ const blockFacts = new Map<string, DataFlowFact>();
620
+ const changed = new Set<string>();
621
+
622
+ // Initialize all blocks with BOTTOM (no information)
623
+ for (const blockId of cfg.blocks.keys()) {
624
+ blockFacts.set(blockId, {
625
+ env: new Map(),
626
+ constraints: [],
627
+ pathCondition: [],
628
+ });
629
+ }
630
+
631
+ // Set entry block
632
+ blockFacts.set(cfg.entryBlock, this.cloneFact(entryFact));
633
+ this.worklist.push(cfg.entryBlock);
634
+ changed.add(cfg.entryBlock);
635
+
636
+ let iterations = 0;
637
+
638
+ // Worklist algorithm
639
+ while (this.worklist.length > 0 && iterations < this.maxIterations) {
640
+ iterations++;
641
+ stats.fixedPointIterations++;
642
+
643
+ // Pop from worklist
644
+ const blockId = this.worklist.shift()!;
645
+ changed.delete(blockId);
646
+
647
+ const block = cfg.blocks.get(blockId)!;
648
+ const currentFact = blockFacts.get(blockId)!;
649
+
650
+ // Compute flow through predecessors (JOIN)
651
+ if (block.predecessors.length > 0) {
652
+ const joinedFact = this.joinFacts(
653
+ block.predecessors.map(predId => blockFacts.get(predId)!)
654
+ );
655
+ // If join changed anything, update and propagate
656
+ if (!this.factsEqual(currentFact, joinedFact)) {
657
+ blockFacts.set(blockId, joinedFact);
658
+ for (const succId of block.successors) {
659
+ if (!changed.has(succId)) {
660
+ this.worklist.push(succId);
661
+ changed.add(succId);
662
+ }
663
+ }
664
+ continue;
665
+ }
666
+ }
667
+
668
+ // TRANSFER FUNCTION - apply block's statements
669
+ let newFact = this.cloneFact(currentFact);
670
+
671
+ for (const stmt of block.statements) {
672
+ newFact = this.transfer(stmt, newFact, stats);
673
+ }
674
+
675
+ // Handle branch condition (add to path condition)
676
+ if (block.branch) {
677
+ stats.conditionalBranches++;
678
+ const thenFact = this.cloneFact(newFact);
679
+ const elseFact = this.cloneFact(newFact);
680
+
681
+ // Add path condition for then-branch and NARROW TYPES
682
+ thenFact.pathCondition.push({
683
+ expression: block.branch.condition.getText(),
684
+ polarity: true,
685
+ });
686
+ // Apply type narrowing based on condition
687
+ this.narrowTypesFromCondition(thenFact, block.branch.condition, true, stats);
688
+
689
+ // Add path condition for else-branch (negated) and NARROW TYPES
690
+ elseFact.pathCondition.push({
691
+ expression: block.branch.condition.getText(),
692
+ polarity: false,
693
+ });
694
+ // Apply type narrowing based on negated condition
695
+ this.narrowTypesFromCondition(elseFact, block.branch.condition, false, stats);
696
+
697
+ // Propagate to successors
698
+ const thenBlock = cfg.blocks.get(block.branch.trueTarget)!;
699
+ const elseBlock = cfg.blocks.get(block.branch.falseTarget)!;
700
+
701
+ if (!this.factsEqual(blockFacts.get(thenBlock.id)!, thenFact)) {
702
+ blockFacts.set(thenBlock.id, thenFact);
703
+ if (!changed.has(thenBlock.id)) {
704
+ this.worklist.push(thenBlock.id);
705
+ changed.add(thenBlock.id);
706
+ }
707
+ }
708
+
709
+ if (!this.factsEqual(blockFacts.get(elseBlock.id)!, elseFact)) {
710
+ blockFacts.set(elseBlock.id, elseFact);
711
+ if (!changed.has(elseBlock.id)) {
712
+ this.worklist.push(elseBlock.id);
713
+ changed.add(elseBlock.id);
714
+ }
715
+ }
716
+ } else {
717
+ // No branch - normal flow to successors
718
+ for (const succId of block.successors) {
719
+ if (!this.factsEqual(blockFacts.get(succId)!, newFact)) {
720
+ blockFacts.set(succId, newFact);
721
+ if (!changed.has(succId)) {
722
+ this.worklist.push(succId);
723
+ changed.add(succId);
724
+ }
725
+ }
726
+ }
727
+ }
728
+
729
+ stats.nodesAnalyzed++;
730
+ }
731
+
732
+ return blockFacts;
733
+ }
734
+
735
+ /**
736
+ * JOIN operation - combine facts from multiple predecessors
737
+ */
738
+ private joinFacts(facts: DataFlowFact[]): DataFlowFact {
739
+ if (facts.length === 0) {
740
+ return {
741
+ env: new Map(),
742
+ constraints: [],
743
+ pathCondition: [],
744
+ };
745
+ }
746
+
747
+ if (facts.length === 1) {
748
+ return this.cloneFact(facts[0]);
749
+ }
750
+
751
+ const result: DataFlowFact = {
752
+ env: new Map(),
753
+ constraints: [],
754
+ pathCondition: [],
755
+ };
756
+
757
+ // Join all environments
758
+ const allVars = new Set<string>();
759
+ for (const fact of facts) {
760
+ for (const [v] of fact.env) {
761
+ allVars.add(v);
762
+ }
763
+ }
764
+
765
+ for (const varName of allVars) {
766
+ const values = facts
767
+ .map(f => f.env.get(varName))
768
+ .filter((v): v is AbstractValue => v !== undefined);
769
+
770
+ if (values.length === 0) continue;
771
+
772
+ // Lattice meet operation (intersection of possible values)
773
+ result.env.set(varName, this.latticeMeet(values));
774
+ }
775
+
776
+ // Union path conditions
777
+ for (const fact of facts) {
778
+ for (const pc of fact.pathCondition) {
779
+ if (!result.pathCondition.some(p => p.expression === pc.expression && p.polarity === pc.polarity)) {
780
+ result.pathCondition.push(pc);
781
+ }
782
+ }
783
+ }
784
+
785
+ return result;
786
+ }
787
+
788
+ /**
789
+ * LATTICE MEET - intersection of abstract values
790
+ */
791
+ private latticeMeet(values: AbstractValue[]): AbstractValue {
792
+ if (values.length === 0) {
793
+ return this.createAbstractValue(this.checker.getTypeAtLocation(ts.factory.createIdentifier('undefined')));
794
+ }
795
+
796
+ if (values.length === 1) {
797
+ return values[0];
798
+ }
799
+
800
+ // For now, simplified meet: union of constants, intersection of types
801
+ const result: AbstractValue = {
802
+ type: values[0].type,
803
+ constants: new Set(),
804
+ nullable: values.some(v => v.nullable),
805
+ properties: new Map(),
806
+ tainted: values.some(v => v.tainted),
807
+ escapes: new Set(),
808
+ };
809
+
810
+ // Union of constants
811
+ for (const v of values) {
812
+ for (const c of v.constants) {
813
+ result.constants.add(c);
814
+ }
815
+ for (const e of v.escapes) {
816
+ result.escapes.add(e);
817
+ }
818
+ }
819
+
820
+ // Intersect property types (simplified)
821
+ const allProps = new Set<string>();
822
+ for (const v of values) {
823
+ for (const [p] of v.properties) {
824
+ allProps.add(p);
825
+ }
826
+ }
827
+
828
+ for (const propName of allProps) {
829
+ const propValues = values
830
+ .map(v => v.properties.get(propName))
831
+ .filter((v): v is AbstractValue => v !== undefined);
832
+
833
+ if (propValues.length > 0) {
834
+ result.properties.set(propName, this.latticeMeet(propValues));
835
+ }
836
+ }
837
+
838
+ return result;
839
+ }
840
+
841
+ /**
842
+ * TRANSFER FUNCTION - apply a statement's effect on facts
843
+ */
844
+ private transfer(stmt: ts.Statement, fact: DataFlowFact, stats: DataFlowResult['statistics']): DataFlowFact {
845
+ // Variable declaration
846
+ if (ts.isVariableStatement(stmt)) {
847
+ for (const decl of stmt.declarationList.declarations) {
848
+ if (ts.isIdentifier(decl.name) && decl.initializer) {
849
+ const varName = decl.name.text;
850
+ const rhsFact = this.transferExpr(decl.initializer, fact, stats);
851
+ const rhsValue = this.evaluateExpr(decl.initializer, rhsFact);
852
+
853
+ // Check for taint (user input)
854
+ if (this.isTaintedSource(decl.initializer)) {
855
+ rhsValue.tainted = true;
856
+ }
857
+
858
+ // Check for escape
859
+ if (this.doesEscape(decl.initializer)) {
860
+ rhsValue.escapes.add('return');
861
+ }
862
+
863
+ fact.env.set(varName, rhsValue);
864
+ }
865
+ }
866
+ return fact;
867
+ }
868
+
869
+ // Assignment
870
+ if (ts.isExpressionStatement(stmt) && ts.isBinaryExpression(stmt.expression) && stmt.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
871
+ const assign = stmt.expression;
872
+ if (ts.isIdentifier(assign.left)) {
873
+ const varName = assign.left.text;
874
+ const rhsFact = this.transferExpr(assign.right, fact, stats);
875
+ const rhsValue = this.evaluateExpr(assign.right, rhsFact);
876
+
877
+ // Check for taint propagation
878
+ const rhsFactValue = this.evaluateExpr(assign.right, fact);
879
+ if (rhsFactValue.tainted) {
880
+ rhsValue.tainted = true;
881
+ }
882
+
883
+ fact.env.set(varName, rhsValue);
884
+ }
885
+ return fact;
886
+ }
887
+
888
+ // Return statement
889
+ if (ts.isReturnStatement(stmt) && stmt.expression) {
890
+ const retValue = this.evaluateExpr(stmt.expression, fact);
891
+ // Mark as escaping via return
892
+ retValue.escapes.add('return');
893
+ fact.env.set('__return__', retValue);
894
+ return fact;
895
+ }
896
+
897
+ // Call expression
898
+ if (ts.isExpressionStatement(stmt) && ts.isCallExpression(stmt.expression)) {
899
+ this.analyzeCallSite(stmt.expression, fact, stats);
900
+ return fact;
901
+ }
902
+
903
+ return fact;
904
+ }
905
+
906
+ /**
907
+ * Transfer function for expressions
908
+ */
909
+ private transferExpr(expr: ts.Expression, fact: DataFlowFact, stats: DataFlowResult['statistics']): DataFlowFact {
910
+ // Handle conditional expression (ternary)
911
+ if (ts.isConditionalExpression(expr)) {
912
+ // Both branches contribute
913
+ const thenFact = this.transferExpr(expr.whenTrue, fact, stats);
914
+ const elseFact = this.transferExpr(expr.whenFalse, fact, stats);
915
+ return this.joinFacts([thenFact, elseFact]);
916
+ }
917
+
918
+ return fact;
919
+ }
920
+
921
+ /**
922
+ * Evaluate expression to get abstract value
923
+ */
924
+ private evaluateExpr(expr: ts.Expression, fact: DataFlowFact): AbstractValue {
925
+ // Identifier
926
+ if (ts.isIdentifier(expr)) {
927
+ const varName = expr.text;
928
+ const value = fact.env.get(varName);
929
+ if (value) return value;
930
+ // Unknown variable
931
+ const unknownType = this.checker.getTypeAtLocation(expr);
932
+ return this.createAbstractValue(unknownType);
933
+ }
934
+
935
+ // String literal
936
+ if (ts.isStringLiteral(expr)) {
937
+ return {
938
+ type: 'string',
939
+ constants: new Set([`'${expr.text}'`]),
940
+ nullable: false,
941
+ properties: new Map(),
942
+ tainted: false,
943
+ escapes: new Set(),
944
+ };
945
+ }
946
+
947
+ // Numeric literal
948
+ if (ts.isNumericLiteral(expr)) {
949
+ return {
950
+ type: 'number',
951
+ constants: new Set([expr.text]),
952
+ nullable: false,
953
+ properties: new Map(),
954
+ tainted: false,
955
+ escapes: new Set(),
956
+ };
957
+ }
958
+
959
+ // Property access (obj.prop)
960
+ if (ts.isPropertyAccessExpression(expr)) {
961
+ const objValue = this.evaluateExpr(expr.expression, fact);
962
+ const propName = expr.name.text;
963
+
964
+ // Look up property in object's type
965
+ const propValue = objValue.properties.get(propName);
966
+ if (propValue) return propValue;
967
+
968
+ // Or use the type checker
969
+ const type = this.checker.getTypeAtLocation(expr);
970
+ return this.createAbstractValue(type);
971
+ }
972
+
973
+ // Call expression
974
+ if (ts.isCallExpression(expr)) {
975
+ const type = this.checker.getTypeAtLocation(expr);
976
+ return this.createAbstractValue(type);
977
+ }
978
+
979
+ // Binary expression
980
+ if (ts.isBinaryExpression(expr)) {
981
+ const type = this.checker.getTypeAtLocation(expr);
982
+ return this.createAbstractValue(type);
983
+ }
984
+
985
+ // Element access (arr[i])
986
+ if (ts.isElementAccessExpression(expr)) {
987
+ const type = this.checker.getTypeAtLocation(expr);
988
+ return this.createAbstractValue(type);
989
+ }
990
+
991
+ // Default - use type checker
992
+ const type = this.checker.getTypeAtLocation(expr);
993
+ return this.createAbstractValue(type);
994
+ }
995
+
996
+ /**
997
+ * Analyze a call site (interprocedural analysis)
998
+ */
999
+ private analyzeCallSite(callExpr: ts.CallExpression, fact: DataFlowFact, stats: DataFlowResult['statistics']): void {
1000
+ const calleeExpr = callExpr.expression;
1001
+ let calleeName = '';
1002
+
1003
+ if (ts.isIdentifier(calleeExpr)) {
1004
+ calleeName = calleeExpr.text;
1005
+ }
1006
+
1007
+ // Check for known taint sources
1008
+ if (calleeName === 'readFile' || calleeName === 'readFileSync') {
1009
+ stats.taintedValues++;
1010
+ }
1011
+ if (calleeName === 'fetch' || calleeName === 'axios' || calleeName === 'http.get') {
1012
+ // Network input is potentially tainted
1013
+ }
1014
+
1015
+ // Try to resolve the callee function
1016
+ const type = this.checker.getTypeAtLocation(calleeExpr);
1017
+ const symbol = type.symbol;
1018
+ if (!symbol) return;
1019
+
1020
+ const declarations = symbol.getDeclarations();
1021
+ if (!declarations || declarations.length === 0) return;
1022
+
1023
+ const calleeFunc = declarations[0];
1024
+ if (!ts.isFunctionDeclaration(calleeFunc) && !ts.isArrowFunction(calleeFunc)) return;
1025
+
1026
+ stats.callSitesAnalyzed++;
1027
+
1028
+ // Build argument facts
1029
+ const argFacts = new Map<string, AbstractValue>();
1030
+ for (let i = 0; i < callExpr.arguments.length; i++) {
1031
+ const arg = callExpr.arguments[i];
1032
+ const paramName = calleeFunc.parameters[i]?.name;
1033
+ if (ts.isIdentifier(paramName) && paramName.text) {
1034
+ argFacts.set(paramName.text, this.evaluateExpr(arg, fact));
1035
+ }
1036
+ }
1037
+
1038
+ // Check if we've analyzed this call site (context sensitivity cache)
1039
+ const callKey = `${callExpr.getSourceFile().fileName}:${callExpr.getStart()}:${calleeName}`;
1040
+ if (this.analyzedCallSites.has(callKey)) return;
1041
+ this.analyzedCallSites.add(callKey);
1042
+
1043
+ // Build CFG for callee and analyze with argument bindings
1044
+ const calleeCFG = this.buildCFG(calleeFunc);
1045
+ const calleeEntryFact = this.createEntryFact(calleeFunc);
1046
+
1047
+ // Override with actual argument values
1048
+ for (const [paramName, argValue] of argFacts) {
1049
+ calleeEntryFact.env.set(paramName, argValue);
1050
+ }
1051
+
1052
+ // Run analysis on callee
1053
+ this.runWorklistAnalysis(calleeCFG, calleeEntryFact, stats);
1054
+ }
1055
+
1056
+ /**
1057
+ * Check if expression is a taint source
1058
+ */
1059
+ private isTaintedSource(expr: ts.Expression): boolean {
1060
+ if (ts.isCallExpression(expr)) {
1061
+ const callee = expr.expression;
1062
+ if (ts.isIdentifier(callee)) {
1063
+ const name = callee.text;
1064
+ // Known taint sources
1065
+ if (['readFile', 'readFileSync', 'fetch', 'axios', 'http.request',
1066
+ 'process.argv', 'process.env', 'JSON.parse', 'document.cookie',
1067
+ 'localStorage.getItem', 'sessionStorage.getItem'].includes(name)) {
1068
+ return true;
1069
+ }
1070
+ }
1071
+ }
1072
+ return false;
1073
+ }
1074
+
1075
+ /**
1076
+ * Check if expression causes escape
1077
+ */
1078
+ private doesEscape(expr: ts.Expression): boolean {
1079
+ // Return statement makes value escape
1080
+ if (ts.isReturnStatement(expr)) return true;
1081
+
1082
+ // Passing as argument makes it escape to that function
1083
+ if (ts.isCallExpression(expr)) return true;
1084
+
1085
+ // Property assignment to external object
1086
+ if (ts.isPropertyAccessExpression(expr)) {
1087
+ const propAccess = expr;
1088
+ if (ts.isIdentifier(propAccess.expression)) {
1089
+ const name = propAccess.expression.text;
1090
+ // Known external objects
1091
+ if (['global', 'window', 'document', 'console', 'process'].includes(name)) {
1092
+ return true;
1093
+ }
1094
+ }
1095
+ }
1096
+
1097
+ return false;
1098
+ }
1099
+
1100
+ /**
1101
+ * Analyze async patterns (Promise, await)
1102
+ */
1103
+ private analyzeAsyncPatterns(
1104
+ func: ts.FunctionDeclaration | ts.ArrowFunction,
1105
+ flowPaths: FlowPath[],
1106
+ stats: DataFlowResult['statistics']
1107
+ ): void {
1108
+ const visit = (node: ts.Node): void => {
1109
+ // Promise.then chain
1110
+ if (ts.isPropertyAccessExpression(node) && node.name.text === 'then') {
1111
+ const callExpr = node.parent;
1112
+ if (ts.isCallExpression(callExpr)) {
1113
+ const callback = callExpr.arguments[0];
1114
+ if (ts.isArrowFunction(callback) && callback.parameters.length > 0) {
1115
+ stats.promiseUnwraps++;
1116
+
1117
+ const paramType = this.checker.getTypeAtLocation(callback.parameters[0]);
1118
+ const paramTypeStr = this.checker.typeToString(paramType);
1119
+
1120
+ flowPaths.push({
1121
+ source: `Promise.then callback at line ${this.getLine(node.getSourceFile(), node)}`,
1122
+ sink: `${paramTypeStr} at line ${this.getLine(node.getSourceFile(), callback)}`,
1123
+ path: ['Promise', 'then', 'callback'],
1124
+ typeAtSink: paramTypeStr,
1125
+ typeAtSource: 'T (Promise<T>)',
1126
+ isTainted: false,
1127
+ });
1128
+ stats.pathsTracked++;
1129
+ }
1130
+ }
1131
+ }
1132
+
1133
+ // await expression
1134
+ if (ts.isAwaitExpression(node)) {
1135
+ stats.promiseUnwraps++;
1136
+ }
1137
+
1138
+ ts.forEachChild(node, visit);
1139
+ };
1140
+
1141
+ visit(func);
1142
+ }
1143
+
1144
+ /**
1145
+ * Check for data leaks (tainted -> escape)
1146
+ */
1147
+ private checkDataLeaks(flowPaths: FlowPath[], taintedPaths: TaintedPath[]): boolean {
1148
+ // If any tainted path reaches a sensitive sink, it's a leak
1149
+ for (const tp of taintedPaths) {
1150
+ if (tp.sink.includes('return') || tp.sink.includes('global') || tp.sink.includes('write')) {
1151
+ return true;
1152
+ }
1153
+ }
1154
+ return false;
1155
+ }
1156
+
1157
+ /**
1158
+ * Clone a data flow fact
1159
+ */
1160
+ private cloneFact(fact: DataFlowFact): DataFlowFact {
1161
+ const cloned = {
1162
+ env: new Map<string, AbstractValue>(),
1163
+ constraints: [...fact.constraints],
1164
+ pathCondition: [...fact.pathCondition],
1165
+ };
1166
+
1167
+ for (const [key, value] of fact.env) {
1168
+ cloned.env.set(key, {
1169
+ ...value,
1170
+ constants: new Set(value.constants),
1171
+ properties: new Map(value.properties),
1172
+ escapes: new Set(value.escapes),
1173
+ });
1174
+ }
1175
+
1176
+ return cloned;
1177
+ }
1178
+
1179
+ /**
1180
+ * Narrow types based on branch condition
1181
+ * E.g., if (x != null) narrows x from T | null to T
1182
+ * if (x > 1000) narrows the possible range of x
1183
+ */
1184
+ private narrowTypesFromCondition(
1185
+ fact: DataFlowFact,
1186
+ cond: ts.Expression,
1187
+ isThenBranch: boolean,
1188
+ stats: DataFlowResult['statistics']
1189
+ ): void {
1190
+ // Handle binary expressions: x != null, x === 'value', x > 1000, etc.
1191
+ if (ts.isBinaryExpression(cond)) {
1192
+ const left = cond.left;
1193
+ const right = cond.right;
1194
+ const op = cond.operatorToken.kind;
1195
+
1196
+ if (ts.isIdentifier(left)) {
1197
+ const varName = left.text;
1198
+ const varValue = fact.env.get(varName);
1199
+
1200
+ if (!varValue) return;
1201
+
1202
+ // Handle null checks: x != null, x !== null, x == null, x === null
1203
+ if (right.kind === ts.SyntaxKind.NullKeyword) {
1204
+ if (op === ts.SyntaxKind.ExclamationEqualsEqualsToken ||
1205
+ op === ts.SyntaxKind.ExclamationEqualsToken) {
1206
+ // Then branch: x != null means x is not null
1207
+ if (isThenBranch) {
1208
+ varValue.nullable = false;
1209
+ stats.typesNarrowed++;
1210
+ }
1211
+ } else if (op === ts.SyntaxKind.EqualsEqualsToken ||
1212
+ op === ts.SyntaxKind.EqualsEqualsEqualsToken) {
1213
+ // Then branch: x == null means x IS null
1214
+ if (isThenBranch) {
1215
+ varValue.constants.add('null');
1216
+ stats.typesNarrowed++;
1217
+ }
1218
+ }
1219
+ }
1220
+
1221
+ // Handle numeric comparisons: x > 1000, x <= 0
1222
+ if (varValue.type === 'number' && ts.isNumericLiteral(right)) {
1223
+ const numValue = right.text;
1224
+ if (op === ts.SyntaxKind.GreaterThanToken) {
1225
+ if (isThenBranch) {
1226
+ varValue.constants.add(`>${numValue}`);
1227
+ stats.typesNarrowed++;
1228
+ }
1229
+ } else if (op === ts.SyntaxKind.LessThanToken) {
1230
+ if (isThenBranch) {
1231
+ varValue.constants.add(`<${numValue}`);
1232
+ stats.typesNarrowed++;
1233
+ }
1234
+ }
1235
+ }
1236
+ }
1237
+
1238
+ // Handle instanceof checks
1239
+ if (ts.isBinaryExpression(cond) && ts.isIdentifier(right)) {
1240
+ const rightName = right.text;
1241
+ if (op === ts.SyntaxKind.InstanceOfKeyword) {
1242
+ if (isThenBranch) {
1243
+ const varName = (cond.left as ts.Identifier).text;
1244
+ const varValue = fact.env.get(varName);
1245
+ if (varValue) {
1246
+ varValue.type = rightName;
1247
+ stats.typesNarrowed++;
1248
+ }
1249
+ }
1250
+ }
1251
+ }
1252
+ }
1253
+
1254
+ // Handle unary expressions: if (x)
1255
+ if (ts.isPrefixUnaryExpression(cond) && cond.operator === ts.SyntaxKind.ExclamationToken) {
1256
+ const operand = cond.operand;
1257
+ if (ts.isIdentifier(operand)) {
1258
+ const varName = operand.text;
1259
+ const varValue = fact.env.get(varName);
1260
+ if (varValue) {
1261
+ if (isThenBranch) {
1262
+ // !x means x is falsy (null, undefined, 0, false, '')
1263
+ // We can narrow nullable types
1264
+ if (varValue.nullable) {
1265
+ varValue.nullable = false;
1266
+ stats.typesNarrowed++;
1267
+ }
1268
+ }
1269
+ }
1270
+ }
1271
+ }
1272
+
1273
+ // Handle identifier directly: if (data)
1274
+ if (ts.isIdentifier(cond)) {
1275
+ const varName = cond.text;
1276
+ const varValue = fact.env.get(varName);
1277
+ if (varValue) {
1278
+ if (isThenBranch) {
1279
+ // truthy check - we know it's not null/undefined/false
1280
+ varValue.nullable = false;
1281
+ stats.typesNarrowed++;
1282
+ }
1283
+ }
1284
+ }
1285
+ }
1286
+
1287
+ /**
1288
+ * Check if two facts are equal
1289
+ */
1290
+ private factsEqual(a: DataFlowFact, b: DataFlowFact): boolean {
1291
+ if (a.env.size !== b.env.size) return false;
1292
+
1293
+ for (const [key, aVal] of a.env) {
1294
+ const bVal = b.env.get(key);
1295
+ if (!bVal) return false;
1296
+ if (aVal.type !== bVal.type) return false;
1297
+ if (aVal.tainted !== bVal.tainted) return false;
1298
+ if (aVal.nullable !== bVal.nullable) return false;
1299
+ }
1300
+
1301
+ return true;
1302
+ }
1303
+
1304
+ /**
1305
+ * Get line number
1306
+ */
1307
+ private getLine(sourceFile: ts.SourceFile | undefined, node: ts.Node): number {
1308
+ if (!sourceFile) return 0;
1309
+ return sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
1310
+ }
1311
+
1312
+ /**
1313
+ * Calculate confidence level
1314
+ */
1315
+ private calculateConfidence(stats: DataFlowResult['statistics']): 'high' | 'medium' | 'low' {
1316
+ if (stats.fixedPointIterations >= 50 && stats.callSitesAnalyzed >= 10) return 'high';
1317
+ if (stats.fixedPointIterations >= 20 && stats.callSitesAnalyzed >= 5) return 'medium';
1318
+ return 'low';
1319
+ }
1320
+
1321
+ /**
1322
+ * Create empty result
1323
+ */
1324
+ private createEmptyResult(confidence: 'high' | 'medium' | 'low', stats: DataFlowResult['statistics'], duration: number): DataFlowResult {
1325
+ return {
1326
+ hasDataLeaks: false,
1327
+ flowPaths: [],
1328
+ taintedPaths: [],
1329
+ typeNarrowing: new Map(),
1330
+ statistics: stats,
1331
+ confidence,
1332
+ duration,
1333
+ finalFacts: new Map(),
1334
+ };
1335
+ }
1336
+
1337
+ /**
1338
+ * Format as text
1339
+ */
1340
+ formatAsText(result: DataFlowResult, functionName: string): string {
1341
+ const lines: string[] = [];
1342
+
1343
+ lines.push('');
1344
+ lines.push('═══════════════════════════════════════════════════════════════');
1345
+ lines.push(' 📊 商业级数据流分析 (DataFlow Pro) ');
1346
+ lines.push('═══════════════════════════════════════════════════════════════');
1347
+ lines.push('');
1348
+
1349
+ lines.push(`📈 分析统计`);
1350
+ lines.push(` 基本块: ${result.statistics.blocksConstructed}`);
1351
+ lines.push(` 调用点: ${result.statistics.callSitesAnalyzed}`);
1352
+ lines.push(` 迭代次数: ${result.statistics.fixedPointIterations}`);
1353
+ lines.push(` 节点分析: ${result.statistics.nodesAnalyzed}`);
1354
+ lines.push(` 约束生成: ${result.statistics.constraintsGenerated}`);
1355
+ lines.push(` Promise解包: ${result.statistics.promiseUnwraps}`);
1356
+ lines.push(` 条件分支: ${result.statistics.conditionalBranches}`);
1357
+ lines.push(` 类型收窄: ${result.statistics.typesNarrowed}`);
1358
+ lines.push(` 污点值: ${result.statistics.taintedValues}`);
1359
+ lines.push(` 逃逸值: ${result.statistics.escapedValues}`);
1360
+ lines.push(` 置信度: ${result.confidence}`);
1361
+ lines.push(` 耗时: ${result.duration}ms`);
1362
+ lines.push('');
1363
+
1364
+ if (result.taintedPaths.length > 0) {
1365
+ lines.push('⚠️ 污点传播路径:');
1366
+ for (const tp of result.taintedPaths.slice(0, 5)) {
1367
+ lines.push(` ${tp.source} → ${tp.sink} (来源: ${tp.taintSource})`);
1368
+ }
1369
+ lines.push('');
1370
+ }
1371
+
1372
+ if (result.typeNarrowing.size > 0) {
1373
+ lines.push('🔽 类型收窄:');
1374
+ for (const [varName, narrowing] of result.typeNarrowing) {
1375
+ lines.push(` ${varName}:`);
1376
+ for (const n of narrowing) {
1377
+ lines.push(` → 第${n.line}行: ${n.types.join(' | ')}`);
1378
+ }
1379
+ }
1380
+ lines.push('');
1381
+ }
1382
+
1383
+ if (result.flowPaths.length > 0) {
1384
+ lines.push('🔗 数据流路径:');
1385
+ for (const fp of result.flowPaths.slice(0, 5)) {
1386
+ lines.push(` ${fp.source}`);
1387
+ lines.push(` → ${fp.sink} (${fp.typeAtSink})`);
1388
+ if (fp.isTainted) lines.push(` ⚠️ 污点数据`);
1389
+ }
1390
+ lines.push('');
1391
+ }
1392
+
1393
+ if (result.hasDataLeaks) {
1394
+ lines.push('🔴 警告: 检测到数据泄漏风险!');
1395
+ lines.push('');
1396
+ } else {
1397
+ lines.push('✅ 未检测到数据泄漏');
1398
+ lines.push('');
1399
+ }
1400
+
1401
+ return lines.join('\n');
1402
+ }
1403
+ }