@oddessentials/odd-ai-reviewers 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 +190 -0
- package/dist/__tests__/hermetic-setup.d.ts +55 -0
- package/dist/__tests__/hermetic-setup.d.ts.map +1 -0
- package/dist/__tests__/hermetic-setup.js +62 -0
- package/dist/__tests__/hermetic-setup.js.map +1 -0
- package/dist/__tests__/test-utils/hermetic.d.ts +84 -0
- package/dist/__tests__/test-utils/hermetic.d.ts.map +1 -0
- package/dist/__tests__/test-utils/hermetic.js +147 -0
- package/dist/__tests__/test-utils/hermetic.js.map +1 -0
- package/dist/agents/ai_semantic_review.d.ts +12 -0
- package/dist/agents/ai_semantic_review.d.ts.map +1 -0
- package/dist/agents/ai_semantic_review.js +317 -0
- package/dist/agents/ai_semantic_review.js.map +1 -0
- package/dist/agents/control_flow/budget.d.ts +162 -0
- package/dist/agents/control_flow/budget.d.ts.map +1 -0
- package/dist/agents/control_flow/budget.js +331 -0
- package/dist/agents/control_flow/budget.js.map +1 -0
- package/dist/agents/control_flow/cfg-builder.d.ts +26 -0
- package/dist/agents/control_flow/cfg-builder.d.ts.map +1 -0
- package/dist/agents/control_flow/cfg-builder.js +776 -0
- package/dist/agents/control_flow/cfg-builder.js.map +1 -0
- package/dist/agents/control_flow/cfg-types.d.ts +186 -0
- package/dist/agents/control_flow/cfg-types.d.ts.map +1 -0
- package/dist/agents/control_flow/cfg-types.js +114 -0
- package/dist/agents/control_flow/cfg-types.js.map +1 -0
- package/dist/agents/control_flow/finding-generator.d.ts +118 -0
- package/dist/agents/control_flow/finding-generator.d.ts.map +1 -0
- package/dist/agents/control_flow/finding-generator.js +354 -0
- package/dist/agents/control_flow/finding-generator.js.map +1 -0
- package/dist/agents/control_flow/index.d.ts +39 -0
- package/dist/agents/control_flow/index.d.ts.map +1 -0
- package/dist/agents/control_flow/index.js +270 -0
- package/dist/agents/control_flow/index.js.map +1 -0
- package/dist/agents/control_flow/logger.d.ts +333 -0
- package/dist/agents/control_flow/logger.d.ts.map +1 -0
- package/dist/agents/control_flow/logger.js +607 -0
- package/dist/agents/control_flow/logger.js.map +1 -0
- package/dist/agents/control_flow/mitigation-detector.d.ts +207 -0
- package/dist/agents/control_flow/mitigation-detector.d.ts.map +1 -0
- package/dist/agents/control_flow/mitigation-detector.js +625 -0
- package/dist/agents/control_flow/mitigation-detector.js.map +1 -0
- package/dist/agents/control_flow/mitigation-patterns.d.ts +53 -0
- package/dist/agents/control_flow/mitigation-patterns.d.ts.map +1 -0
- package/dist/agents/control_flow/mitigation-patterns.js +620 -0
- package/dist/agents/control_flow/mitigation-patterns.js.map +1 -0
- package/dist/agents/control_flow/path-analyzer.d.ts +287 -0
- package/dist/agents/control_flow/path-analyzer.d.ts.map +1 -0
- package/dist/agents/control_flow/path-analyzer.js +695 -0
- package/dist/agents/control_flow/path-analyzer.js.map +1 -0
- package/dist/agents/control_flow/pattern-validator.d.ts +132 -0
- package/dist/agents/control_flow/pattern-validator.d.ts.map +1 -0
- package/dist/agents/control_flow/pattern-validator.js +420 -0
- package/dist/agents/control_flow/pattern-validator.js.map +1 -0
- package/dist/agents/control_flow/timeout-regex.d.ts +144 -0
- package/dist/agents/control_flow/timeout-regex.d.ts.map +1 -0
- package/dist/agents/control_flow/timeout-regex.js +339 -0
- package/dist/agents/control_flow/timeout-regex.js.map +1 -0
- package/dist/agents/control_flow/types.d.ts +782 -0
- package/dist/agents/control_flow/types.d.ts.map +1 -0
- package/dist/agents/control_flow/types.js +428 -0
- package/dist/agents/control_flow/types.js.map +1 -0
- package/dist/agents/control_flow/vulnerability-detector.d.ts +85 -0
- package/dist/agents/control_flow/vulnerability-detector.d.ts.map +1 -0
- package/dist/agents/control_flow/vulnerability-detector.js +493 -0
- package/dist/agents/control_flow/vulnerability-detector.js.map +1 -0
- package/dist/agents/date-utils.d.ts +19 -0
- package/dist/agents/date-utils.d.ts.map +1 -0
- package/dist/agents/date-utils.js +29 -0
- package/dist/agents/date-utils.js.map +1 -0
- package/dist/agents/index.d.ts +25 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +50 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/json-utils.d.ts +34 -0
- package/dist/agents/json-utils.d.ts.map +1 -0
- package/dist/agents/json-utils.js +62 -0
- package/dist/agents/json-utils.js.map +1 -0
- package/dist/agents/local_llm.d.ts +24 -0
- package/dist/agents/local_llm.d.ts.map +1 -0
- package/dist/agents/local_llm.js +566 -0
- package/dist/agents/local_llm.js.map +1 -0
- package/dist/agents/metadata.d.ts +57 -0
- package/dist/agents/metadata.d.ts.map +1 -0
- package/dist/agents/metadata.js +45 -0
- package/dist/agents/metadata.js.map +1 -0
- package/dist/agents/opencode.d.ts +18 -0
- package/dist/agents/opencode.d.ts.map +1 -0
- package/dist/agents/opencode.js +364 -0
- package/dist/agents/opencode.js.map +1 -0
- package/dist/agents/path-filter.d.ts +25 -0
- package/dist/agents/path-filter.d.ts.map +1 -0
- package/dist/agents/path-filter.js +43 -0
- package/dist/agents/path-filter.js.map +1 -0
- package/dist/agents/pr_agent.d.ts +3 -0
- package/dist/agents/pr_agent.d.ts.map +1 -0
- package/dist/agents/pr_agent.js +312 -0
- package/dist/agents/pr_agent.js.map +1 -0
- package/dist/agents/retry.d.ts +12 -0
- package/dist/agents/retry.d.ts.map +1 -0
- package/dist/agents/retry.js +65 -0
- package/dist/agents/retry.js.map +1 -0
- package/dist/agents/reviewdog.d.ts +24 -0
- package/dist/agents/reviewdog.d.ts.map +1 -0
- package/dist/agents/reviewdog.js +259 -0
- package/dist/agents/reviewdog.js.map +1 -0
- package/dist/agents/security.d.ts +49 -0
- package/dist/agents/security.d.ts.map +1 -0
- package/dist/agents/security.js +302 -0
- package/dist/agents/security.js.map +1 -0
- package/dist/agents/semgrep.d.ts +8 -0
- package/dist/agents/semgrep.d.ts.map +1 -0
- package/dist/agents/semgrep.js +157 -0
- package/dist/agents/semgrep.js.map +1 -0
- package/dist/agents/types.d.ts +450 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/agents/types.js +127 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/budget.d.ts +59 -0
- package/dist/budget.d.ts.map +1 -0
- package/dist/budget.js +82 -0
- package/dist/budget.js.map +1 -0
- package/dist/cache/key.d.ts +49 -0
- package/dist/cache/key.d.ts.map +1 -0
- package/dist/cache/key.js +71 -0
- package/dist/cache/key.js.map +1 -0
- package/dist/cache/store.d.ts +47 -0
- package/dist/cache/store.d.ts.map +1 -0
- package/dist/cache/store.js +328 -0
- package/dist/cache/store.js.map +1 -0
- package/dist/cli/commands/check.d.ts +60 -0
- package/dist/cli/commands/check.d.ts.map +1 -0
- package/dist/cli/commands/check.js +163 -0
- package/dist/cli/commands/check.js.map +1 -0
- package/dist/cli/commands/index.d.ts +12 -0
- package/dist/cli/commands/index.d.ts.map +1 -0
- package/dist/cli/commands/index.js +12 -0
- package/dist/cli/commands/index.js.map +1 -0
- package/dist/cli/commands/local-review.d.ts +149 -0
- package/dist/cli/commands/local-review.d.ts.map +1 -0
- package/dist/cli/commands/local-review.js +755 -0
- package/dist/cli/commands/local-review.js.map +1 -0
- package/dist/cli/config-wizard.d.ts +87 -0
- package/dist/cli/config-wizard.d.ts.map +1 -0
- package/dist/cli/config-wizard.js +240 -0
- package/dist/cli/config-wizard.js.map +1 -0
- package/dist/cli/dependencies/catalog.d.ts +44 -0
- package/dist/cli/dependencies/catalog.d.ts.map +1 -0
- package/dist/cli/dependencies/catalog.js +89 -0
- package/dist/cli/dependencies/catalog.js.map +1 -0
- package/dist/cli/dependencies/checker.d.ts +42 -0
- package/dist/cli/dependencies/checker.d.ts.map +1 -0
- package/dist/cli/dependencies/checker.js +240 -0
- package/dist/cli/dependencies/checker.js.map +1 -0
- package/dist/cli/dependencies/index.d.ts +16 -0
- package/dist/cli/dependencies/index.d.ts.map +1 -0
- package/dist/cli/dependencies/index.js +16 -0
- package/dist/cli/dependencies/index.js.map +1 -0
- package/dist/cli/dependencies/messages.d.ts +58 -0
- package/dist/cli/dependencies/messages.d.ts.map +1 -0
- package/dist/cli/dependencies/messages.js +183 -0
- package/dist/cli/dependencies/messages.js.map +1 -0
- package/dist/cli/dependencies/platform.d.ts +25 -0
- package/dist/cli/dependencies/platform.d.ts.map +1 -0
- package/dist/cli/dependencies/platform.js +42 -0
- package/dist/cli/dependencies/platform.js.map +1 -0
- package/dist/cli/dependencies/schemas.d.ts +65 -0
- package/dist/cli/dependencies/schemas.d.ts.map +1 -0
- package/dist/cli/dependencies/schemas.js +42 -0
- package/dist/cli/dependencies/schemas.js.map +1 -0
- package/dist/cli/dependencies/types.d.ts +112 -0
- package/dist/cli/dependencies/types.d.ts.map +1 -0
- package/dist/cli/dependencies/types.js +6 -0
- package/dist/cli/dependencies/types.js.map +1 -0
- package/dist/cli/dependencies/version.d.ts +67 -0
- package/dist/cli/dependencies/version.d.ts.map +1 -0
- package/dist/cli/dependencies/version.js +125 -0
- package/dist/cli/dependencies/version.js.map +1 -0
- package/dist/cli/git-context.d.ts +105 -0
- package/dist/cli/git-context.d.ts.map +1 -0
- package/dist/cli/git-context.js +313 -0
- package/dist/cli/git-context.js.map +1 -0
- package/dist/cli/interactive-prompts.d.ts +126 -0
- package/dist/cli/interactive-prompts.d.ts.map +1 -0
- package/dist/cli/interactive-prompts.js +128 -0
- package/dist/cli/interactive-prompts.js.map +1 -0
- package/dist/cli/options/index.d.ts +7 -0
- package/dist/cli/options/index.d.ts.map +1 -0
- package/dist/cli/options/index.js +11 -0
- package/dist/cli/options/index.js.map +1 -0
- package/dist/cli/options/local-review-options.d.ts +221 -0
- package/dist/cli/options/local-review-options.d.ts.map +1 -0
- package/dist/cli/options/local-review-options.js +332 -0
- package/dist/cli/options/local-review-options.js.map +1 -0
- package/dist/cli/output/colors.d.ts +154 -0
- package/dist/cli/output/colors.d.ts.map +1 -0
- package/dist/cli/output/colors.js +255 -0
- package/dist/cli/output/colors.js.map +1 -0
- package/dist/cli/output/errors.d.ts +157 -0
- package/dist/cli/output/errors.d.ts.map +1 -0
- package/dist/cli/output/errors.js +266 -0
- package/dist/cli/output/errors.js.map +1 -0
- package/dist/cli/output/index.d.ts +12 -0
- package/dist/cli/output/index.d.ts.map +1 -0
- package/dist/cli/output/index.js +15 -0
- package/dist/cli/output/index.js.map +1 -0
- package/dist/cli/output/progress.d.ts +237 -0
- package/dist/cli/output/progress.d.ts.map +1 -0
- package/dist/cli/output/progress.js +405 -0
- package/dist/cli/output/progress.js.map +1 -0
- package/dist/cli/signals.d.ts +145 -0
- package/dist/cli/signals.d.ts.map +1 -0
- package/dist/cli/signals.js +223 -0
- package/dist/cli/signals.js.map +1 -0
- package/dist/cli/validation-report.d.ts +106 -0
- package/dist/cli/validation-report.d.ts.map +1 -0
- package/dist/cli/validation-report.js +108 -0
- package/dist/cli/validation-report.js.map +1 -0
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +12 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/mitigation-config.d.ts +94 -0
- package/dist/config/mitigation-config.d.ts.map +1 -0
- package/dist/config/mitigation-config.js +430 -0
- package/dist/config/mitigation-config.js.map +1 -0
- package/dist/config/providers.d.ts +118 -0
- package/dist/config/providers.d.ts.map +1 -0
- package/dist/config/providers.js +229 -0
- package/dist/config/providers.js.map +1 -0
- package/dist/config/schemas.d.ts +278 -0
- package/dist/config/schemas.d.ts.map +1 -0
- package/dist/config/schemas.js +111 -0
- package/dist/config/schemas.js.map +1 -0
- package/dist/config/zero-config.d.ts +126 -0
- package/dist/config/zero-config.d.ts.map +1 -0
- package/dist/config/zero-config.js +243 -0
- package/dist/config/zero-config.js.map +1 -0
- package/dist/config.d.ts +110 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +302 -0
- package/dist/config.js.map +1 -0
- package/dist/diff.d.ts +224 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +832 -0
- package/dist/diff.js.map +1 -0
- package/dist/git-validators.d.ts +106 -0
- package/dist/git-validators.d.ts.map +1 -0
- package/dist/git-validators.js +224 -0
- package/dist/git-validators.js.map +1 -0
- package/dist/main.d.ts +61 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +704 -0
- package/dist/main.js.map +1 -0
- package/dist/phases/execute.d.ts +60 -0
- package/dist/phases/execute.d.ts.map +1 -0
- package/dist/phases/execute.js +168 -0
- package/dist/phases/execute.js.map +1 -0
- package/dist/phases/index.d.ts +9 -0
- package/dist/phases/index.d.ts.map +1 -0
- package/dist/phases/index.js +9 -0
- package/dist/phases/index.js.map +1 -0
- package/dist/phases/preflight.d.ts +40 -0
- package/dist/phases/preflight.d.ts.map +1 -0
- package/dist/phases/preflight.js +122 -0
- package/dist/phases/preflight.js.map +1 -0
- package/dist/phases/report.d.ts +51 -0
- package/dist/phases/report.d.ts.map +1 -0
- package/dist/phases/report.js +152 -0
- package/dist/phases/report.js.map +1 -0
- package/dist/policy.d.ts +33 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +34 -0
- package/dist/policy.js.map +1 -0
- package/dist/preflight.d.ts +181 -0
- package/dist/preflight.d.ts.map +1 -0
- package/dist/preflight.js +627 -0
- package/dist/preflight.js.map +1 -0
- package/dist/report/ado.d.ts +53 -0
- package/dist/report/ado.d.ts.map +1 -0
- package/dist/report/ado.js +411 -0
- package/dist/report/ado.js.map +1 -0
- package/dist/report/agent-icons.d.ts +36 -0
- package/dist/report/agent-icons.d.ts.map +1 -0
- package/dist/report/agent-icons.js +46 -0
- package/dist/report/agent-icons.js.map +1 -0
- package/dist/report/base.d.ts +30 -0
- package/dist/report/base.d.ts.map +1 -0
- package/dist/report/base.js +64 -0
- package/dist/report/base.js.map +1 -0
- package/dist/report/formats.d.ts +206 -0
- package/dist/report/formats.d.ts.map +1 -0
- package/dist/report/formats.js +481 -0
- package/dist/report/formats.js.map +1 -0
- package/dist/report/github.d.ts +44 -0
- package/dist/report/github.d.ts.map +1 -0
- package/dist/report/github.js +409 -0
- package/dist/report/github.js.map +1 -0
- package/dist/report/line-resolver.d.ts +208 -0
- package/dist/report/line-resolver.d.ts.map +1 -0
- package/dist/report/line-resolver.js +578 -0
- package/dist/report/line-resolver.js.map +1 -0
- package/dist/report/resolution.d.ts +158 -0
- package/dist/report/resolution.d.ts.map +1 -0
- package/dist/report/resolution.js +272 -0
- package/dist/report/resolution.js.map +1 -0
- package/dist/report/sanitize.d.ts +32 -0
- package/dist/report/sanitize.d.ts.map +1 -0
- package/dist/report/sanitize.js +84 -0
- package/dist/report/sanitize.js.map +1 -0
- package/dist/report/terminal.d.ts +440 -0
- package/dist/report/terminal.d.ts.map +1 -0
- package/dist/report/terminal.js +840 -0
- package/dist/report/terminal.js.map +1 -0
- package/dist/reviewignore.d.ts +125 -0
- package/dist/reviewignore.d.ts.map +1 -0
- package/dist/reviewignore.js +335 -0
- package/dist/reviewignore.js.map +1 -0
- package/dist/security-logger.d.ts +178 -0
- package/dist/security-logger.d.ts.map +1 -0
- package/dist/security-logger.js +256 -0
- package/dist/security-logger.js.map +1 -0
- package/dist/telemetry/backends/console.d.ts +24 -0
- package/dist/telemetry/backends/console.d.ts.map +1 -0
- package/dist/telemetry/backends/console.js +54 -0
- package/dist/telemetry/backends/console.js.map +1 -0
- package/dist/telemetry/backends/jsonl.d.ts +31 -0
- package/dist/telemetry/backends/jsonl.d.ts.map +1 -0
- package/dist/telemetry/backends/jsonl.js +121 -0
- package/dist/telemetry/backends/jsonl.js.map +1 -0
- package/dist/telemetry/emitter.d.ts +43 -0
- package/dist/telemetry/emitter.d.ts.map +1 -0
- package/dist/telemetry/emitter.js +83 -0
- package/dist/telemetry/emitter.js.map +1 -0
- package/dist/telemetry/hook.d.ts +53 -0
- package/dist/telemetry/hook.d.ts.map +1 -0
- package/dist/telemetry/hook.js +118 -0
- package/dist/telemetry/hook.js.map +1 -0
- package/dist/telemetry/index.d.ts +58 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +143 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/telemetry/types.d.ts +139 -0
- package/dist/telemetry/types.d.ts.map +1 -0
- package/dist/telemetry/types.js +133 -0
- package/dist/telemetry/types.js.map +1 -0
- package/dist/trust.d.ts +65 -0
- package/dist/trust.d.ts.map +1 -0
- package/dist/trust.js +78 -0
- package/dist/trust.js.map +1 -0
- package/dist/types/assert-never.d.ts +30 -0
- package/dist/types/assert-never.d.ts.map +1 -0
- package/dist/types/assert-never.js +32 -0
- package/dist/types/assert-never.js.map +1 -0
- package/dist/types/branded.d.ts +172 -0
- package/dist/types/branded.d.ts.map +1 -0
- package/dist/types/branded.js +262 -0
- package/dist/types/branded.js.map +1 -0
- package/dist/types/errors.d.ts +320 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/errors.js +551 -0
- package/dist/types/errors.js.map +1 -0
- package/dist/types/index.d.ts +37 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +77 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/result.d.ts +323 -0
- package/dist/types/result.d.ts.map +1 -0
- package/dist/types/result.js +423 -0
- package/dist/types/result.js.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Control Flow Graph Builder
|
|
3
|
+
*
|
|
4
|
+
* Constructs control flow graphs from TypeScript/JavaScript AST.
|
|
5
|
+
* Implements FR-001 (track data flow through control structures) and
|
|
6
|
+
* FR-002 (language-specific CFG construction).
|
|
7
|
+
*/
|
|
8
|
+
import ts from 'typescript';
|
|
9
|
+
import { createBuilderContext, generateNodeId, createNode, addEdge, getLineNumber, getEndLineNumber, } from './cfg-types.js';
|
|
10
|
+
/**
|
|
11
|
+
* Build a control flow graph for a function
|
|
12
|
+
*/
|
|
13
|
+
export function buildCFG(functionNode, sourceFile, filePath) {
|
|
14
|
+
const ctx = createBuilderContext(sourceFile, filePath);
|
|
15
|
+
// Get function name and check if async
|
|
16
|
+
const functionName = getFunctionName(functionNode);
|
|
17
|
+
const startLine = getLineNumber(functionNode, sourceFile);
|
|
18
|
+
const endLine = getEndLineNumber(functionNode, sourceFile);
|
|
19
|
+
const functionId = `${filePath}:${startLine}:${functionName}`;
|
|
20
|
+
const isAsync = isAsyncFunction(functionNode);
|
|
21
|
+
// Track await boundaries
|
|
22
|
+
const awaitBoundaries = [];
|
|
23
|
+
// Create entry node
|
|
24
|
+
const entryId = generateNodeId(ctx, 'entry');
|
|
25
|
+
const entryNode = createNode(entryId, 'entry', startLine, startLine);
|
|
26
|
+
ctx.nodes.set(entryId, entryNode);
|
|
27
|
+
ctx.entryNode = entryId;
|
|
28
|
+
// Process function body
|
|
29
|
+
const body = functionNode.body;
|
|
30
|
+
if (body) {
|
|
31
|
+
if (ts.isBlock(body)) {
|
|
32
|
+
const exitId = processBlock(ctx, body, entryId);
|
|
33
|
+
if (exitId) {
|
|
34
|
+
// Create implicit exit node if function doesn't explicitly return
|
|
35
|
+
const implicitExitId = generateNodeId(ctx, 'exit');
|
|
36
|
+
const implicitExit = createNode(implicitExitId, 'exit', endLine, endLine);
|
|
37
|
+
ctx.nodes.set(implicitExitId, implicitExit);
|
|
38
|
+
addEdge(ctx, exitId, implicitExitId, 'sequential');
|
|
39
|
+
ctx.exitNodes.push(implicitExitId);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
// Arrow function with expression body - check for await
|
|
44
|
+
const awaits = findAwaitExpressions(body, sourceFile);
|
|
45
|
+
if (awaits.length > 0 && isAsync && awaits[0]) {
|
|
46
|
+
// Create await node for the expression
|
|
47
|
+
const awaitExpr = awaits[0];
|
|
48
|
+
const awaitNodeId = generateNodeId(ctx, 'await');
|
|
49
|
+
const awaitNode = createNode(awaitNodeId, 'await', getLineNumber(awaitExpr, sourceFile), getEndLineNumber(awaitExpr, sourceFile));
|
|
50
|
+
awaitNode.awaitExpression = awaitExpr;
|
|
51
|
+
awaitNode.isAsyncBoundary = true;
|
|
52
|
+
ctx.nodes.set(awaitNodeId, awaitNode);
|
|
53
|
+
addEdge(ctx, entryId, awaitNodeId, 'await');
|
|
54
|
+
awaitBoundaries.push(awaitNodeId);
|
|
55
|
+
// Check for call expressions in the await
|
|
56
|
+
if (ts.isCallExpression(awaitExpr.expression)) {
|
|
57
|
+
findCallExpressions(ctx, awaitExpr.expression, awaitNodeId);
|
|
58
|
+
}
|
|
59
|
+
// Create exit node
|
|
60
|
+
const exitId = generateNodeId(ctx, 'exit');
|
|
61
|
+
const exitNode = createNode(exitId, 'exit', endLine, endLine);
|
|
62
|
+
ctx.nodes.set(exitId, exitNode);
|
|
63
|
+
addEdge(ctx, awaitNodeId, exitId, 'return');
|
|
64
|
+
ctx.exitNodes.push(exitId);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// No await - regular expression body
|
|
68
|
+
const bodyNodeId = generateNodeId(ctx, 'body');
|
|
69
|
+
const bodyNode = createNode(bodyNodeId, 'basic', getLineNumber(body, sourceFile), getEndLineNumber(body, sourceFile));
|
|
70
|
+
bodyNode.statements.push(ts.factory.createReturnStatement(body));
|
|
71
|
+
ctx.nodes.set(bodyNodeId, bodyNode);
|
|
72
|
+
addEdge(ctx, entryId, bodyNodeId, 'sequential');
|
|
73
|
+
// Check for call expressions
|
|
74
|
+
findCallExpressions(ctx, body, bodyNodeId);
|
|
75
|
+
// Create exit node
|
|
76
|
+
const exitId = generateNodeId(ctx, 'exit');
|
|
77
|
+
const exitNode = createNode(exitId, 'exit', endLine, endLine);
|
|
78
|
+
ctx.nodes.set(exitId, exitNode);
|
|
79
|
+
addEdge(ctx, bodyNodeId, exitId, 'return');
|
|
80
|
+
ctx.exitNodes.push(exitId);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// No body (declaration only)
|
|
86
|
+
const exitId = generateNodeId(ctx, 'exit');
|
|
87
|
+
const exitNode = createNode(exitId, 'exit', endLine, endLine);
|
|
88
|
+
ctx.nodes.set(exitId, exitNode);
|
|
89
|
+
addEdge(ctx, entryId, exitId, 'sequential');
|
|
90
|
+
ctx.exitNodes.push(exitId);
|
|
91
|
+
}
|
|
92
|
+
// Collect all await boundaries from nodes
|
|
93
|
+
for (const [nodeId, node] of ctx.nodes) {
|
|
94
|
+
if (node.type === 'await' && !awaitBoundaries.includes(nodeId)) {
|
|
95
|
+
awaitBoundaries.push(nodeId);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
functionId,
|
|
100
|
+
functionName,
|
|
101
|
+
filePath,
|
|
102
|
+
startLine,
|
|
103
|
+
endLine,
|
|
104
|
+
nodes: ctx.nodes,
|
|
105
|
+
edges: ctx.edges,
|
|
106
|
+
entryNode: ctx.entryNode ?? entryId,
|
|
107
|
+
exitNodes: ctx.exitNodes,
|
|
108
|
+
callSites: ctx.callSites,
|
|
109
|
+
functionNode,
|
|
110
|
+
isAsync,
|
|
111
|
+
awaitBoundaries,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Process a block of statements, returning the last node ID or null if all paths exit
|
|
116
|
+
*/
|
|
117
|
+
function processBlock(ctx, block, entryPoint) {
|
|
118
|
+
let currentNode = entryPoint;
|
|
119
|
+
for (const statement of block.statements) {
|
|
120
|
+
const result = processStatement(ctx, statement, currentNode);
|
|
121
|
+
if (result === null) {
|
|
122
|
+
// All paths from this statement exit (return/throw)
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
currentNode = result;
|
|
126
|
+
}
|
|
127
|
+
return currentNode;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Process a single statement, returning the exit node ID or null if it exits
|
|
131
|
+
*/
|
|
132
|
+
function processStatement(ctx, statement, entryPoint) {
|
|
133
|
+
const sourceFile = ctx.sourceFile;
|
|
134
|
+
const lineStart = getLineNumber(statement, sourceFile);
|
|
135
|
+
const lineEnd = getEndLineNumber(statement, sourceFile);
|
|
136
|
+
// Handle different statement types
|
|
137
|
+
if (ts.isIfStatement(statement)) {
|
|
138
|
+
return processIfStatement(ctx, statement, entryPoint);
|
|
139
|
+
}
|
|
140
|
+
if (ts.isSwitchStatement(statement)) {
|
|
141
|
+
return processSwitchStatement(ctx, statement, entryPoint);
|
|
142
|
+
}
|
|
143
|
+
if (ts.isForStatement(statement) ||
|
|
144
|
+
ts.isForInStatement(statement) ||
|
|
145
|
+
ts.isForOfStatement(statement)) {
|
|
146
|
+
return processForLoop(ctx, statement, entryPoint);
|
|
147
|
+
}
|
|
148
|
+
if (ts.isWhileStatement(statement)) {
|
|
149
|
+
return processWhileLoop(ctx, statement, entryPoint);
|
|
150
|
+
}
|
|
151
|
+
if (ts.isDoStatement(statement)) {
|
|
152
|
+
return processDoWhileLoop(ctx, statement, entryPoint);
|
|
153
|
+
}
|
|
154
|
+
if (ts.isTryStatement(statement)) {
|
|
155
|
+
return processTryStatement(ctx, statement, entryPoint);
|
|
156
|
+
}
|
|
157
|
+
if (ts.isReturnStatement(statement)) {
|
|
158
|
+
return processReturnStatement(ctx, statement, entryPoint);
|
|
159
|
+
}
|
|
160
|
+
if (ts.isThrowStatement(statement)) {
|
|
161
|
+
return processThrowStatement(ctx, statement, entryPoint);
|
|
162
|
+
}
|
|
163
|
+
if (ts.isBreakStatement(statement)) {
|
|
164
|
+
return processBreakStatement(ctx, statement, entryPoint);
|
|
165
|
+
}
|
|
166
|
+
if (ts.isContinueStatement(statement)) {
|
|
167
|
+
return processContinueStatement(ctx, statement, entryPoint);
|
|
168
|
+
}
|
|
169
|
+
if (ts.isBlock(statement)) {
|
|
170
|
+
return processBlock(ctx, statement, entryPoint);
|
|
171
|
+
}
|
|
172
|
+
// Check for await expressions in the statement
|
|
173
|
+
const awaits = findAwaitExpressions(statement, sourceFile);
|
|
174
|
+
if (awaits.length > 0) {
|
|
175
|
+
// Statement contains await - create await node(s)
|
|
176
|
+
return processAwaitStatement(ctx, statement, entryPoint, awaits);
|
|
177
|
+
}
|
|
178
|
+
// Default: basic block statement
|
|
179
|
+
const nodeId = generateNodeId(ctx, 'basic');
|
|
180
|
+
const node = createNode(nodeId, 'basic', lineStart, lineEnd);
|
|
181
|
+
node.statements.push(statement);
|
|
182
|
+
ctx.nodes.set(nodeId, node);
|
|
183
|
+
addEdge(ctx, entryPoint, nodeId, 'sequential');
|
|
184
|
+
// Find call expressions in this statement
|
|
185
|
+
findCallExpressions(ctx, statement, nodeId);
|
|
186
|
+
return nodeId;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Process a statement containing await expressions.
|
|
190
|
+
* Creates await nodes to represent async boundaries.
|
|
191
|
+
*/
|
|
192
|
+
function processAwaitStatement(ctx, statement, entryPoint, awaits) {
|
|
193
|
+
const sourceFile = ctx.sourceFile;
|
|
194
|
+
const firstAwait = awaits[0];
|
|
195
|
+
// Guard against empty awaits array (should not happen based on call sites)
|
|
196
|
+
if (!firstAwait) {
|
|
197
|
+
// Fall back to basic node
|
|
198
|
+
const nodeId = generateNodeId(ctx, 'basic');
|
|
199
|
+
const node = createNode(nodeId, 'basic', getLineNumber(statement, sourceFile), getEndLineNumber(statement, sourceFile));
|
|
200
|
+
node.statements.push(statement);
|
|
201
|
+
ctx.nodes.set(nodeId, node);
|
|
202
|
+
addEdge(ctx, entryPoint, nodeId, 'sequential');
|
|
203
|
+
return nodeId;
|
|
204
|
+
}
|
|
205
|
+
// For simplicity, we create a single await node for the statement
|
|
206
|
+
// In a more sophisticated implementation, we could split around each await
|
|
207
|
+
const lineStart = getLineNumber(statement, sourceFile);
|
|
208
|
+
const lineEnd = getEndLineNumber(statement, sourceFile);
|
|
209
|
+
// Create the await node
|
|
210
|
+
const awaitNodeId = generateNodeId(ctx, 'await');
|
|
211
|
+
const awaitNode = createNode(awaitNodeId, 'await', lineStart, lineEnd);
|
|
212
|
+
awaitNode.statements.push(statement);
|
|
213
|
+
awaitNode.awaitExpression = firstAwait;
|
|
214
|
+
awaitNode.isAsyncBoundary = true;
|
|
215
|
+
ctx.nodes.set(awaitNodeId, awaitNode);
|
|
216
|
+
addEdge(ctx, entryPoint, awaitNodeId, 'await');
|
|
217
|
+
// Find call expressions in the await (the awaited expression)
|
|
218
|
+
for (const awaitExpr of awaits) {
|
|
219
|
+
if (ts.isCallExpression(awaitExpr.expression)) {
|
|
220
|
+
findCallExpressions(ctx, awaitExpr.expression, awaitNodeId);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Also find other call expressions in the statement
|
|
224
|
+
findCallExpressions(ctx, statement, awaitNodeId);
|
|
225
|
+
return awaitNodeId;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Process an if statement
|
|
229
|
+
*/
|
|
230
|
+
function processIfStatement(ctx, stmt, entryPoint) {
|
|
231
|
+
const sourceFile = ctx.sourceFile;
|
|
232
|
+
const lineStart = getLineNumber(stmt, sourceFile);
|
|
233
|
+
// Create branch node for the condition
|
|
234
|
+
const branchId = generateNodeId(ctx, 'branch');
|
|
235
|
+
const branchNode = createNode(branchId, 'branch', lineStart, lineStart);
|
|
236
|
+
ctx.nodes.set(branchId, branchNode);
|
|
237
|
+
addEdge(ctx, entryPoint, branchId, 'sequential');
|
|
238
|
+
// Find call expressions in condition
|
|
239
|
+
findCallExpressions(ctx, stmt.expression, branchId);
|
|
240
|
+
// Create merge node for after the if
|
|
241
|
+
const mergeId = generateNodeId(ctx, 'merge');
|
|
242
|
+
const mergeNode = createNode(mergeId, 'merge', getEndLineNumber(stmt, sourceFile), getEndLineNumber(stmt, sourceFile));
|
|
243
|
+
ctx.nodes.set(mergeId, mergeNode);
|
|
244
|
+
// Process then branch
|
|
245
|
+
const thenEntry = generateNodeId(ctx, 'then');
|
|
246
|
+
const thenNode = createNode(thenEntry, 'basic', getLineNumber(stmt.thenStatement, sourceFile), getLineNumber(stmt.thenStatement, sourceFile));
|
|
247
|
+
ctx.nodes.set(thenEntry, thenNode);
|
|
248
|
+
addEdge(ctx, branchId, thenEntry, 'branch_true', stmt.expression, true);
|
|
249
|
+
let thenExit;
|
|
250
|
+
if (ts.isBlock(stmt.thenStatement)) {
|
|
251
|
+
thenExit = processBlock(ctx, stmt.thenStatement, thenEntry);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
thenExit = processStatement(ctx, stmt.thenStatement, thenEntry);
|
|
255
|
+
}
|
|
256
|
+
if (thenExit !== null) {
|
|
257
|
+
addEdge(ctx, thenExit, mergeId, 'sequential');
|
|
258
|
+
}
|
|
259
|
+
// Process else branch
|
|
260
|
+
let elseExit = null;
|
|
261
|
+
if (stmt.elseStatement) {
|
|
262
|
+
const elseEntry = generateNodeId(ctx, 'else');
|
|
263
|
+
const elseNode = createNode(elseEntry, 'basic', getLineNumber(stmt.elseStatement, sourceFile), getLineNumber(stmt.elseStatement, sourceFile));
|
|
264
|
+
ctx.nodes.set(elseEntry, elseNode);
|
|
265
|
+
addEdge(ctx, branchId, elseEntry, 'branch_false', stmt.expression, false);
|
|
266
|
+
if (ts.isBlock(stmt.elseStatement)) {
|
|
267
|
+
elseExit = processBlock(ctx, stmt.elseStatement, elseEntry);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
elseExit = processStatement(ctx, stmt.elseStatement, elseEntry);
|
|
271
|
+
}
|
|
272
|
+
if (elseExit !== null) {
|
|
273
|
+
addEdge(ctx, elseExit, mergeId, 'sequential');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
// No else branch - false condition goes directly to merge
|
|
278
|
+
addEdge(ctx, branchId, mergeId, 'branch_false', stmt.expression, false);
|
|
279
|
+
elseExit = mergeId;
|
|
280
|
+
}
|
|
281
|
+
// If both branches exit, the merge is unreachable
|
|
282
|
+
if (thenExit === null && (stmt.elseStatement ? elseExit === null : false)) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
return mergeId;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Process a switch statement
|
|
289
|
+
*/
|
|
290
|
+
function processSwitchStatement(ctx, stmt, entryPoint) {
|
|
291
|
+
const sourceFile = ctx.sourceFile;
|
|
292
|
+
const lineStart = getLineNumber(stmt, sourceFile);
|
|
293
|
+
// Create branch node for the switch expression
|
|
294
|
+
const branchId = generateNodeId(ctx, 'switch');
|
|
295
|
+
const branchNode = createNode(branchId, 'branch', lineStart, lineStart);
|
|
296
|
+
ctx.nodes.set(branchId, branchNode);
|
|
297
|
+
addEdge(ctx, entryPoint, branchId, 'sequential');
|
|
298
|
+
findCallExpressions(ctx, stmt.expression, branchId);
|
|
299
|
+
// Create merge node for after switch
|
|
300
|
+
const mergeId = generateNodeId(ctx, 'merge');
|
|
301
|
+
const mergeNode = createNode(mergeId, 'merge', getEndLineNumber(stmt, sourceFile), getEndLineNumber(stmt, sourceFile));
|
|
302
|
+
ctx.nodes.set(mergeId, mergeNode);
|
|
303
|
+
// Push break target
|
|
304
|
+
ctx.breakTargets.push(mergeId);
|
|
305
|
+
let hasDefault = false;
|
|
306
|
+
let allCasesExit = true;
|
|
307
|
+
let fallthrough = null;
|
|
308
|
+
for (const clause of stmt.caseBlock.clauses) {
|
|
309
|
+
const caseId = generateNodeId(ctx, ts.isDefaultClause(clause) ? 'default' : 'case');
|
|
310
|
+
const caseNode = createNode(caseId, 'basic', getLineNumber(clause, sourceFile), getLineNumber(clause, sourceFile));
|
|
311
|
+
ctx.nodes.set(caseId, caseNode);
|
|
312
|
+
if (ts.isDefaultClause(clause)) {
|
|
313
|
+
hasDefault = true;
|
|
314
|
+
}
|
|
315
|
+
// Edge from branch to case (or fallthrough from previous case)
|
|
316
|
+
if (fallthrough) {
|
|
317
|
+
addEdge(ctx, fallthrough, caseId, 'sequential');
|
|
318
|
+
}
|
|
319
|
+
addEdge(ctx, branchId, caseId, ts.isDefaultClause(clause) ? 'branch_false' : 'branch_true');
|
|
320
|
+
// Process case statements
|
|
321
|
+
let caseExit = caseId;
|
|
322
|
+
for (const statement of clause.statements) {
|
|
323
|
+
if (caseExit === null)
|
|
324
|
+
break;
|
|
325
|
+
caseExit = processStatement(ctx, statement, caseExit);
|
|
326
|
+
}
|
|
327
|
+
if (caseExit !== null) {
|
|
328
|
+
fallthrough = caseExit;
|
|
329
|
+
allCasesExit = false;
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
fallthrough = null;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// Connect last fallthrough to merge
|
|
336
|
+
if (fallthrough) {
|
|
337
|
+
addEdge(ctx, fallthrough, mergeId, 'sequential');
|
|
338
|
+
}
|
|
339
|
+
// If no default case, switch can fall through without matching
|
|
340
|
+
if (!hasDefault) {
|
|
341
|
+
addEdge(ctx, branchId, mergeId, 'branch_false');
|
|
342
|
+
allCasesExit = false;
|
|
343
|
+
}
|
|
344
|
+
ctx.breakTargets.pop();
|
|
345
|
+
return allCasesExit ? null : mergeId;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Process a for loop (for, for-in, for-of)
|
|
349
|
+
*/
|
|
350
|
+
function processForLoop(ctx, stmt, entryPoint) {
|
|
351
|
+
const sourceFile = ctx.sourceFile;
|
|
352
|
+
const lineStart = getLineNumber(stmt, sourceFile);
|
|
353
|
+
// Create loop header node
|
|
354
|
+
const headerId = generateNodeId(ctx, 'loop_header');
|
|
355
|
+
const headerNode = createNode(headerId, 'loop_header', lineStart, lineStart);
|
|
356
|
+
ctx.nodes.set(headerId, headerNode);
|
|
357
|
+
// For standard for loop, process initializer first
|
|
358
|
+
if (ts.isForStatement(stmt) && stmt.initializer) {
|
|
359
|
+
const initId = generateNodeId(ctx, 'for_init');
|
|
360
|
+
const initNode = createNode(initId, 'basic', lineStart, lineStart);
|
|
361
|
+
if (ts.isVariableDeclarationList(stmt.initializer)) {
|
|
362
|
+
// Convert to statement for storage
|
|
363
|
+
initNode.statements.push(ts.factory.createVariableStatement(undefined, stmt.initializer));
|
|
364
|
+
}
|
|
365
|
+
ctx.nodes.set(initId, initNode);
|
|
366
|
+
addEdge(ctx, entryPoint, initId, 'sequential');
|
|
367
|
+
addEdge(ctx, initId, headerId, 'sequential');
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
addEdge(ctx, entryPoint, headerId, 'sequential');
|
|
371
|
+
}
|
|
372
|
+
// Create exit node for after loop
|
|
373
|
+
const exitId = generateNodeId(ctx, 'merge');
|
|
374
|
+
const exitNode = createNode(exitId, 'merge', getEndLineNumber(stmt, sourceFile), getEndLineNumber(stmt, sourceFile));
|
|
375
|
+
ctx.nodes.set(exitId, exitNode);
|
|
376
|
+
// Edge from header to exit (loop condition false)
|
|
377
|
+
addEdge(ctx, headerId, exitId, 'loop_exit');
|
|
378
|
+
// Push break/continue targets
|
|
379
|
+
ctx.breakTargets.push(exitId);
|
|
380
|
+
ctx.continueTargets.push(headerId);
|
|
381
|
+
// Create loop body node
|
|
382
|
+
const bodyId = generateNodeId(ctx, 'loop_body');
|
|
383
|
+
const bodyNode = createNode(bodyId, 'loop_body', getLineNumber(stmt.statement, sourceFile), getLineNumber(stmt.statement, sourceFile));
|
|
384
|
+
ctx.nodes.set(bodyId, bodyNode);
|
|
385
|
+
addEdge(ctx, headerId, bodyId, 'branch_true');
|
|
386
|
+
// Process body
|
|
387
|
+
let bodyExit;
|
|
388
|
+
if (ts.isBlock(stmt.statement)) {
|
|
389
|
+
bodyExit = processBlock(ctx, stmt.statement, bodyId);
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
bodyExit = processStatement(ctx, stmt.statement, bodyId);
|
|
393
|
+
}
|
|
394
|
+
// For standard for loop, process incrementor
|
|
395
|
+
if (ts.isForStatement(stmt) && stmt.incrementor && bodyExit !== null) {
|
|
396
|
+
const incId = generateNodeId(ctx, 'for_inc');
|
|
397
|
+
const incNode = createNode(incId, 'basic', getLineNumber(stmt.incrementor, sourceFile), getLineNumber(stmt.incrementor, sourceFile));
|
|
398
|
+
ctx.nodes.set(incId, incNode);
|
|
399
|
+
addEdge(ctx, bodyExit, incId, 'sequential');
|
|
400
|
+
addEdge(ctx, incId, headerId, 'loop_back');
|
|
401
|
+
}
|
|
402
|
+
else if (bodyExit !== null) {
|
|
403
|
+
addEdge(ctx, bodyExit, headerId, 'loop_back');
|
|
404
|
+
}
|
|
405
|
+
ctx.breakTargets.pop();
|
|
406
|
+
ctx.continueTargets.pop();
|
|
407
|
+
return exitId;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Process a while loop
|
|
411
|
+
*/
|
|
412
|
+
function processWhileLoop(ctx, stmt, entryPoint) {
|
|
413
|
+
const sourceFile = ctx.sourceFile;
|
|
414
|
+
const lineStart = getLineNumber(stmt, sourceFile);
|
|
415
|
+
// Create loop header
|
|
416
|
+
const headerId = generateNodeId(ctx, 'loop_header');
|
|
417
|
+
const headerNode = createNode(headerId, 'loop_header', lineStart, lineStart);
|
|
418
|
+
ctx.nodes.set(headerId, headerNode);
|
|
419
|
+
addEdge(ctx, entryPoint, headerId, 'sequential');
|
|
420
|
+
findCallExpressions(ctx, stmt.expression, headerId);
|
|
421
|
+
// Create exit node
|
|
422
|
+
const exitId = generateNodeId(ctx, 'merge');
|
|
423
|
+
const exitNode = createNode(exitId, 'merge', getEndLineNumber(stmt, sourceFile), getEndLineNumber(stmt, sourceFile));
|
|
424
|
+
ctx.nodes.set(exitId, exitNode);
|
|
425
|
+
addEdge(ctx, headerId, exitId, 'loop_exit', stmt.expression, false);
|
|
426
|
+
// Push targets
|
|
427
|
+
ctx.breakTargets.push(exitId);
|
|
428
|
+
ctx.continueTargets.push(headerId);
|
|
429
|
+
// Create body entry
|
|
430
|
+
const bodyId = generateNodeId(ctx, 'loop_body');
|
|
431
|
+
const bodyNode = createNode(bodyId, 'loop_body', getLineNumber(stmt.statement, sourceFile), getLineNumber(stmt.statement, sourceFile));
|
|
432
|
+
ctx.nodes.set(bodyId, bodyNode);
|
|
433
|
+
addEdge(ctx, headerId, bodyId, 'branch_true', stmt.expression, true);
|
|
434
|
+
// Process body
|
|
435
|
+
let bodyExit;
|
|
436
|
+
if (ts.isBlock(stmt.statement)) {
|
|
437
|
+
bodyExit = processBlock(ctx, stmt.statement, bodyId);
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
bodyExit = processStatement(ctx, stmt.statement, bodyId);
|
|
441
|
+
}
|
|
442
|
+
if (bodyExit !== null) {
|
|
443
|
+
addEdge(ctx, bodyExit, headerId, 'loop_back');
|
|
444
|
+
}
|
|
445
|
+
ctx.breakTargets.pop();
|
|
446
|
+
ctx.continueTargets.pop();
|
|
447
|
+
return exitId;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Process a do-while loop
|
|
451
|
+
*/
|
|
452
|
+
function processDoWhileLoop(ctx, stmt, entryPoint) {
|
|
453
|
+
const sourceFile = ctx.sourceFile;
|
|
454
|
+
const lineStart = getLineNumber(stmt, sourceFile);
|
|
455
|
+
// Create body entry (executed first in do-while)
|
|
456
|
+
const bodyId = generateNodeId(ctx, 'loop_body');
|
|
457
|
+
const bodyNode = createNode(bodyId, 'loop_body', lineStart, lineStart);
|
|
458
|
+
ctx.nodes.set(bodyId, bodyNode);
|
|
459
|
+
addEdge(ctx, entryPoint, bodyId, 'sequential');
|
|
460
|
+
// Create loop header (condition check at end)
|
|
461
|
+
const headerId = generateNodeId(ctx, 'loop_header');
|
|
462
|
+
const headerNode = createNode(headerId, 'loop_header', getLineNumber(stmt.expression, sourceFile), getEndLineNumber(stmt.expression, sourceFile));
|
|
463
|
+
ctx.nodes.set(headerId, headerNode);
|
|
464
|
+
findCallExpressions(ctx, stmt.expression, headerId);
|
|
465
|
+
// Create exit node
|
|
466
|
+
const exitId = generateNodeId(ctx, 'merge');
|
|
467
|
+
const exitNode = createNode(exitId, 'merge', getEndLineNumber(stmt, sourceFile), getEndLineNumber(stmt, sourceFile));
|
|
468
|
+
ctx.nodes.set(exitId, exitNode);
|
|
469
|
+
// Push targets
|
|
470
|
+
ctx.breakTargets.push(exitId);
|
|
471
|
+
ctx.continueTargets.push(headerId);
|
|
472
|
+
// Process body
|
|
473
|
+
let bodyExit;
|
|
474
|
+
if (ts.isBlock(stmt.statement)) {
|
|
475
|
+
bodyExit = processBlock(ctx, stmt.statement, bodyId);
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
bodyExit = processStatement(ctx, stmt.statement, bodyId);
|
|
479
|
+
}
|
|
480
|
+
if (bodyExit !== null) {
|
|
481
|
+
addEdge(ctx, bodyExit, headerId, 'sequential');
|
|
482
|
+
}
|
|
483
|
+
// Loop back or exit from header
|
|
484
|
+
addEdge(ctx, headerId, bodyId, 'loop_back', stmt.expression, true);
|
|
485
|
+
addEdge(ctx, headerId, exitId, 'loop_exit', stmt.expression, false);
|
|
486
|
+
ctx.breakTargets.pop();
|
|
487
|
+
ctx.continueTargets.pop();
|
|
488
|
+
return exitId;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Process a try statement
|
|
492
|
+
*/
|
|
493
|
+
function processTryStatement(ctx, stmt, entryPoint) {
|
|
494
|
+
const sourceFile = ctx.sourceFile;
|
|
495
|
+
const lineEnd = getEndLineNumber(stmt, sourceFile);
|
|
496
|
+
// Create merge node for after try/catch/finally
|
|
497
|
+
const mergeId = generateNodeId(ctx, 'merge');
|
|
498
|
+
const mergeNode = createNode(mergeId, 'merge', lineEnd, lineEnd);
|
|
499
|
+
ctx.nodes.set(mergeId, mergeNode);
|
|
500
|
+
// Process try block
|
|
501
|
+
const tryExit = processBlock(ctx, stmt.tryBlock, entryPoint);
|
|
502
|
+
// Process catch clause
|
|
503
|
+
let catchExit = null;
|
|
504
|
+
if (stmt.catchClause) {
|
|
505
|
+
const catchId = generateNodeId(ctx, 'catch');
|
|
506
|
+
const catchNode = createNode(catchId, 'basic', getLineNumber(stmt.catchClause, sourceFile), getLineNumber(stmt.catchClause, sourceFile));
|
|
507
|
+
ctx.nodes.set(catchId, catchNode);
|
|
508
|
+
// Exception edge from try entry to catch
|
|
509
|
+
addEdge(ctx, entryPoint, catchId, 'exception');
|
|
510
|
+
catchExit = processBlock(ctx, stmt.catchClause.block, catchId);
|
|
511
|
+
}
|
|
512
|
+
// Process finally clause
|
|
513
|
+
if (stmt.finallyBlock) {
|
|
514
|
+
const finallyId = generateNodeId(ctx, 'finally');
|
|
515
|
+
const finallyNode = createNode(finallyId, 'basic', getLineNumber(stmt.finallyBlock, sourceFile), getLineNumber(stmt.finallyBlock, sourceFile));
|
|
516
|
+
ctx.nodes.set(finallyId, finallyNode);
|
|
517
|
+
// Connect try exit to finally
|
|
518
|
+
if (tryExit !== null) {
|
|
519
|
+
addEdge(ctx, tryExit, finallyId, 'sequential');
|
|
520
|
+
}
|
|
521
|
+
// Connect catch exit to finally
|
|
522
|
+
if (catchExit !== null) {
|
|
523
|
+
addEdge(ctx, catchExit, finallyId, 'sequential');
|
|
524
|
+
}
|
|
525
|
+
const finallyExit = processBlock(ctx, stmt.finallyBlock, finallyId);
|
|
526
|
+
if (finallyExit !== null) {
|
|
527
|
+
addEdge(ctx, finallyExit, mergeId, 'sequential');
|
|
528
|
+
}
|
|
529
|
+
return finallyExit !== null ? mergeId : null;
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
// No finally - connect try/catch exits to merge
|
|
533
|
+
if (tryExit !== null) {
|
|
534
|
+
addEdge(ctx, tryExit, mergeId, 'sequential');
|
|
535
|
+
}
|
|
536
|
+
if (catchExit !== null) {
|
|
537
|
+
addEdge(ctx, catchExit, mergeId, 'sequential');
|
|
538
|
+
}
|
|
539
|
+
return tryExit !== null || catchExit !== null ? mergeId : null;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Process a return statement
|
|
544
|
+
*/
|
|
545
|
+
function processReturnStatement(ctx, stmt, entryPoint) {
|
|
546
|
+
const sourceFile = ctx.sourceFile;
|
|
547
|
+
const lineStart = getLineNumber(stmt, sourceFile);
|
|
548
|
+
const lineEnd = getEndLineNumber(stmt, sourceFile);
|
|
549
|
+
// Check for await in the return expression (e.g., return await foo())
|
|
550
|
+
if (stmt.expression) {
|
|
551
|
+
const awaits = findAwaitExpressions(stmt.expression, sourceFile);
|
|
552
|
+
if (awaits.length > 0) {
|
|
553
|
+
// Create await node before exit
|
|
554
|
+
const awaitNodeId = generateNodeId(ctx, 'await');
|
|
555
|
+
const awaitNode = createNode(awaitNodeId, 'await', lineStart, lineEnd);
|
|
556
|
+
awaitNode.statements.push(stmt);
|
|
557
|
+
awaitNode.awaitExpression = awaits[0];
|
|
558
|
+
awaitNode.isAsyncBoundary = true;
|
|
559
|
+
ctx.nodes.set(awaitNodeId, awaitNode);
|
|
560
|
+
addEdge(ctx, entryPoint, awaitNodeId, 'await');
|
|
561
|
+
// Find call expressions in the await
|
|
562
|
+
for (const awaitExpr of awaits) {
|
|
563
|
+
if (ts.isCallExpression(awaitExpr.expression)) {
|
|
564
|
+
findCallExpressions(ctx, awaitExpr.expression, awaitNodeId);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
// Create exit node connected from await
|
|
568
|
+
const exitId = generateNodeId(ctx, 'exit');
|
|
569
|
+
const exitNode = createNode(exitId, 'exit', lineStart, lineEnd);
|
|
570
|
+
ctx.nodes.set(exitId, exitNode);
|
|
571
|
+
addEdge(ctx, awaitNodeId, exitId, 'return');
|
|
572
|
+
ctx.exitNodes.push(exitId);
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
// No await - normal return processing
|
|
577
|
+
const exitId = generateNodeId(ctx, 'exit');
|
|
578
|
+
const exitNode = createNode(exitId, 'exit', lineStart, lineEnd);
|
|
579
|
+
exitNode.statements.push(stmt);
|
|
580
|
+
ctx.nodes.set(exitId, exitNode);
|
|
581
|
+
addEdge(ctx, entryPoint, exitId, 'return');
|
|
582
|
+
ctx.exitNodes.push(exitId);
|
|
583
|
+
if (stmt.expression) {
|
|
584
|
+
findCallExpressions(ctx, stmt.expression, exitId);
|
|
585
|
+
}
|
|
586
|
+
return null; // Return always exits
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Process a throw statement
|
|
590
|
+
*/
|
|
591
|
+
function processThrowStatement(ctx, stmt, entryPoint) {
|
|
592
|
+
const sourceFile = ctx.sourceFile;
|
|
593
|
+
const lineStart = getLineNumber(stmt, sourceFile);
|
|
594
|
+
const lineEnd = getEndLineNumber(stmt, sourceFile);
|
|
595
|
+
// Create throw node
|
|
596
|
+
const throwId = generateNodeId(ctx, 'throw');
|
|
597
|
+
const throwNode = createNode(throwId, 'throw', lineStart, lineEnd);
|
|
598
|
+
throwNode.statements.push(stmt);
|
|
599
|
+
ctx.nodes.set(throwId, throwNode);
|
|
600
|
+
addEdge(ctx, entryPoint, throwId, 'exception');
|
|
601
|
+
ctx.exitNodes.push(throwId);
|
|
602
|
+
findCallExpressions(ctx, stmt.expression, throwId);
|
|
603
|
+
return null; // Throw always exits
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Process a break statement
|
|
607
|
+
*/
|
|
608
|
+
function processBreakStatement(ctx, stmt, entryPoint) {
|
|
609
|
+
const target = ctx.breakTargets[ctx.breakTargets.length - 1];
|
|
610
|
+
if (target) {
|
|
611
|
+
addEdge(ctx, entryPoint, target, 'sequential');
|
|
612
|
+
}
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Process a continue statement
|
|
617
|
+
*/
|
|
618
|
+
function processContinueStatement(ctx, stmt, entryPoint) {
|
|
619
|
+
const target = ctx.continueTargets[ctx.continueTargets.length - 1];
|
|
620
|
+
if (target) {
|
|
621
|
+
addEdge(ctx, entryPoint, target, 'loop_back');
|
|
622
|
+
}
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Find and record call expressions in a node
|
|
627
|
+
*/
|
|
628
|
+
function findCallExpressions(ctx, node, cfgNodeId) {
|
|
629
|
+
const sourceFile = ctx.sourceFile;
|
|
630
|
+
function visit(n) {
|
|
631
|
+
if (ts.isCallExpression(n)) {
|
|
632
|
+
const callSite = createCallSite(n, cfgNodeId, sourceFile, ctx.filePath);
|
|
633
|
+
ctx.callSites.push(callSite);
|
|
634
|
+
}
|
|
635
|
+
ts.forEachChild(n, visit);
|
|
636
|
+
}
|
|
637
|
+
visit(node);
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Find await expressions in a node and record them.
|
|
641
|
+
* Returns array of await expression locations for creating await nodes.
|
|
642
|
+
*/
|
|
643
|
+
function findAwaitExpressions(node, _sourceFile) {
|
|
644
|
+
const awaits = [];
|
|
645
|
+
function visit(n) {
|
|
646
|
+
if (ts.isAwaitExpression(n)) {
|
|
647
|
+
awaits.push(n);
|
|
648
|
+
}
|
|
649
|
+
ts.forEachChild(n, visit);
|
|
650
|
+
}
|
|
651
|
+
visit(node);
|
|
652
|
+
return awaits;
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Check if a function is async
|
|
656
|
+
*/
|
|
657
|
+
function isAsyncFunction(node) {
|
|
658
|
+
if (!node.modifiers)
|
|
659
|
+
return false;
|
|
660
|
+
return node.modifiers.some((mod) => mod.kind === ts.SyntaxKind.AsyncKeyword);
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Create a call site record
|
|
664
|
+
*/
|
|
665
|
+
function createCallSite(call, nodeId, sourceFile, filePath) {
|
|
666
|
+
const location = {
|
|
667
|
+
file: filePath,
|
|
668
|
+
line: getLineNumber(call, sourceFile),
|
|
669
|
+
column: call.getStart(sourceFile) -
|
|
670
|
+
sourceFile.getLineAndCharacterOfPosition(call.getStart(sourceFile)).character,
|
|
671
|
+
endLine: getEndLineNumber(call, sourceFile),
|
|
672
|
+
};
|
|
673
|
+
let calleeName = '<unknown>';
|
|
674
|
+
let isDynamic = false;
|
|
675
|
+
if (ts.isIdentifier(call.expression)) {
|
|
676
|
+
calleeName = call.expression.text;
|
|
677
|
+
}
|
|
678
|
+
else if (ts.isPropertyAccessExpression(call.expression)) {
|
|
679
|
+
calleeName = call.expression.name.text;
|
|
680
|
+
}
|
|
681
|
+
else if (ts.isElementAccessExpression(call.expression)) {
|
|
682
|
+
isDynamic = true;
|
|
683
|
+
calleeName = '<computed>';
|
|
684
|
+
}
|
|
685
|
+
else {
|
|
686
|
+
isDynamic = true;
|
|
687
|
+
}
|
|
688
|
+
return {
|
|
689
|
+
nodeId,
|
|
690
|
+
calleeName,
|
|
691
|
+
calleeFile: undefined, // Resolved later during inter-procedural analysis
|
|
692
|
+
isResolved: false,
|
|
693
|
+
isDynamic,
|
|
694
|
+
location,
|
|
695
|
+
callExpression: call,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Get the name of a function
|
|
700
|
+
*/
|
|
701
|
+
function getFunctionName(node) {
|
|
702
|
+
if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) {
|
|
703
|
+
return node.name?.getText() ?? '<anonymous>';
|
|
704
|
+
}
|
|
705
|
+
if (ts.isFunctionExpression(node)) {
|
|
706
|
+
return node.name?.text ?? '<anonymous>';
|
|
707
|
+
}
|
|
708
|
+
if (ts.isArrowFunction(node)) {
|
|
709
|
+
// Try to get name from parent variable declaration
|
|
710
|
+
if (ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
|
|
711
|
+
return node.parent.name.text;
|
|
712
|
+
}
|
|
713
|
+
return '<arrow>';
|
|
714
|
+
}
|
|
715
|
+
if (ts.isConstructorDeclaration(node)) {
|
|
716
|
+
return 'constructor';
|
|
717
|
+
}
|
|
718
|
+
if (ts.isGetAccessorDeclaration(node)) {
|
|
719
|
+
return `get ${node.name.getText()}`;
|
|
720
|
+
}
|
|
721
|
+
if (ts.isSetAccessorDeclaration(node)) {
|
|
722
|
+
return `set ${node.name.getText()}`;
|
|
723
|
+
}
|
|
724
|
+
return '<anonymous>';
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Find all functions in a source file
|
|
728
|
+
*/
|
|
729
|
+
export function findFunctions(sourceFile) {
|
|
730
|
+
const functions = [];
|
|
731
|
+
function visit(node) {
|
|
732
|
+
if (ts.isFunctionDeclaration(node) ||
|
|
733
|
+
ts.isFunctionExpression(node) ||
|
|
734
|
+
ts.isArrowFunction(node) ||
|
|
735
|
+
ts.isMethodDeclaration(node) ||
|
|
736
|
+
ts.isConstructorDeclaration(node) ||
|
|
737
|
+
ts.isGetAccessorDeclaration(node) ||
|
|
738
|
+
ts.isSetAccessorDeclaration(node)) {
|
|
739
|
+
functions.push(node);
|
|
740
|
+
}
|
|
741
|
+
ts.forEachChild(node, visit);
|
|
742
|
+
}
|
|
743
|
+
visit(sourceFile);
|
|
744
|
+
return functions;
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Parse a source file
|
|
748
|
+
*/
|
|
749
|
+
export function parseSourceFile(content, filePath, scriptKind) {
|
|
750
|
+
// Determine script kind from file extension if not provided
|
|
751
|
+
if (scriptKind === undefined) {
|
|
752
|
+
if (filePath.endsWith('.tsx')) {
|
|
753
|
+
scriptKind = ts.ScriptKind.TSX;
|
|
754
|
+
}
|
|
755
|
+
else if (filePath.endsWith('.ts')) {
|
|
756
|
+
scriptKind = ts.ScriptKind.TS;
|
|
757
|
+
}
|
|
758
|
+
else if (filePath.endsWith('.jsx')) {
|
|
759
|
+
scriptKind = ts.ScriptKind.JSX;
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
scriptKind = ts.ScriptKind.JS;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
return ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, // setParentNodes
|
|
766
|
+
scriptKind);
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Build CFGs for all functions in a source file
|
|
770
|
+
*/
|
|
771
|
+
export function buildAllCFGs(content, filePath) {
|
|
772
|
+
const sourceFile = parseSourceFile(content, filePath);
|
|
773
|
+
const functions = findFunctions(sourceFile);
|
|
774
|
+
return functions.map((fn) => buildCFG(fn, sourceFile, filePath));
|
|
775
|
+
}
|
|
776
|
+
//# sourceMappingURL=cfg-builder.js.map
|