octocode-cli 1.2.5 → 1.2.7
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/LICENSE +21 -63
- package/README.md +86 -109
- package/out/octocode-cli.js +7027 -7014
- package/package.json +8 -6
- package/skills/README.md +97 -120
- package/skills/octocode-code-engineer/.claude/settings.local.json +18 -0
- package/skills/octocode-code-engineer/.octocode/rfc/RFC-code-engineer-weakness-fixes.md +255 -0
- package/skills/octocode-code-engineer/.plan/VALIDATED_PLAN.md +223 -0
- package/skills/octocode-code-engineer/README.md +178 -0
- package/skills/octocode-code-engineer/SKILL.md +418 -0
- package/skills/octocode-code-engineer/coverage/architecture.ts.html +7828 -0
- package/skills/octocode-code-engineer/coverage/ast-helpers.ts.html +211 -0
- package/skills/octocode-code-engineer/coverage/ast-search.ts.html +1795 -0
- package/skills/octocode-code-engineer/coverage/base.css +224 -0
- package/skills/octocode-code-engineer/coverage/block-navigation.js +87 -0
- package/skills/octocode-code-engineer/coverage/cache.ts.html +376 -0
- package/skills/octocode-code-engineer/coverage/cli.ts.html +982 -0
- package/skills/octocode-code-engineer/coverage/clover.xml +3217 -0
- package/skills/octocode-code-engineer/coverage/collect-effects.ts.html +664 -0
- package/skills/octocode-code-engineer/coverage/collect-input-sources.ts.html +577 -0
- package/skills/octocode-code-engineer/coverage/collect-performance.ts.html +331 -0
- package/skills/octocode-code-engineer/coverage/collect-prototype-pollution.ts.html +421 -0
- package/skills/octocode-code-engineer/coverage/collect-security.ts.html +604 -0
- package/skills/octocode-code-engineer/coverage/collect-test-profile.ts.html +589 -0
- package/skills/octocode-code-engineer/coverage/coverage-final.json +30 -0
- package/skills/octocode-code-engineer/coverage/dependencies.ts.html +997 -0
- package/skills/octocode-code-engineer/coverage/dependency-summary.ts.html +688 -0
- package/skills/octocode-code-engineer/coverage/discovery.ts.html +322 -0
- package/skills/octocode-code-engineer/coverage/favicon.png +0 -0
- package/skills/octocode-code-engineer/coverage/graph-analytics.ts.html +1510 -0
- package/skills/octocode-code-engineer/coverage/index.html +536 -0
- package/skills/octocode-code-engineer/coverage/index.ts.html +826 -0
- package/skills/octocode-code-engineer/coverage/metrics.ts.html +553 -0
- package/skills/octocode-code-engineer/coverage/pipeline.ts.html +2044 -0
- package/skills/octocode-code-engineer/coverage/prettify.css +1 -0
- package/skills/octocode-code-engineer/coverage/prettify.js +2 -0
- package/skills/octocode-code-engineer/coverage/report-analysis.ts.html +1570 -0
- package/skills/octocode-code-engineer/coverage/report-writer.ts.html +1102 -0
- package/skills/octocode-code-engineer/coverage/security-detectors.ts.html +1747 -0
- package/skills/octocode-code-engineer/coverage/semantic-detectors.ts.html +2152 -0
- package/skills/octocode-code-engineer/coverage/semantic.ts.html +1897 -0
- package/skills/octocode-code-engineer/coverage/sort-arrow-sprite.png +0 -0
- package/skills/octocode-code-engineer/coverage/sorter.js +210 -0
- package/skills/octocode-code-engineer/coverage/summary-md.ts.html +1222 -0
- package/skills/octocode-code-engineer/coverage/test-quality-detectors.ts.html +1039 -0
- package/skills/octocode-code-engineer/coverage/tree-sitter-analyzer.ts.html +955 -0
- package/skills/octocode-code-engineer/coverage/ts-analyzer.ts.html +1213 -0
- package/skills/octocode-code-engineer/coverage/types.ts.html +2473 -0
- package/skills/octocode-code-engineer/coverage/utils.ts.html +820 -0
- package/skills/octocode-code-engineer/eslint.config.mjs +54 -0
- package/skills/octocode-code-engineer/minify-scripts.mjs +32 -0
- package/skills/octocode-code-engineer/package.json +54 -0
- package/skills/octocode-code-engineer/references/agent-ast-reading-rfc.md +95 -0
- package/skills/octocode-code-engineer/references/architecture-techniques.md +121 -0
- package/skills/octocode-code-engineer/references/ast-search.md +210 -0
- package/skills/octocode-code-engineer/references/ast-tree-search.md +151 -0
- package/skills/octocode-code-engineer/references/cli-reference.md +167 -0
- package/skills/octocode-code-engineer/references/concepts.md +107 -0
- package/skills/octocode-code-engineer/references/finding-categories.md +128 -0
- package/skills/octocode-code-engineer/references/improvement-roadmap.md +304 -0
- package/skills/octocode-code-engineer/references/output-files.md +144 -0
- package/skills/octocode-code-engineer/references/playbooks.md +204 -0
- package/skills/octocode-code-engineer/references/present-results.md +136 -0
- package/skills/octocode-code-engineer/references/tool-workflows.md +566 -0
- package/skills/octocode-code-engineer/references/validate-investigate.md +225 -0
- package/skills/octocode-code-engineer/scripts/analysis/dependencies.js +1 -0
- package/skills/octocode-code-engineer/scripts/analysis/dependency-summary.js +1 -0
- package/skills/octocode-code-engineer/scripts/analysis/discovery.js +1 -0
- package/skills/octocode-code-engineer/scripts/analysis/graph-analytics.js +1 -0
- package/skills/octocode-code-engineer/scripts/analysis/semantic.js +1 -0
- package/skills/octocode-code-engineer/scripts/ast/helpers.js +1 -0
- package/skills/octocode-code-engineer/scripts/ast/metrics.js +1 -0
- package/skills/octocode-code-engineer/scripts/ast/search.js +2 -0
- package/skills/octocode-code-engineer/scripts/ast/tree-search.js +2 -0
- package/skills/octocode-code-engineer/scripts/ast/tree-sitter.js +1 -0
- package/skills/octocode-code-engineer/scripts/ast/ts-analyzer.js +1 -0
- package/skills/octocode-code-engineer/scripts/collectors/chains.js +1 -0
- package/skills/octocode-code-engineer/scripts/collectors/effects.js +1 -0
- package/skills/octocode-code-engineer/scripts/collectors/input-sources.js +1 -0
- package/skills/octocode-code-engineer/scripts/collectors/performance.js +1 -0
- package/skills/octocode-code-engineer/scripts/collectors/prototype-pollution.js +1 -0
- package/skills/octocode-code-engineer/scripts/collectors/security.js +1 -0
- package/skills/octocode-code-engineer/scripts/collectors/test-profile.js +1 -0
- package/skills/octocode-code-engineer/scripts/common/is-direct-run.js +1 -0
- package/skills/octocode-code-engineer/scripts/common/utils.js +1 -0
- package/skills/octocode-code-engineer/scripts/detectors/code-quality.js +1 -0
- package/skills/octocode-code-engineer/scripts/detectors/cohesion.js +1 -0
- package/skills/octocode-code-engineer/scripts/detectors/coupling.js +1 -0
- package/skills/octocode-code-engineer/scripts/detectors/cycle.js +1 -0
- package/skills/octocode-code-engineer/scripts/detectors/dead-code.js +1 -0
- package/skills/octocode-code-engineer/scripts/detectors/import-style.js +1 -0
- package/skills/octocode-code-engineer/scripts/detectors/index.js +1 -0
- package/skills/octocode-code-engineer/scripts/detectors/security.js +1 -0
- package/skills/octocode-code-engineer/scripts/detectors/semantic.js +1 -0
- package/skills/octocode-code-engineer/scripts/detectors/shared.js +1 -0
- package/skills/octocode-code-engineer/scripts/detectors/test-quality.js +1 -0
- package/skills/octocode-code-engineer/scripts/index.js +1 -0
- package/skills/octocode-code-engineer/scripts/pipeline/cache.js +1 -0
- package/skills/octocode-code-engineer/scripts/pipeline/cli.js +1 -0
- package/skills/octocode-code-engineer/scripts/pipeline/main.js +2 -0
- package/skills/octocode-code-engineer/scripts/reporting/analysis.js +1 -0
- package/skills/octocode-code-engineer/scripts/reporting/summary-md.js +1 -0
- package/skills/octocode-code-engineer/scripts/reporting/writer.js +1 -0
- package/skills/octocode-code-engineer/scripts/types/constants.js +1 -0
- package/skills/octocode-code-engineer/scripts/types/index.js +1 -0
- package/skills/octocode-code-engineer/scripts/types/interfaces.js +1 -0
- package/skills/octocode-code-engineer/src/analysis/dependencies.test.ts +545 -0
- package/skills/octocode-code-engineer/src/analysis/dependencies.ts +406 -0
- package/skills/octocode-code-engineer/src/analysis/dependency-summary.test.ts +566 -0
- package/skills/octocode-code-engineer/src/analysis/dependency-summary.ts +257 -0
- package/skills/octocode-code-engineer/src/analysis/discovery.test.ts +420 -0
- package/skills/octocode-code-engineer/src/analysis/discovery.ts +87 -0
- package/skills/octocode-code-engineer/src/analysis/graph-analytics.test.ts +449 -0
- package/skills/octocode-code-engineer/src/analysis/graph-analytics.ts +534 -0
- package/skills/octocode-code-engineer/src/analysis/semantic.test.ts +1533 -0
- package/skills/octocode-code-engineer/src/analysis/semantic.ts +830 -0
- package/skills/octocode-code-engineer/src/ast/helpers.test.ts +185 -0
- package/skills/octocode-code-engineer/src/ast/helpers.ts +62 -0
- package/skills/octocode-code-engineer/src/ast/metrics.test.ts +304 -0
- package/skills/octocode-code-engineer/src/ast/metrics.ts +204 -0
- package/skills/octocode-code-engineer/src/ast/search.test.ts +647 -0
- package/skills/octocode-code-engineer/src/ast/search.ts +648 -0
- package/skills/octocode-code-engineer/src/ast/tree-search.test.ts +199 -0
- package/skills/octocode-code-engineer/src/ast/tree-search.ts +392 -0
- package/skills/octocode-code-engineer/src/ast/tree-sitter.test.ts +407 -0
- package/skills/octocode-code-engineer/src/ast/tree-sitter.ts +402 -0
- package/skills/octocode-code-engineer/src/ast/ts-analyzer.test.ts +1864 -0
- package/skills/octocode-code-engineer/src/ast/ts-analyzer.ts +509 -0
- package/skills/octocode-code-engineer/src/collectors/chains.ts +74 -0
- package/skills/octocode-code-engineer/src/collectors/effects.test.ts +490 -0
- package/skills/octocode-code-engineer/src/collectors/effects.ts +332 -0
- package/skills/octocode-code-engineer/src/collectors/input-sources.test.ts +144 -0
- package/skills/octocode-code-engineer/src/collectors/input-sources.ts +196 -0
- package/skills/octocode-code-engineer/src/collectors/performance.test.ts +82 -0
- package/skills/octocode-code-engineer/src/collectors/performance.ts +141 -0
- package/skills/octocode-code-engineer/src/collectors/prototype-pollution.test.ts +55 -0
- package/skills/octocode-code-engineer/src/collectors/prototype-pollution.ts +162 -0
- package/skills/octocode-code-engineer/src/collectors/security.test.ts +124 -0
- package/skills/octocode-code-engineer/src/collectors/security.ts +309 -0
- package/skills/octocode-code-engineer/src/collectors/test-profile.test.ts +97 -0
- package/skills/octocode-code-engineer/src/collectors/test-profile.ts +269 -0
- package/skills/octocode-code-engineer/src/common/is-direct-run.test.ts +32 -0
- package/skills/octocode-code-engineer/src/common/is-direct-run.ts +13 -0
- package/skills/octocode-code-engineer/src/common/utils.test.ts +463 -0
- package/skills/octocode-code-engineer/src/common/utils.ts +304 -0
- package/skills/octocode-code-engineer/src/detectors/code-quality.ts +966 -0
- package/skills/octocode-code-engineer/src/detectors/cohesion.ts +539 -0
- package/skills/octocode-code-engineer/src/detectors/coupling.ts +323 -0
- package/skills/octocode-code-engineer/src/detectors/cycle.ts +349 -0
- package/skills/octocode-code-engineer/src/detectors/dead-code.ts +320 -0
- package/skills/octocode-code-engineer/src/detectors/import-style.ts +376 -0
- package/skills/octocode-code-engineer/src/detectors/index.test.ts +3061 -0
- package/skills/octocode-code-engineer/src/detectors/index.ts +88 -0
- package/skills/octocode-code-engineer/src/detectors/security.test.ts +882 -0
- package/skills/octocode-code-engineer/src/detectors/security.ts +821 -0
- package/skills/octocode-code-engineer/src/detectors/semantic.ts +758 -0
- package/skills/octocode-code-engineer/src/detectors/shared.ts +49 -0
- package/skills/octocode-code-engineer/src/detectors/test-quality.test.ts +388 -0
- package/skills/octocode-code-engineer/src/detectors/test-quality.ts +367 -0
- package/skills/octocode-code-engineer/src/index.test.ts +4425 -0
- package/skills/octocode-code-engineer/src/index.ts +403 -0
- package/skills/octocode-code-engineer/src/pipeline/cache.test.ts +199 -0
- package/skills/octocode-code-engineer/src/pipeline/cache.ts +130 -0
- package/skills/octocode-code-engineer/src/pipeline/cli.test.ts +493 -0
- package/skills/octocode-code-engineer/src/pipeline/cli.ts +344 -0
- package/skills/octocode-code-engineer/src/pipeline/main.test.ts +174 -0
- package/skills/octocode-code-engineer/src/pipeline/main.ts +1074 -0
- package/skills/octocode-code-engineer/src/pipeline.test.ts +84 -0
- package/skills/octocode-code-engineer/src/reporting/analysis.test.ts +782 -0
- package/skills/octocode-code-engineer/src/reporting/analysis.ts +688 -0
- package/skills/octocode-code-engineer/src/reporting/output-contract.test.ts +463 -0
- package/skills/octocode-code-engineer/src/reporting/summary-md.test.ts +421 -0
- package/skills/octocode-code-engineer/src/reporting/summary-md.ts +714 -0
- package/skills/octocode-code-engineer/src/reporting/writer.ts +430 -0
- package/skills/octocode-code-engineer/src/sanity.test.ts +47 -0
- package/skills/octocode-code-engineer/src/types/constants.ts +248 -0
- package/skills/octocode-code-engineer/src/types/index.ts +80 -0
- package/skills/octocode-code-engineer/src/types/interfaces.ts +682 -0
- package/skills/octocode-code-engineer/tsconfig.json +17 -0
- package/skills/octocode-code-engineer/vitest.config.ts +8 -0
- package/skills/octocode-documentation-writer/README.md +113 -0
- package/skills/octocode-documentation-writer/SKILL.md +886 -0
- package/skills/octocode-documentation-writer/references/agent-discovery-analysis.md +453 -0
- package/skills/octocode-documentation-writer/references/agent-documentation-writer.md +255 -0
- package/skills/octocode-documentation-writer/references/agent-engineer-questions.md +247 -0
- package/skills/octocode-documentation-writer/references/agent-orchestrator.md +370 -0
- package/skills/octocode-documentation-writer/references/agent-qa-validator.md +227 -0
- package/skills/octocode-documentation-writer/references/agent-researcher.md +250 -0
- package/skills/octocode-documentation-writer/schemas/analysis-schema.json +886 -0
- package/skills/octocode-documentation-writer/schemas/discovery-tasks.json +96 -0
- package/skills/octocode-documentation-writer/schemas/documentation-structure.json +373 -0
- package/skills/octocode-documentation-writer/schemas/partial-discovery-schema.json +102 -0
- package/skills/octocode-documentation-writer/schemas/partial-research-schema.json +98 -0
- package/skills/octocode-documentation-writer/schemas/qa-results-schema.json +113 -0
- package/skills/octocode-documentation-writer/schemas/questions-schema.json +228 -0
- package/skills/octocode-documentation-writer/schemas/research-schema.json +104 -0
- package/skills/octocode-documentation-writer/schemas/state-schema.json +222 -0
- package/skills/octocode-documentation-writer/schemas/work-assignments-schema.json +74 -0
- package/skills/octocode-plan/SKILL.md +122 -116
- package/skills/octocode-prompt-optimizer/SKILL.md +617 -0
- package/skills/octocode-pull-request-reviewer/README.md +249 -0
- package/skills/octocode-pull-request-reviewer/SKILL.md +479 -0
- package/skills/octocode-pull-request-reviewer/references/dependency-check.md +74 -0
- package/skills/octocode-pull-request-reviewer/references/domain-reviewers.md +24 -0
- package/skills/octocode-pull-request-reviewer/references/execution-lifecycle.md +441 -0
- package/skills/octocode-pull-request-reviewer/references/flow-analysis-protocol.md +64 -0
- package/skills/octocode-pull-request-reviewer/references/output-template.md +174 -0
- package/skills/octocode-pull-request-reviewer/references/parallel-agent-protocol.md +182 -0
- package/skills/octocode-pull-request-reviewer/references/review-guidelines.md +26 -0
- package/skills/octocode-pull-request-reviewer/references/verification-checklist.md +40 -0
- package/skills/octocode-research/.claude/settings.local.json +46 -0
- package/skills/octocode-research/.octocode/plan/code-review-fixes/plan.md +312 -0
- package/skills/octocode-research/.octocode/plan/code-review-fixes/research.md +212 -0
- package/skills/octocode-research/.octocode/plans/NODE_SERVER_START_PLAN.md +755 -0
- package/skills/octocode-research/.octocode/research/code-review/research.md +371 -0
- package/skills/octocode-research/.octocode/review/IMPROVEMENTS.md +391 -0
- package/skills/octocode-research/.octocode/review/REVIEW_PLAN.md +289 -0
- package/skills/octocode-research/.octocode/review/REVIEW_REPORT.md +356 -0
- package/skills/octocode-research/AGENTS.md +349 -0
- package/skills/octocode-research/README.md +494 -0
- package/skills/octocode-research/SKILL.md +652 -274
- package/skills/octocode-research/docs/API_REFERENCE.md +562 -0
- package/skills/octocode-research/docs/ARCHITECTURE.md +554 -0
- package/skills/octocode-research/docs/FLOWS.md +577 -0
- package/skills/octocode-research/docs/OVERVIEW.md +564 -0
- package/skills/octocode-research/docs/SERVER_FLOWS.md +631 -0
- package/skills/octocode-research/ecosystem.config.cjs +88 -0
- package/skills/octocode-research/eslint.config.mjs +27 -0
- package/skills/octocode-research/package.json +84 -0
- package/skills/octocode-research/references/GUARDRAILS.md +40 -0
- package/skills/octocode-research/references/PARALLEL_AGENT_PROTOCOL.md +178 -0
- package/skills/octocode-research/references/roast-prompt.md +149 -0
- package/skills/octocode-research/scripts/server-init.d.ts +2 -0
- package/skills/octocode-research/scripts/server-init.js +2 -0
- package/skills/octocode-research/scripts/server.d.ts +8 -0
- package/skills/octocode-research/scripts/server.js +445 -0
- package/skills/octocode-research/src/__tests__/integration/circuitBreaker.test.ts +205 -0
- package/skills/octocode-research/src/__tests__/integration/routes.test.ts +374 -0
- package/skills/octocode-research/src/__tests__/unit/circuitBreaker.test.ts +245 -0
- package/skills/octocode-research/src/__tests__/unit/errorHandler.test.ts +183 -0
- package/skills/octocode-research/src/__tests__/unit/httpPreprocess.test.ts +157 -0
- package/skills/octocode-research/src/__tests__/unit/logger.test.ts +143 -0
- package/skills/octocode-research/src/__tests__/unit/queryParser.test.ts +130 -0
- package/skills/octocode-research/src/__tests__/unit/responseBuilder.test.ts +469 -0
- package/skills/octocode-research/src/__tests__/unit/retry.test.ts +205 -0
- package/skills/octocode-research/src/index.ts +186 -0
- package/skills/octocode-research/src/mcpCache.ts +49 -0
- package/skills/octocode-research/src/middleware/errorHandler.ts +65 -0
- package/skills/octocode-research/src/middleware/logger.ts +61 -0
- package/skills/octocode-research/src/middleware/queryParser.ts +115 -0
- package/skills/octocode-research/src/middleware/readiness.ts +17 -0
- package/skills/octocode-research/src/routes/github.ts +197 -0
- package/skills/octocode-research/src/routes/local.ts +175 -0
- package/skills/octocode-research/src/routes/lsp.ts +177 -0
- package/skills/octocode-research/src/routes/package.ts +127 -0
- package/skills/octocode-research/src/routes/prompts.ts +138 -0
- package/skills/octocode-research/src/routes/tools.ts +677 -0
- package/skills/octocode-research/src/server-init.ts +363 -0
- package/skills/octocode-research/src/server.ts +285 -0
- package/skills/octocode-research/src/types/errorGuards.ts +151 -0
- package/skills/octocode-research/src/types/express.d.ts +76 -0
- package/skills/octocode-research/src/types/guards.ts +98 -0
- package/skills/octocode-research/src/types/mcp.ts +119 -0
- package/skills/octocode-research/src/types/responses.ts +199 -0
- package/skills/octocode-research/src/types/toolTypes.ts +33 -0
- package/skills/octocode-research/src/utils/asyncTimeout.ts +116 -0
- package/skills/octocode-research/src/utils/circuitBreaker.ts +492 -0
- package/skills/octocode-research/src/utils/colors.ts +53 -0
- package/skills/octocode-research/src/utils/errorQueue.ts +71 -0
- package/skills/octocode-research/src/utils/logEmoji.ts +103 -0
- package/skills/octocode-research/src/utils/logger.ts +413 -0
- package/skills/octocode-research/src/utils/resilience.ts +169 -0
- package/skills/octocode-research/src/utils/responseBuilder.ts +495 -0
- package/skills/octocode-research/src/utils/responseFactory.ts +100 -0
- package/skills/octocode-research/src/utils/responseParser.ts +272 -0
- package/skills/octocode-research/src/utils/retry.ts +280 -0
- package/skills/octocode-research/src/utils/routeFactory.ts +117 -0
- package/skills/octocode-research/src/utils/url.ts +20 -0
- package/skills/octocode-research/src/validation/httpPreprocess.ts +155 -0
- package/skills/octocode-research/src/validation/index.ts +2 -0
- package/skills/octocode-research/src/validation/schemas.ts +578 -0
- package/skills/octocode-research/src/validation/toolCallSchema.ts +132 -0
- package/skills/octocode-research/tsconfig.json +21 -0
- package/skills/octocode-research/tsdown.config.ts +42 -0
- package/skills/octocode-research/vitest.config.ts +20 -0
- package/skills/octocode-researcher/SKILL.md +461 -0
- package/skills/octocode-researcher/references/fallbacks.md +120 -0
- package/skills/{octocode-local-search → octocode-researcher}/references/tool-reference.md +132 -49
- package/skills/{octocode-local-search → octocode-researcher}/references/workflow-patterns.md +204 -4
- package/skills/octocode-rfc-generator/SKILL.md +223 -0
- package/skills/octocode-rfc-generator/references/rfc-template.md +193 -0
- package/skills/octocode-roast/SKILL.md +63 -21
- package/skills/octocode-implement/SKILL.md +0 -293
- package/skills/octocode-implement/references/execution-phases.md +0 -317
- package/skills/octocode-implement/references/tool-reference.md +0 -403
- package/skills/octocode-implement/references/workflow-patterns.md +0 -385
- package/skills/octocode-local-search/SKILL.md +0 -449
- package/skills/octocode-pr-review/SKILL.md +0 -391
- package/skills/octocode-pr-review/references/domain-reviewers.md +0 -105
- package/skills/octocode-pr-review/references/execution-lifecycle.md +0 -116
- package/skills/octocode-pr-review/references/research-flows.md +0 -75
- package/skills/octocode-research/references/tool-reference.md +0 -304
- package/skills/octocode-research/references/workflow-patterns.md +0 -325
|
@@ -0,0 +1,1864 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
analyzeSourceFile,
|
|
6
|
+
buildDependencyCriticality,
|
|
7
|
+
collectMetrics,
|
|
8
|
+
computeHalstead,
|
|
9
|
+
computeMaintainabilityIndex,
|
|
10
|
+
countLinesInNode,
|
|
11
|
+
getFunctionName,
|
|
12
|
+
isFunctionLike,
|
|
13
|
+
} from './ts-analyzer.js';
|
|
14
|
+
import { DEFAULT_OPTS } from '../types/index.js';
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
DependencyProfile,
|
|
18
|
+
FileEntry,
|
|
19
|
+
FlowMaps,
|
|
20
|
+
PackageFileSummary,
|
|
21
|
+
TreeEntry,
|
|
22
|
+
} from '../types/index.js';
|
|
23
|
+
|
|
24
|
+
function parse(code: string, fileName = '/repo/src/test.ts'): ts.SourceFile {
|
|
25
|
+
return ts.createSourceFile(fileName, code, ts.ScriptTarget.ESNext, true);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function firstStatement(src: ts.SourceFile): ts.Node {
|
|
29
|
+
return src.statements[0];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function emptyPackageSummary(): PackageFileSummary {
|
|
33
|
+
return {
|
|
34
|
+
fileCount: 0,
|
|
35
|
+
nodeCount: 0,
|
|
36
|
+
functionCount: 0,
|
|
37
|
+
flowCount: 0,
|
|
38
|
+
kindCounts: {},
|
|
39
|
+
functions: [],
|
|
40
|
+
flows: [],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function emptyMaps(): FlowMaps {
|
|
45
|
+
return { flowMap: new Map(), controlMap: new Map() };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const emptyProfile: DependencyProfile = {
|
|
49
|
+
internalDependencies: [],
|
|
50
|
+
externalDependencies: [],
|
|
51
|
+
unresolvedDependencies: [],
|
|
52
|
+
declaredExports: [],
|
|
53
|
+
importedSymbols: [],
|
|
54
|
+
reExports: [],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const testOpts = { ...DEFAULT_OPTS, root: '/repo', emitTree: false };
|
|
58
|
+
|
|
59
|
+
describe('isFunctionLike', () => {
|
|
60
|
+
it('matches function declarations', () => {
|
|
61
|
+
const src = parse('function foo() {}');
|
|
62
|
+
expect(isFunctionLike(firstStatement(src))).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('matches arrow functions in variable declarations', () => {
|
|
66
|
+
const src = parse('const f = () => {};');
|
|
67
|
+
const decl = (firstStatement(src) as ts.VariableStatement).declarationList
|
|
68
|
+
.declarations[0];
|
|
69
|
+
expect(isFunctionLike(decl.initializer!)).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('matches method declarations in class', () => {
|
|
73
|
+
const src = parse('class A { method() {} }');
|
|
74
|
+
const cls = firstStatement(src) as ts.ClassDeclaration;
|
|
75
|
+
const method = cls.members[0];
|
|
76
|
+
expect(isFunctionLike(method)).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('rejects non-function nodes', () => {
|
|
80
|
+
const src = parse('const x = 1;');
|
|
81
|
+
expect(isFunctionLike(firstStatement(src))).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('matches getters and setters', () => {
|
|
85
|
+
const src = parse(
|
|
86
|
+
'class A { get val() { return 1; } set val(v: number) {} }'
|
|
87
|
+
);
|
|
88
|
+
const cls = firstStatement(src) as ts.ClassDeclaration;
|
|
89
|
+
expect(isFunctionLike(cls.members[0])).toBe(true);
|
|
90
|
+
expect(isFunctionLike(cls.members[1])).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('getFunctionName', () => {
|
|
95
|
+
it('returns name of function declaration', () => {
|
|
96
|
+
const src = parse('function greet() {}');
|
|
97
|
+
expect(getFunctionName(firstStatement(src), src)).toBe('greet');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('returns variable name for arrow function', () => {
|
|
101
|
+
const src = parse('const handler = () => {};');
|
|
102
|
+
const decl = (firstStatement(src) as ts.VariableStatement).declarationList
|
|
103
|
+
.declarations[0];
|
|
104
|
+
expect(getFunctionName(decl.initializer!, src)).toBe('handler');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('returns <anonymous> for unnamed function expression', () => {
|
|
108
|
+
const src = parse('(function() {})');
|
|
109
|
+
const expr = (firstStatement(src) as ts.ExpressionStatement).expression;
|
|
110
|
+
const paren = (expr as ts.ParenthesizedExpression).expression;
|
|
111
|
+
expect(getFunctionName(paren, src)).toBe('<anonymous>');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('collectMetrics', () => {
|
|
116
|
+
it('returns base complexity of 1 for empty function', () => {
|
|
117
|
+
const src = parse('function f() {}');
|
|
118
|
+
const fn = firstStatement(src) as ts.FunctionDeclaration;
|
|
119
|
+
const metrics = collectMetrics(fn.body!);
|
|
120
|
+
expect(metrics.complexity).toBe(1);
|
|
121
|
+
expect(metrics.maxBranchDepth).toBe(0);
|
|
122
|
+
expect(metrics.returns).toBe(0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('increments complexity for if statement', () => {
|
|
126
|
+
const src = parse('function f(x: boolean) { if (x) { return; } }');
|
|
127
|
+
const fn = firstStatement(src) as ts.FunctionDeclaration;
|
|
128
|
+
const metrics = collectMetrics(fn.body!);
|
|
129
|
+
expect(metrics.complexity).toBeGreaterThan(1);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('tracks max branch depth', () => {
|
|
133
|
+
const src = parse(`function f(a: boolean, b: boolean) {
|
|
134
|
+
if (a) { if (b) { return; } }
|
|
135
|
+
}`);
|
|
136
|
+
const fn = firstStatement(src) as ts.FunctionDeclaration;
|
|
137
|
+
const metrics = collectMetrics(fn.body!);
|
|
138
|
+
expect(metrics.maxBranchDepth).toBe(2);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('tracks loop depth', () => {
|
|
142
|
+
const src = parse(`function f() {
|
|
143
|
+
for (let i = 0; i < 10; i++) {
|
|
144
|
+
for (let j = 0; j < 10; j++) {}
|
|
145
|
+
}
|
|
146
|
+
}`);
|
|
147
|
+
const fn = firstStatement(src) as ts.FunctionDeclaration;
|
|
148
|
+
const metrics = collectMetrics(fn.body!);
|
|
149
|
+
expect(metrics.maxLoopDepth).toBe(2);
|
|
150
|
+
expect(metrics.loops).toBe(2);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('counts await expressions', () => {
|
|
154
|
+
const src = parse(
|
|
155
|
+
'async function f() { await fetch("x"); await fetch("y"); }'
|
|
156
|
+
);
|
|
157
|
+
const fn = firstStatement(src) as ts.FunctionDeclaration;
|
|
158
|
+
const metrics = collectMetrics(fn.body!);
|
|
159
|
+
expect(metrics.awaits).toBe(2);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('counts call expressions', () => {
|
|
163
|
+
const src = parse('function f() { a(); b(); c(); }');
|
|
164
|
+
const fn = firstStatement(src) as ts.FunctionDeclaration;
|
|
165
|
+
const metrics = collectMetrics(fn.body!);
|
|
166
|
+
expect(metrics.calls).toBe(3);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('counts return and throw statements', () => {
|
|
170
|
+
const src = parse(
|
|
171
|
+
'function f(x: boolean) { if (x) return 1; throw new Error(); }'
|
|
172
|
+
);
|
|
173
|
+
const fn = firstStatement(src) as ts.FunctionDeclaration;
|
|
174
|
+
const metrics = collectMetrics(fn.body!);
|
|
175
|
+
expect(metrics.returns).toBe(2);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('counts logical operators as complexity', () => {
|
|
179
|
+
const src = parse(
|
|
180
|
+
'function f(a: boolean, b: boolean, c: boolean) { return a && b || c; }'
|
|
181
|
+
);
|
|
182
|
+
const fn = firstStatement(src) as ts.FunctionDeclaration;
|
|
183
|
+
const metrics = collectMetrics(fn.body!);
|
|
184
|
+
expect(metrics.complexity).toBeGreaterThanOrEqual(3);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('handles switch + catch', () => {
|
|
188
|
+
const src = parse(`function f(x: number) {
|
|
189
|
+
switch(x) { case 1: break; case 2: break; }
|
|
190
|
+
try {} catch(e) {}
|
|
191
|
+
}`);
|
|
192
|
+
const fn = firstStatement(src) as ts.FunctionDeclaration;
|
|
193
|
+
const metrics = collectMetrics(fn.body!);
|
|
194
|
+
expect(metrics.complexity).toBeGreaterThan(2);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('buildDependencyCriticality', () => {
|
|
199
|
+
it('returns score of 1 for null input', () => {
|
|
200
|
+
const result = buildDependencyCriticality(null, testOpts);
|
|
201
|
+
expect(result.score).toBe(1);
|
|
202
|
+
expect(result.functionCount).toBe(0);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('computes score based on complexity and function count', () => {
|
|
206
|
+
const entry: FileEntry = {
|
|
207
|
+
package: 'test',
|
|
208
|
+
file: 'src/a.ts',
|
|
209
|
+
parseEngine: 'typescript',
|
|
210
|
+
nodeCount: 100,
|
|
211
|
+
kindCounts: {},
|
|
212
|
+
functions: [
|
|
213
|
+
{
|
|
214
|
+
kind: 'FunctionDeclaration',
|
|
215
|
+
name: 'f1',
|
|
216
|
+
nameHint: 'f1',
|
|
217
|
+
file: 'src/a.ts',
|
|
218
|
+
lineStart: 1,
|
|
219
|
+
lineEnd: 10,
|
|
220
|
+
columnStart: 1,
|
|
221
|
+
columnEnd: 1,
|
|
222
|
+
statementCount: 5,
|
|
223
|
+
complexity: 10,
|
|
224
|
+
maxBranchDepth: 2,
|
|
225
|
+
maxLoopDepth: 1,
|
|
226
|
+
returns: 1,
|
|
227
|
+
awaits: 0,
|
|
228
|
+
calls: 3,
|
|
229
|
+
loops: 1,
|
|
230
|
+
lengthLines: 10,
|
|
231
|
+
cognitiveComplexity: 5,
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
flows: [
|
|
235
|
+
{
|
|
236
|
+
kind: 'IfStatement',
|
|
237
|
+
file: 'src/a.ts',
|
|
238
|
+
lineStart: 2,
|
|
239
|
+
lineEnd: 4,
|
|
240
|
+
columnStart: 1,
|
|
241
|
+
columnEnd: 1,
|
|
242
|
+
statementCount: 2,
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
dependencyProfile: emptyProfile,
|
|
246
|
+
};
|
|
247
|
+
const result = buildDependencyCriticality(entry, testOpts);
|
|
248
|
+
expect(result.score).toBeGreaterThan(1);
|
|
249
|
+
expect(result.functionCount).toBe(1);
|
|
250
|
+
expect(result.flows).toBe(1);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('counts high complexity functions', () => {
|
|
254
|
+
const entry: FileEntry = {
|
|
255
|
+
package: 'test',
|
|
256
|
+
file: 'src/a.ts',
|
|
257
|
+
parseEngine: 'typescript',
|
|
258
|
+
nodeCount: 100,
|
|
259
|
+
kindCounts: {},
|
|
260
|
+
functions: [
|
|
261
|
+
{
|
|
262
|
+
kind: 'FunctionDeclaration',
|
|
263
|
+
name: 'complex',
|
|
264
|
+
nameHint: 'complex',
|
|
265
|
+
file: 'src/a.ts',
|
|
266
|
+
lineStart: 1,
|
|
267
|
+
lineEnd: 50,
|
|
268
|
+
columnStart: 1,
|
|
269
|
+
columnEnd: 1,
|
|
270
|
+
statementCount: 30,
|
|
271
|
+
complexity: 35,
|
|
272
|
+
maxBranchDepth: 5,
|
|
273
|
+
maxLoopDepth: 3,
|
|
274
|
+
returns: 4,
|
|
275
|
+
awaits: 0,
|
|
276
|
+
calls: 10,
|
|
277
|
+
loops: 3,
|
|
278
|
+
lengthLines: 50,
|
|
279
|
+
cognitiveComplexity: 20,
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
flows: [],
|
|
283
|
+
dependencyProfile: emptyProfile,
|
|
284
|
+
};
|
|
285
|
+
const result = buildDependencyCriticality(entry, testOpts);
|
|
286
|
+
expect(result.highComplexityFunctions).toBe(1);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe('countLinesInNode', () => {
|
|
291
|
+
it('counts lines of single-line node', () => {
|
|
292
|
+
const src = parse('const x = 1;');
|
|
293
|
+
expect(countLinesInNode(src, firstStatement(src))).toBe(1);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('counts lines of multi-line function', () => {
|
|
297
|
+
const src = parse('function f() {\n const x = 1;\n return x;\n}');
|
|
298
|
+
expect(countLinesInNode(src, firstStatement(src))).toBe(4);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe('analyzeSourceFile', () => {
|
|
303
|
+
it('extracts functions from source file', () => {
|
|
304
|
+
const src = parse(
|
|
305
|
+
'function greet() { return "hi"; }\nconst add = (a: number, b: number) => a + b;'
|
|
306
|
+
);
|
|
307
|
+
const summary = emptyPackageSummary();
|
|
308
|
+
const maps = emptyMaps();
|
|
309
|
+
const trees: TreeEntry[] = [];
|
|
310
|
+
const result = analyzeSourceFile(
|
|
311
|
+
src,
|
|
312
|
+
'test-pkg',
|
|
313
|
+
summary,
|
|
314
|
+
testOpts,
|
|
315
|
+
maps,
|
|
316
|
+
trees,
|
|
317
|
+
emptyProfile
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
expect(result.functions.length).toBe(2);
|
|
321
|
+
expect(result.functions[0].name).toBe('greet');
|
|
322
|
+
expect(result.package).toBe('test-pkg');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('extracts control flows', () => {
|
|
326
|
+
const src = parse('function f(x: boolean) { if (x) { console.log(x); } }');
|
|
327
|
+
const summary = emptyPackageSummary();
|
|
328
|
+
const maps = emptyMaps();
|
|
329
|
+
const trees: TreeEntry[] = [];
|
|
330
|
+
const result = analyzeSourceFile(
|
|
331
|
+
src,
|
|
332
|
+
'pkg',
|
|
333
|
+
summary,
|
|
334
|
+
testOpts,
|
|
335
|
+
maps,
|
|
336
|
+
trees,
|
|
337
|
+
emptyProfile
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
expect(result.flows.length).toBeGreaterThan(0);
|
|
341
|
+
expect(result.flows[0].kind).toBe('IfStatement');
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('counts nodes', () => {
|
|
345
|
+
const src = parse('const x = 1;\nfunction f() { return 2; }');
|
|
346
|
+
const summary = emptyPackageSummary();
|
|
347
|
+
const result = analyzeSourceFile(
|
|
348
|
+
src,
|
|
349
|
+
'pkg',
|
|
350
|
+
summary,
|
|
351
|
+
testOpts,
|
|
352
|
+
emptyMaps(),
|
|
353
|
+
[],
|
|
354
|
+
emptyProfile
|
|
355
|
+
);
|
|
356
|
+
expect(result.nodeCount).toBeGreaterThan(0);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('populates kindCounts', () => {
|
|
360
|
+
const src = parse('const x = 1;\nconst y = 2;');
|
|
361
|
+
const summary = emptyPackageSummary();
|
|
362
|
+
const result = analyzeSourceFile(
|
|
363
|
+
src,
|
|
364
|
+
'pkg',
|
|
365
|
+
summary,
|
|
366
|
+
testOpts,
|
|
367
|
+
emptyMaps(),
|
|
368
|
+
[],
|
|
369
|
+
emptyProfile
|
|
370
|
+
);
|
|
371
|
+
expect(Object.keys(result.kindCounts).length).toBeGreaterThan(0);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('updates package summary stats', () => {
|
|
375
|
+
const src = parse('function f() { return 1; }');
|
|
376
|
+
const summary = emptyPackageSummary();
|
|
377
|
+
analyzeSourceFile(
|
|
378
|
+
src,
|
|
379
|
+
'pkg',
|
|
380
|
+
summary,
|
|
381
|
+
testOpts,
|
|
382
|
+
emptyMaps(),
|
|
383
|
+
[],
|
|
384
|
+
emptyProfile
|
|
385
|
+
);
|
|
386
|
+
expect(summary.fileCount).toBe(1);
|
|
387
|
+
expect(summary.functionCount).toBe(1);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('adds duplicate functions to flowMap', () => {
|
|
391
|
+
const code = `function bigFn() {
|
|
392
|
+
const a = 1; const b = 2; const c = 3;
|
|
393
|
+
const d = 4; const e = 5; const f = 6;
|
|
394
|
+
return a + b + c + d + e + f;
|
|
395
|
+
}`;
|
|
396
|
+
const src = parse(code);
|
|
397
|
+
const maps = emptyMaps();
|
|
398
|
+
const summary = emptyPackageSummary();
|
|
399
|
+
analyzeSourceFile(
|
|
400
|
+
src,
|
|
401
|
+
'pkg',
|
|
402
|
+
summary,
|
|
403
|
+
{ ...testOpts, minFunctionStatements: 6 },
|
|
404
|
+
maps,
|
|
405
|
+
[],
|
|
406
|
+
emptyProfile
|
|
407
|
+
);
|
|
408
|
+
expect(maps.flowMap.size).toBeGreaterThan(0);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('computes cognitive complexity for functions', () => {
|
|
412
|
+
const code = `function complex(a: boolean, b: boolean) {
|
|
413
|
+
if (a) { if (b) { return 1; } } return 0;
|
|
414
|
+
}`;
|
|
415
|
+
const src = parse(code);
|
|
416
|
+
const summary = emptyPackageSummary();
|
|
417
|
+
const result = analyzeSourceFile(
|
|
418
|
+
src,
|
|
419
|
+
'pkg',
|
|
420
|
+
summary,
|
|
421
|
+
testOpts,
|
|
422
|
+
emptyMaps(),
|
|
423
|
+
[],
|
|
424
|
+
emptyProfile
|
|
425
|
+
);
|
|
426
|
+
const fn = result.functions.find(f => f.name === 'complex');
|
|
427
|
+
expect(fn?.cognitiveComplexity).toBeGreaterThan(0);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('records param count', () => {
|
|
431
|
+
const src = parse('function f(a: number, b: string, c: boolean) {}');
|
|
432
|
+
const summary = emptyPackageSummary();
|
|
433
|
+
const result = analyzeSourceFile(
|
|
434
|
+
src,
|
|
435
|
+
'pkg',
|
|
436
|
+
summary,
|
|
437
|
+
testOpts,
|
|
438
|
+
emptyMaps(),
|
|
439
|
+
[],
|
|
440
|
+
emptyProfile
|
|
441
|
+
);
|
|
442
|
+
expect(result.functions[0].params).toBe(3);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('sets declared flag for function declarations', () => {
|
|
446
|
+
const src = parse('function named() {}');
|
|
447
|
+
const summary = emptyPackageSummary();
|
|
448
|
+
const result = analyzeSourceFile(
|
|
449
|
+
src,
|
|
450
|
+
'pkg',
|
|
451
|
+
summary,
|
|
452
|
+
testOpts,
|
|
453
|
+
emptyMaps(),
|
|
454
|
+
[],
|
|
455
|
+
emptyProfile
|
|
456
|
+
);
|
|
457
|
+
expect(result.functions[0].declared).toBe(true);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('collects empty catch blocks', () => {
|
|
461
|
+
const src = parse('function f() { try { throw 1; } catch(e) {} }');
|
|
462
|
+
const summary = emptyPackageSummary();
|
|
463
|
+
const result = analyzeSourceFile(
|
|
464
|
+
src,
|
|
465
|
+
'pkg',
|
|
466
|
+
summary,
|
|
467
|
+
testOpts,
|
|
468
|
+
emptyMaps(),
|
|
469
|
+
[],
|
|
470
|
+
emptyProfile
|
|
471
|
+
);
|
|
472
|
+
expect(result.emptyCatches?.length).toBe(1);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it('does not flag non-empty catch blocks', () => {
|
|
476
|
+
const src = parse('function f() { try {} catch(e) { console.log(e); } }');
|
|
477
|
+
const summary = emptyPackageSummary();
|
|
478
|
+
const result = analyzeSourceFile(
|
|
479
|
+
src,
|
|
480
|
+
'pkg',
|
|
481
|
+
summary,
|
|
482
|
+
testOpts,
|
|
483
|
+
emptyMaps(),
|
|
484
|
+
[],
|
|
485
|
+
emptyProfile
|
|
486
|
+
);
|
|
487
|
+
expect(result.emptyCatches?.length).toBe(0);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('collects switches without default', () => {
|
|
491
|
+
const src = parse(
|
|
492
|
+
'function f(x: number) { switch(x) { case 1: break; case 2: break; } }'
|
|
493
|
+
);
|
|
494
|
+
const summary = emptyPackageSummary();
|
|
495
|
+
const result = analyzeSourceFile(
|
|
496
|
+
src,
|
|
497
|
+
'pkg',
|
|
498
|
+
summary,
|
|
499
|
+
testOpts,
|
|
500
|
+
emptyMaps(),
|
|
501
|
+
[],
|
|
502
|
+
emptyProfile
|
|
503
|
+
);
|
|
504
|
+
expect(result.switchesWithoutDefault?.length).toBe(1);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('does not flag switches with default', () => {
|
|
508
|
+
const src = parse(
|
|
509
|
+
'function f(x: number) { switch(x) { case 1: break; default: break; } }'
|
|
510
|
+
);
|
|
511
|
+
const summary = emptyPackageSummary();
|
|
512
|
+
const result = analyzeSourceFile(
|
|
513
|
+
src,
|
|
514
|
+
'pkg',
|
|
515
|
+
summary,
|
|
516
|
+
testOpts,
|
|
517
|
+
emptyMaps(),
|
|
518
|
+
[],
|
|
519
|
+
emptyProfile
|
|
520
|
+
);
|
|
521
|
+
expect(result.switchesWithoutDefault?.length).toBe(0);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('counts any type annotations', () => {
|
|
525
|
+
const src = parse(
|
|
526
|
+
'const a: any = 1; const b: any = 2; function f(x: any) {}'
|
|
527
|
+
);
|
|
528
|
+
const summary = emptyPackageSummary();
|
|
529
|
+
const result = analyzeSourceFile(
|
|
530
|
+
src,
|
|
531
|
+
'pkg',
|
|
532
|
+
summary,
|
|
533
|
+
testOpts,
|
|
534
|
+
emptyMaps(),
|
|
535
|
+
[],
|
|
536
|
+
emptyProfile
|
|
537
|
+
);
|
|
538
|
+
expect(result.anyCount).toBeGreaterThanOrEqual(3);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it('collects magic numbers', () => {
|
|
542
|
+
const src = parse(
|
|
543
|
+
'function f() { let x = 42; let y = 99; return x + y + 300; }'
|
|
544
|
+
);
|
|
545
|
+
const summary = emptyPackageSummary();
|
|
546
|
+
const result = analyzeSourceFile(
|
|
547
|
+
src,
|
|
548
|
+
'pkg',
|
|
549
|
+
summary,
|
|
550
|
+
testOpts,
|
|
551
|
+
emptyMaps(),
|
|
552
|
+
[],
|
|
553
|
+
emptyProfile
|
|
554
|
+
);
|
|
555
|
+
expect(result.magicNumbers!.length).toBeGreaterThanOrEqual(3);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it('excludes 0 and 1 from magic numbers', () => {
|
|
559
|
+
const src = parse('function f() { return 0 + 1; }');
|
|
560
|
+
const summary = emptyPackageSummary();
|
|
561
|
+
const result = analyzeSourceFile(
|
|
562
|
+
src,
|
|
563
|
+
'pkg',
|
|
564
|
+
summary,
|
|
565
|
+
testOpts,
|
|
566
|
+
emptyMaps(),
|
|
567
|
+
[],
|
|
568
|
+
emptyProfile
|
|
569
|
+
);
|
|
570
|
+
expect(result.magicNumbers?.length).toBe(0);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it('excludes const declarations from magic numbers', () => {
|
|
574
|
+
const src = parse('const TIMEOUT = 5000;');
|
|
575
|
+
const summary = emptyPackageSummary();
|
|
576
|
+
const result = analyzeSourceFile(
|
|
577
|
+
src,
|
|
578
|
+
'pkg',
|
|
579
|
+
summary,
|
|
580
|
+
testOpts,
|
|
581
|
+
emptyMaps(),
|
|
582
|
+
[],
|
|
583
|
+
emptyProfile
|
|
584
|
+
);
|
|
585
|
+
expect(result.magicNumbers?.length).toBe(0);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it('computes halstead metrics for functions', () => {
|
|
589
|
+
const src = parse('function f(a: number, b: number) { return a + b * 2; }');
|
|
590
|
+
const summary = emptyPackageSummary();
|
|
591
|
+
const result = analyzeSourceFile(
|
|
592
|
+
src,
|
|
593
|
+
'pkg',
|
|
594
|
+
summary,
|
|
595
|
+
testOpts,
|
|
596
|
+
emptyMaps(),
|
|
597
|
+
[],
|
|
598
|
+
emptyProfile
|
|
599
|
+
);
|
|
600
|
+
expect(result.functions[0].halstead).toBeDefined();
|
|
601
|
+
expect(result.functions[0].halstead!.volume).toBeGreaterThan(0);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('computes maintainability index for functions', () => {
|
|
605
|
+
const src = parse('function f(a: number) { return a + 1; }');
|
|
606
|
+
const summary = emptyPackageSummary();
|
|
607
|
+
const result = analyzeSourceFile(
|
|
608
|
+
src,
|
|
609
|
+
'pkg',
|
|
610
|
+
summary,
|
|
611
|
+
testOpts,
|
|
612
|
+
emptyMaps(),
|
|
613
|
+
[],
|
|
614
|
+
emptyProfile
|
|
615
|
+
);
|
|
616
|
+
expect(result.functions[0].maintainabilityIndex).toBeDefined();
|
|
617
|
+
expect(result.functions[0].maintainabilityIndex!).toBeGreaterThan(0);
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
describe('computeHalstead', () => {
|
|
622
|
+
it('returns zeroes for empty body', () => {
|
|
623
|
+
const src = parse('function f() {}');
|
|
624
|
+
const fn = firstStatement(src) as ts.FunctionDeclaration;
|
|
625
|
+
const h = computeHalstead(fn.body!);
|
|
626
|
+
expect(h.length).toBe(0);
|
|
627
|
+
expect(h.volume).toBe(0);
|
|
628
|
+
expect(h.effort).toBe(0);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it('counts operators and operands for simple expression', () => {
|
|
632
|
+
const src = parse('function f(a: number, b: number) { return a + b; }');
|
|
633
|
+
const fn = firstStatement(src) as ts.FunctionDeclaration;
|
|
634
|
+
const h = computeHalstead(fn.body!);
|
|
635
|
+
expect(h.distinctOperators).toBeGreaterThan(0);
|
|
636
|
+
expect(h.distinctOperands).toBeGreaterThan(0);
|
|
637
|
+
expect(h.volume).toBeGreaterThan(0);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it('computes estimated bugs based on volume', () => {
|
|
641
|
+
const src = parse(`function f(x: number) {
|
|
642
|
+
const a = x + 1; const b = x - 2; const c = a * b;
|
|
643
|
+
return c / x + a - b;
|
|
644
|
+
}`);
|
|
645
|
+
const fn = firstStatement(src) as ts.FunctionDeclaration;
|
|
646
|
+
const h = computeHalstead(fn.body!);
|
|
647
|
+
expect(h.estimatedBugs).toBeGreaterThan(0);
|
|
648
|
+
expect(h.estimatedBugs).toBe(h.volume / 3000);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it('difficulty increases with repeated operands', () => {
|
|
652
|
+
const simpleCode = 'function f() { const a = 1; return a; }';
|
|
653
|
+
const repetitiveCode =
|
|
654
|
+
'function f() { const a = 1; const b = a; const c = a; const d = a; return a + b + c + d; }';
|
|
655
|
+
const simple = computeHalstead(
|
|
656
|
+
(firstStatement(parse(simpleCode)) as ts.FunctionDeclaration).body!
|
|
657
|
+
);
|
|
658
|
+
const repetitive = computeHalstead(
|
|
659
|
+
(firstStatement(parse(repetitiveCode)) as ts.FunctionDeclaration).body!
|
|
660
|
+
);
|
|
661
|
+
expect(repetitive.difficulty).toBeGreaterThan(simple.difficulty);
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
describe('computeMaintainabilityIndex', () => {
|
|
666
|
+
it('returns high MI for simple code', () => {
|
|
667
|
+
const mi = computeMaintainabilityIndex(10, 1, 5);
|
|
668
|
+
expect(mi).toBeGreaterThan(50);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
it('returns low MI for complex code', () => {
|
|
672
|
+
const mi = computeMaintainabilityIndex(50000, 50, 500);
|
|
673
|
+
expect(mi).toBeLessThan(20);
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
it('clamps to 0 minimum', () => {
|
|
677
|
+
const mi = computeMaintainabilityIndex(1e12, 1000, 100000);
|
|
678
|
+
expect(mi).toBe(0);
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
it('returns max ~100 for trivial code', () => {
|
|
682
|
+
const mi = computeMaintainabilityIndex(1, 1, 1);
|
|
683
|
+
expect(mi).toBeGreaterThan(90);
|
|
684
|
+
expect(mi).toBeLessThanOrEqual(100);
|
|
685
|
+
});
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
describe('collectSecurityData (via analyzeSourceFile)', () => {
|
|
689
|
+
it('detects eval() usage', () => {
|
|
690
|
+
const src = parse('function f(s: string) { eval(s); }');
|
|
691
|
+
const result = analyzeSourceFile(
|
|
692
|
+
src,
|
|
693
|
+
'pkg',
|
|
694
|
+
emptyPackageSummary(),
|
|
695
|
+
testOpts,
|
|
696
|
+
emptyMaps(),
|
|
697
|
+
[],
|
|
698
|
+
emptyProfile
|
|
699
|
+
);
|
|
700
|
+
expect(result.evalUsages!.length).toBeGreaterThanOrEqual(1);
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it('detects new Function() usage', () => {
|
|
704
|
+
const src = parse('const fn = new Function("return 1");');
|
|
705
|
+
const result = analyzeSourceFile(
|
|
706
|
+
src,
|
|
707
|
+
'pkg',
|
|
708
|
+
emptyPackageSummary(),
|
|
709
|
+
testOpts,
|
|
710
|
+
emptyMaps(),
|
|
711
|
+
[],
|
|
712
|
+
emptyProfile
|
|
713
|
+
);
|
|
714
|
+
expect(result.evalUsages!.length).toBeGreaterThanOrEqual(1);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
it('detects setTimeout with string arg', () => {
|
|
718
|
+
const src = parse('setTimeout("alert(1)", 100);');
|
|
719
|
+
const result = analyzeSourceFile(
|
|
720
|
+
src,
|
|
721
|
+
'pkg',
|
|
722
|
+
emptyPackageSummary(),
|
|
723
|
+
testOpts,
|
|
724
|
+
emptyMaps(),
|
|
725
|
+
[],
|
|
726
|
+
emptyProfile
|
|
727
|
+
);
|
|
728
|
+
expect(result.evalUsages!.length).toBeGreaterThanOrEqual(1);
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
it('detects innerHTML assignment', () => {
|
|
732
|
+
const src = parse(
|
|
733
|
+
'function f(el: HTMLElement) { el.innerHTML = "<b>hi</b>"; }'
|
|
734
|
+
);
|
|
735
|
+
const result = analyzeSourceFile(
|
|
736
|
+
src,
|
|
737
|
+
'pkg',
|
|
738
|
+
emptyPackageSummary(),
|
|
739
|
+
testOpts,
|
|
740
|
+
emptyMaps(),
|
|
741
|
+
[],
|
|
742
|
+
emptyProfile
|
|
743
|
+
);
|
|
744
|
+
expect(result.unsafeHtmlAssignments!.length).toBeGreaterThanOrEqual(1);
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it('detects outerHTML assignment', () => {
|
|
748
|
+
const src = parse(
|
|
749
|
+
'function f(el: HTMLElement) { el.outerHTML = "<div/>"; }'
|
|
750
|
+
);
|
|
751
|
+
const result = analyzeSourceFile(
|
|
752
|
+
src,
|
|
753
|
+
'pkg',
|
|
754
|
+
emptyPackageSummary(),
|
|
755
|
+
testOpts,
|
|
756
|
+
emptyMaps(),
|
|
757
|
+
[],
|
|
758
|
+
emptyProfile
|
|
759
|
+
);
|
|
760
|
+
expect(result.unsafeHtmlAssignments!.length).toBeGreaterThanOrEqual(1);
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
it('detects dangerouslySetInnerHTML JSX attribute', () => {
|
|
764
|
+
const src = parse(
|
|
765
|
+
'<div dangerouslySetInnerHTML={{ __html: s }} />;',
|
|
766
|
+
'/repo/src/test.tsx'
|
|
767
|
+
);
|
|
768
|
+
const result = analyzeSourceFile(
|
|
769
|
+
src,
|
|
770
|
+
'pkg',
|
|
771
|
+
emptyPackageSummary(),
|
|
772
|
+
testOpts,
|
|
773
|
+
emptyMaps(),
|
|
774
|
+
[],
|
|
775
|
+
emptyProfile
|
|
776
|
+
);
|
|
777
|
+
expect(result.unsafeHtmlAssignments!.length).toBeGreaterThanOrEqual(1);
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
it('detects document.write call', () => {
|
|
781
|
+
const src = parse('document.write("<h1>Hello</h1>");');
|
|
782
|
+
const result = analyzeSourceFile(
|
|
783
|
+
src,
|
|
784
|
+
'pkg',
|
|
785
|
+
emptyPackageSummary(),
|
|
786
|
+
testOpts,
|
|
787
|
+
emptyMaps(),
|
|
788
|
+
[],
|
|
789
|
+
emptyProfile
|
|
790
|
+
);
|
|
791
|
+
expect(result.unsafeHtmlAssignments!.length).toBeGreaterThanOrEqual(1);
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
it('detects hardcoded-secret suspicious strings from pattern matches', () => {
|
|
795
|
+
const src = parse("const cfg = `password = 'mysecret123'`;");
|
|
796
|
+
const result = analyzeSourceFile(
|
|
797
|
+
src,
|
|
798
|
+
'pkg',
|
|
799
|
+
emptyPackageSummary(),
|
|
800
|
+
testOpts,
|
|
801
|
+
emptyMaps(),
|
|
802
|
+
[],
|
|
803
|
+
emptyProfile
|
|
804
|
+
);
|
|
805
|
+
expect(
|
|
806
|
+
result.suspiciousStrings!.some(s => s.kind === 'hardcoded-secret')
|
|
807
|
+
).toBe(true);
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
it('skips placeholder patterns for secrets', () => {
|
|
811
|
+
const src = parse('const key = "YOUR_API_KEY_HERE";');
|
|
812
|
+
const result = analyzeSourceFile(
|
|
813
|
+
src,
|
|
814
|
+
'pkg',
|
|
815
|
+
emptyPackageSummary(),
|
|
816
|
+
testOpts,
|
|
817
|
+
emptyMaps(),
|
|
818
|
+
[],
|
|
819
|
+
emptyProfile
|
|
820
|
+
);
|
|
821
|
+
const secrets = result.suspiciousStrings!.filter(
|
|
822
|
+
s => s.kind === 'hardcoded-secret'
|
|
823
|
+
);
|
|
824
|
+
expect(secrets.length).toBe(0);
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
it('detects high-entropy strings as potential secrets', () => {
|
|
828
|
+
const src = parse('const token = "aB3dE7gH9jK1mN5pQ8sT0uW2xY4zA6c";');
|
|
829
|
+
const result = analyzeSourceFile(
|
|
830
|
+
src,
|
|
831
|
+
'pkg',
|
|
832
|
+
emptyPackageSummary(),
|
|
833
|
+
testOpts,
|
|
834
|
+
emptyMaps(),
|
|
835
|
+
[],
|
|
836
|
+
emptyProfile
|
|
837
|
+
);
|
|
838
|
+
expect(
|
|
839
|
+
result.suspiciousStrings!.some(s => s.kind === 'hardcoded-secret')
|
|
840
|
+
).toBe(true);
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
it('tags error messages separately from secrets', () => {
|
|
844
|
+
const src = parse(
|
|
845
|
+
'const msg = "invalid token provided for authentication service endpoint";'
|
|
846
|
+
);
|
|
847
|
+
const result = analyzeSourceFile(
|
|
848
|
+
src,
|
|
849
|
+
'pkg',
|
|
850
|
+
emptyPackageSummary(),
|
|
851
|
+
testOpts,
|
|
852
|
+
emptyMaps(),
|
|
853
|
+
[],
|
|
854
|
+
emptyProfile
|
|
855
|
+
);
|
|
856
|
+
const secrets = result.suspiciousStrings!.filter(
|
|
857
|
+
s => s.kind === 'hardcoded-secret' && s.context === 'error-message'
|
|
858
|
+
);
|
|
859
|
+
expect(secrets.length).toBeGreaterThanOrEqual(0);
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
it('detects SQL injection risk in template literals', () => {
|
|
863
|
+
const src = parse('const q = `SELECT * FROM users WHERE id = ${userId}`;');
|
|
864
|
+
const result = analyzeSourceFile(
|
|
865
|
+
src,
|
|
866
|
+
'pkg',
|
|
867
|
+
emptyPackageSummary(),
|
|
868
|
+
testOpts,
|
|
869
|
+
emptyMaps(),
|
|
870
|
+
[],
|
|
871
|
+
emptyProfile
|
|
872
|
+
);
|
|
873
|
+
expect(
|
|
874
|
+
result.suspiciousStrings!.some(s => s.kind === 'sql-injection')
|
|
875
|
+
).toBe(true);
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
it('does not flag template without SQL keywords', () => {
|
|
879
|
+
const src = parse('const msg = `Hello ${name}`;');
|
|
880
|
+
const result = analyzeSourceFile(
|
|
881
|
+
src,
|
|
882
|
+
'pkg',
|
|
883
|
+
emptyPackageSummary(),
|
|
884
|
+
testOpts,
|
|
885
|
+
emptyMaps(),
|
|
886
|
+
[],
|
|
887
|
+
emptyProfile
|
|
888
|
+
);
|
|
889
|
+
expect(
|
|
890
|
+
result.suspiciousStrings!.filter(s => s.kind === 'sql-injection').length
|
|
891
|
+
).toBe(0);
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
it('collects regex literals', () => {
|
|
895
|
+
const src = parse('const re = /^[a-z]+$/;');
|
|
896
|
+
const result = analyzeSourceFile(
|
|
897
|
+
src,
|
|
898
|
+
'pkg',
|
|
899
|
+
emptyPackageSummary(),
|
|
900
|
+
testOpts,
|
|
901
|
+
emptyMaps(),
|
|
902
|
+
[],
|
|
903
|
+
emptyProfile
|
|
904
|
+
);
|
|
905
|
+
expect(result.regexLiterals!.length).toBeGreaterThanOrEqual(1);
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
it('tags regex patterns that contain secret keywords as regex-definition', () => {
|
|
909
|
+
const code = "const secretRe = /password = 'foo'/i;";
|
|
910
|
+
const src = parse(code);
|
|
911
|
+
const result = analyzeSourceFile(
|
|
912
|
+
src,
|
|
913
|
+
'pkg',
|
|
914
|
+
emptyPackageSummary(),
|
|
915
|
+
testOpts,
|
|
916
|
+
emptyMaps(),
|
|
917
|
+
[],
|
|
918
|
+
emptyProfile
|
|
919
|
+
);
|
|
920
|
+
const regexDefs = result.suspiciousStrings!.filter(
|
|
921
|
+
s => s.context === 'regex-definition'
|
|
922
|
+
);
|
|
923
|
+
expect(regexDefs.length).toBeGreaterThanOrEqual(1);
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
it('detects type assertion escapes (as any)', () => {
|
|
927
|
+
const src = parse('const x = (value as any).prop;');
|
|
928
|
+
const result = analyzeSourceFile(
|
|
929
|
+
src,
|
|
930
|
+
'pkg',
|
|
931
|
+
emptyPackageSummary(),
|
|
932
|
+
testOpts,
|
|
933
|
+
emptyMaps(),
|
|
934
|
+
[],
|
|
935
|
+
emptyProfile
|
|
936
|
+
);
|
|
937
|
+
expect(result.typeAssertionEscapes!.asAny.length).toBeGreaterThanOrEqual(1);
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
it('detects double assertion (as unknown as T)', () => {
|
|
941
|
+
const src = parse('const x = (value as unknown as string);');
|
|
942
|
+
const result = analyzeSourceFile(
|
|
943
|
+
src,
|
|
944
|
+
'pkg',
|
|
945
|
+
emptyPackageSummary(),
|
|
946
|
+
testOpts,
|
|
947
|
+
emptyMaps(),
|
|
948
|
+
[],
|
|
949
|
+
emptyProfile
|
|
950
|
+
);
|
|
951
|
+
expect(
|
|
952
|
+
result.typeAssertionEscapes!.doubleAssertion.length
|
|
953
|
+
).toBeGreaterThanOrEqual(1);
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
it('detects non-null assertions', () => {
|
|
957
|
+
const src = parse('function f(x?: string) { return x!.length; }');
|
|
958
|
+
const result = analyzeSourceFile(
|
|
959
|
+
src,
|
|
960
|
+
'pkg',
|
|
961
|
+
emptyPackageSummary(),
|
|
962
|
+
testOpts,
|
|
963
|
+
emptyMaps(),
|
|
964
|
+
[],
|
|
965
|
+
emptyProfile
|
|
966
|
+
);
|
|
967
|
+
expect(result.typeAssertionEscapes!.nonNull.length).toBeGreaterThanOrEqual(
|
|
968
|
+
1
|
|
969
|
+
);
|
|
970
|
+
});
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
describe('async pattern detection (via analyzeSourceFile)', () => {
|
|
974
|
+
it('detects unprotected async (await without try-catch)', () => {
|
|
975
|
+
const src = parse('async function f() { await fetch("url"); }');
|
|
976
|
+
const result = analyzeSourceFile(
|
|
977
|
+
src,
|
|
978
|
+
'pkg',
|
|
979
|
+
emptyPackageSummary(),
|
|
980
|
+
testOpts,
|
|
981
|
+
emptyMaps(),
|
|
982
|
+
[],
|
|
983
|
+
emptyProfile
|
|
984
|
+
);
|
|
985
|
+
expect(result.unprotectedAsync?.length).toBeGreaterThanOrEqual(1);
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
it('does not flag async with try-catch as unprotected', () => {
|
|
989
|
+
const src = parse(
|
|
990
|
+
'async function f() { try { await fetch("url"); } catch(e) { console.error(e); } }'
|
|
991
|
+
);
|
|
992
|
+
const result = analyzeSourceFile(
|
|
993
|
+
src,
|
|
994
|
+
'pkg',
|
|
995
|
+
emptyPackageSummary(),
|
|
996
|
+
testOpts,
|
|
997
|
+
emptyMaps(),
|
|
998
|
+
[],
|
|
999
|
+
emptyProfile
|
|
1000
|
+
);
|
|
1001
|
+
expect(result.unprotectedAsync?.length).toBe(0);
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
it('does not flag async with .catch chain as unprotected', () => {
|
|
1005
|
+
const src = parse(
|
|
1006
|
+
'async function f() { await fetch("url").catch(console.error); }'
|
|
1007
|
+
);
|
|
1008
|
+
const result = analyzeSourceFile(
|
|
1009
|
+
src,
|
|
1010
|
+
'pkg',
|
|
1011
|
+
emptyPackageSummary(),
|
|
1012
|
+
testOpts,
|
|
1013
|
+
emptyMaps(),
|
|
1014
|
+
[],
|
|
1015
|
+
emptyProfile
|
|
1016
|
+
);
|
|
1017
|
+
expect(result.unprotectedAsync?.length).toBe(0);
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
it('skips functions with zero awaits in metrics', () => {
|
|
1021
|
+
const src = parse('async function f() { return 1; }');
|
|
1022
|
+
const result = analyzeSourceFile(
|
|
1023
|
+
src,
|
|
1024
|
+
'pkg',
|
|
1025
|
+
emptyPackageSummary(),
|
|
1026
|
+
testOpts,
|
|
1027
|
+
emptyMaps(),
|
|
1028
|
+
[],
|
|
1029
|
+
emptyProfile
|
|
1030
|
+
);
|
|
1031
|
+
expect(result.asyncWithoutAwait?.length ?? 0).toBe(0);
|
|
1032
|
+
expect(result.unprotectedAsync?.length ?? 0).toBe(0);
|
|
1033
|
+
});
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
describe('collectPerformanceData (via analyzeSourceFile)', () => {
|
|
1037
|
+
it('detects await inside for loop', () => {
|
|
1038
|
+
const src = parse(`async function f(urls: string[]) {
|
|
1039
|
+
for (const url of urls) { await fetch(url); }
|
|
1040
|
+
}`);
|
|
1041
|
+
const result = analyzeSourceFile(
|
|
1042
|
+
src,
|
|
1043
|
+
'pkg',
|
|
1044
|
+
emptyPackageSummary(),
|
|
1045
|
+
testOpts,
|
|
1046
|
+
emptyMaps(),
|
|
1047
|
+
[],
|
|
1048
|
+
emptyProfile
|
|
1049
|
+
);
|
|
1050
|
+
expect(result.awaitInLoopLocations!.length).toBeGreaterThanOrEqual(1);
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
it('detects await inside while loop', () => {
|
|
1054
|
+
const src = parse(`async function f() {
|
|
1055
|
+
let i = 0;
|
|
1056
|
+
while (i < 10) { await fetch("url"); i++; }
|
|
1057
|
+
}`);
|
|
1058
|
+
const result = analyzeSourceFile(
|
|
1059
|
+
src,
|
|
1060
|
+
'pkg',
|
|
1061
|
+
emptyPackageSummary(),
|
|
1062
|
+
testOpts,
|
|
1063
|
+
emptyMaps(),
|
|
1064
|
+
[],
|
|
1065
|
+
emptyProfile
|
|
1066
|
+
);
|
|
1067
|
+
expect(result.awaitInLoopLocations!.length).toBeGreaterThanOrEqual(1);
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
it('does not flag await outside loop', () => {
|
|
1071
|
+
const src = parse('async function f() { await fetch("url"); }');
|
|
1072
|
+
const result = analyzeSourceFile(
|
|
1073
|
+
src,
|
|
1074
|
+
'pkg',
|
|
1075
|
+
emptyPackageSummary(),
|
|
1076
|
+
testOpts,
|
|
1077
|
+
emptyMaps(),
|
|
1078
|
+
[],
|
|
1079
|
+
emptyProfile
|
|
1080
|
+
);
|
|
1081
|
+
expect(result.awaitInLoopLocations?.length).toBe(0);
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
it('detects sync I/O calls (readFileSync)', () => {
|
|
1085
|
+
const src = parse('function f() { fs.readFileSync("/path", "utf8"); }');
|
|
1086
|
+
const result = analyzeSourceFile(
|
|
1087
|
+
src,
|
|
1088
|
+
'pkg',
|
|
1089
|
+
emptyPackageSummary(),
|
|
1090
|
+
testOpts,
|
|
1091
|
+
emptyMaps(),
|
|
1092
|
+
[],
|
|
1093
|
+
emptyProfile
|
|
1094
|
+
);
|
|
1095
|
+
expect(result.syncIoCalls!.some(c => c.name === 'readFileSync')).toBe(true);
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
it('detects sync I/O calls (writeFileSync)', () => {
|
|
1099
|
+
const src = parse('function f() { fs.writeFileSync("/path", "data"); }');
|
|
1100
|
+
const result = analyzeSourceFile(
|
|
1101
|
+
src,
|
|
1102
|
+
'pkg',
|
|
1103
|
+
emptyPackageSummary(),
|
|
1104
|
+
testOpts,
|
|
1105
|
+
emptyMaps(),
|
|
1106
|
+
[],
|
|
1107
|
+
emptyProfile
|
|
1108
|
+
);
|
|
1109
|
+
expect(result.syncIoCalls!.some(c => c.name === 'writeFileSync')).toBe(
|
|
1110
|
+
true
|
|
1111
|
+
);
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
it('detects setInterval timer calls', () => {
|
|
1115
|
+
const src = parse('function f() { setInterval(() => {}, 1000); }');
|
|
1116
|
+
const result = analyzeSourceFile(
|
|
1117
|
+
src,
|
|
1118
|
+
'pkg',
|
|
1119
|
+
emptyPackageSummary(),
|
|
1120
|
+
testOpts,
|
|
1121
|
+
emptyMaps(),
|
|
1122
|
+
[],
|
|
1123
|
+
emptyProfile
|
|
1124
|
+
);
|
|
1125
|
+
expect(result.timerCalls!.some(t => t.kind === 'setInterval')).toBe(true);
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
it('detects setTimeout timer calls', () => {
|
|
1129
|
+
const src = parse('function f() { setTimeout(() => {}, 500); }');
|
|
1130
|
+
const result = analyzeSourceFile(
|
|
1131
|
+
src,
|
|
1132
|
+
'pkg',
|
|
1133
|
+
emptyPackageSummary(),
|
|
1134
|
+
testOpts,
|
|
1135
|
+
emptyMaps(),
|
|
1136
|
+
[],
|
|
1137
|
+
emptyProfile
|
|
1138
|
+
);
|
|
1139
|
+
expect(result.timerCalls!.some(t => t.kind === 'setTimeout')).toBe(true);
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
it('marks timer as having cleanup when clearInterval present', () => {
|
|
1143
|
+
const src = parse(`function f() {
|
|
1144
|
+
const id = setInterval(() => {}, 1000);
|
|
1145
|
+
clearInterval(id);
|
|
1146
|
+
}`);
|
|
1147
|
+
const result = analyzeSourceFile(
|
|
1148
|
+
src,
|
|
1149
|
+
'pkg',
|
|
1150
|
+
emptyPackageSummary(),
|
|
1151
|
+
testOpts,
|
|
1152
|
+
emptyMaps(),
|
|
1153
|
+
[],
|
|
1154
|
+
emptyProfile
|
|
1155
|
+
);
|
|
1156
|
+
expect(
|
|
1157
|
+
result.timerCalls!.some(t => t.kind === 'setInterval' && t.hasCleanup)
|
|
1158
|
+
).toBe(true);
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
it('marks timer without cleanup', () => {
|
|
1162
|
+
const src = parse('function f() { setInterval(() => {}, 1000); }');
|
|
1163
|
+
const result = analyzeSourceFile(
|
|
1164
|
+
src,
|
|
1165
|
+
'pkg',
|
|
1166
|
+
emptyPackageSummary(),
|
|
1167
|
+
testOpts,
|
|
1168
|
+
emptyMaps(),
|
|
1169
|
+
[],
|
|
1170
|
+
emptyProfile
|
|
1171
|
+
);
|
|
1172
|
+
expect(
|
|
1173
|
+
result.timerCalls!.some(t => t.kind === 'setInterval' && !t.hasCleanup)
|
|
1174
|
+
).toBe(true);
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
it('detects addEventListener registrations', () => {
|
|
1178
|
+
const src = parse(
|
|
1179
|
+
'function f(el: HTMLElement) { el.addEventListener("click", handler); }'
|
|
1180
|
+
);
|
|
1181
|
+
const result = analyzeSourceFile(
|
|
1182
|
+
src,
|
|
1183
|
+
'pkg',
|
|
1184
|
+
emptyPackageSummary(),
|
|
1185
|
+
testOpts,
|
|
1186
|
+
emptyMaps(),
|
|
1187
|
+
[],
|
|
1188
|
+
emptyProfile
|
|
1189
|
+
);
|
|
1190
|
+
expect(result.listenerRegistrations!.length).toBeGreaterThanOrEqual(1);
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
it('detects removeEventListener calls', () => {
|
|
1194
|
+
const src = parse(
|
|
1195
|
+
'function f(el: HTMLElement) { el.removeEventListener("click", handler); }'
|
|
1196
|
+
);
|
|
1197
|
+
const result = analyzeSourceFile(
|
|
1198
|
+
src,
|
|
1199
|
+
'pkg',
|
|
1200
|
+
emptyPackageSummary(),
|
|
1201
|
+
testOpts,
|
|
1202
|
+
emptyMaps(),
|
|
1203
|
+
[],
|
|
1204
|
+
emptyProfile
|
|
1205
|
+
);
|
|
1206
|
+
expect(result.listenerRemovals!.length).toBeGreaterThanOrEqual(1);
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
it('detects .on() and .off() for event listeners', () => {
|
|
1210
|
+
const src = parse(`function f(emitter: any) {
|
|
1211
|
+
emitter.on("data", handler);
|
|
1212
|
+
emitter.off("data", handler);
|
|
1213
|
+
}`);
|
|
1214
|
+
const result = analyzeSourceFile(
|
|
1215
|
+
src,
|
|
1216
|
+
'pkg',
|
|
1217
|
+
emptyPackageSummary(),
|
|
1218
|
+
testOpts,
|
|
1219
|
+
emptyMaps(),
|
|
1220
|
+
[],
|
|
1221
|
+
emptyProfile
|
|
1222
|
+
);
|
|
1223
|
+
expect(result.listenerRegistrations!.length).toBeGreaterThanOrEqual(1);
|
|
1224
|
+
expect(result.listenerRemovals!.length).toBeGreaterThanOrEqual(1);
|
|
1225
|
+
});
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
describe('collectInputSourceProfile (via analyzeSourceFile)', () => {
|
|
1229
|
+
it('detects function with req parameter as high-confidence input source', () => {
|
|
1230
|
+
const src = parse('function handler(req: any) { eval(req.body); }');
|
|
1231
|
+
const result = analyzeSourceFile(
|
|
1232
|
+
src,
|
|
1233
|
+
'pkg',
|
|
1234
|
+
emptyPackageSummary(),
|
|
1235
|
+
testOpts,
|
|
1236
|
+
emptyMaps(),
|
|
1237
|
+
[],
|
|
1238
|
+
emptyProfile
|
|
1239
|
+
);
|
|
1240
|
+
expect(result.inputSources!.length).toBeGreaterThanOrEqual(1);
|
|
1241
|
+
expect(result.inputSources![0].paramConfidence).toBe('high');
|
|
1242
|
+
expect(result.inputSources![0].sourceParams).toContain('req');
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
it('detects sinks in function body', () => {
|
|
1246
|
+
const src = parse('function handler(req: any) { eval(req.query); }');
|
|
1247
|
+
const result = analyzeSourceFile(
|
|
1248
|
+
src,
|
|
1249
|
+
'pkg',
|
|
1250
|
+
emptyPackageSummary(),
|
|
1251
|
+
testOpts,
|
|
1252
|
+
emptyMaps(),
|
|
1253
|
+
[],
|
|
1254
|
+
emptyProfile
|
|
1255
|
+
);
|
|
1256
|
+
expect(result.inputSources![0].hasSinkInBody).toBe(true);
|
|
1257
|
+
expect(result.inputSources![0].sinkKinds).toContain('eval');
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
it('detects validation patterns (typeof check)', () => {
|
|
1261
|
+
const src = parse(
|
|
1262
|
+
'function handler(req: any) { if (typeof req === "object") { console.log(req); } }'
|
|
1263
|
+
);
|
|
1264
|
+
const result = analyzeSourceFile(
|
|
1265
|
+
src,
|
|
1266
|
+
'pkg',
|
|
1267
|
+
emptyPackageSummary(),
|
|
1268
|
+
testOpts,
|
|
1269
|
+
emptyMaps(),
|
|
1270
|
+
[],
|
|
1271
|
+
emptyProfile
|
|
1272
|
+
);
|
|
1273
|
+
expect(result.inputSources![0].hasValidation).toBe(true);
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
it('detects validation via schema validators (zod/joi)', () => {
|
|
1277
|
+
const src = parse(
|
|
1278
|
+
'function handler(body: any) { const result = z.parse(body); }'
|
|
1279
|
+
);
|
|
1280
|
+
const result = analyzeSourceFile(
|
|
1281
|
+
src,
|
|
1282
|
+
'pkg',
|
|
1283
|
+
emptyPackageSummary(),
|
|
1284
|
+
testOpts,
|
|
1285
|
+
emptyMaps(),
|
|
1286
|
+
[],
|
|
1287
|
+
emptyProfile
|
|
1288
|
+
);
|
|
1289
|
+
expect(result.inputSources![0].hasValidation).toBe(true);
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
it('does not collect input sources for test files', () => {
|
|
1293
|
+
const src = parse(
|
|
1294
|
+
'function handler(req: any) { eval(req); }',
|
|
1295
|
+
'/repo/src/test.test.ts'
|
|
1296
|
+
);
|
|
1297
|
+
const result = analyzeSourceFile(
|
|
1298
|
+
src,
|
|
1299
|
+
'pkg',
|
|
1300
|
+
emptyPackageSummary(),
|
|
1301
|
+
testOpts,
|
|
1302
|
+
emptyMaps(),
|
|
1303
|
+
[],
|
|
1304
|
+
emptyProfile
|
|
1305
|
+
);
|
|
1306
|
+
expect(result.inputSources).toBeUndefined();
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
it('detects medium-confidence params (input, event)', () => {
|
|
1310
|
+
const src = parse('function handler(input: any) { console.log(input); }');
|
|
1311
|
+
const result = analyzeSourceFile(
|
|
1312
|
+
src,
|
|
1313
|
+
'pkg',
|
|
1314
|
+
emptyPackageSummary(),
|
|
1315
|
+
testOpts,
|
|
1316
|
+
emptyMaps(),
|
|
1317
|
+
[],
|
|
1318
|
+
emptyProfile
|
|
1319
|
+
);
|
|
1320
|
+
expect(result.inputSources![0].paramConfidence).toBe('medium');
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
it('tracks calls with input args', () => {
|
|
1324
|
+
const src = parse('function handler(req: any) { processData(req.body); }');
|
|
1325
|
+
const result = analyzeSourceFile(
|
|
1326
|
+
src,
|
|
1327
|
+
'pkg',
|
|
1328
|
+
emptyPackageSummary(),
|
|
1329
|
+
testOpts,
|
|
1330
|
+
emptyMaps(),
|
|
1331
|
+
[],
|
|
1332
|
+
emptyProfile
|
|
1333
|
+
);
|
|
1334
|
+
expect(
|
|
1335
|
+
result.inputSources![0].callsWithInputArgs.length
|
|
1336
|
+
).toBeGreaterThanOrEqual(1);
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
it('does not collect for functions without source params', () => {
|
|
1340
|
+
const src = parse('function helper(count: number) { return count + 1; }');
|
|
1341
|
+
const result = analyzeSourceFile(
|
|
1342
|
+
src,
|
|
1343
|
+
'pkg',
|
|
1344
|
+
emptyPackageSummary(),
|
|
1345
|
+
testOpts,
|
|
1346
|
+
emptyMaps(),
|
|
1347
|
+
[],
|
|
1348
|
+
emptyProfile
|
|
1349
|
+
);
|
|
1350
|
+
expect(result.inputSources?.length ?? 0).toBe(0);
|
|
1351
|
+
});
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
describe('collectTopLevelEffects (via analyzeSourceFile)', () => {
|
|
1355
|
+
it('detects side-effect imports', () => {
|
|
1356
|
+
const src = parse("import './polyfill';");
|
|
1357
|
+
const result = analyzeSourceFile(
|
|
1358
|
+
src,
|
|
1359
|
+
'pkg',
|
|
1360
|
+
emptyPackageSummary(),
|
|
1361
|
+
testOpts,
|
|
1362
|
+
emptyMaps(),
|
|
1363
|
+
[],
|
|
1364
|
+
emptyProfile
|
|
1365
|
+
);
|
|
1366
|
+
expect(
|
|
1367
|
+
result.topLevelEffects!.some(e => e.kind === 'side-effect-import')
|
|
1368
|
+
).toBe(true);
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
it('detects top-level setInterval', () => {
|
|
1372
|
+
const src = parse('setInterval(() => {}, 1000);');
|
|
1373
|
+
const result = analyzeSourceFile(
|
|
1374
|
+
src,
|
|
1375
|
+
'pkg',
|
|
1376
|
+
emptyPackageSummary(),
|
|
1377
|
+
testOpts,
|
|
1378
|
+
emptyMaps(),
|
|
1379
|
+
[],
|
|
1380
|
+
emptyProfile
|
|
1381
|
+
);
|
|
1382
|
+
expect(result.topLevelEffects!.some(e => e.kind === 'timer')).toBe(true);
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
it('detects top-level eval', () => {
|
|
1386
|
+
const src = parse('eval("console.log(1)");');
|
|
1387
|
+
const result = analyzeSourceFile(
|
|
1388
|
+
src,
|
|
1389
|
+
'pkg',
|
|
1390
|
+
emptyPackageSummary(),
|
|
1391
|
+
testOpts,
|
|
1392
|
+
emptyMaps(),
|
|
1393
|
+
[],
|
|
1394
|
+
emptyProfile
|
|
1395
|
+
);
|
|
1396
|
+
expect(result.topLevelEffects!.some(e => e.kind === 'eval')).toBe(true);
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
it('detects top-level new Function()', () => {
|
|
1400
|
+
const src = parse('new Function("return 1");');
|
|
1401
|
+
const result = analyzeSourceFile(
|
|
1402
|
+
src,
|
|
1403
|
+
'pkg',
|
|
1404
|
+
emptyPackageSummary(),
|
|
1405
|
+
testOpts,
|
|
1406
|
+
emptyMaps(),
|
|
1407
|
+
[],
|
|
1408
|
+
emptyProfile
|
|
1409
|
+
);
|
|
1410
|
+
expect(result.topLevelEffects!.some(e => e.kind === 'eval')).toBe(true);
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
it('detects top-level sync I/O in variable initializer', () => {
|
|
1414
|
+
const src = parse('const data = fs.readFileSync("/path", "utf8");');
|
|
1415
|
+
const result = analyzeSourceFile(
|
|
1416
|
+
src,
|
|
1417
|
+
'pkg',
|
|
1418
|
+
emptyPackageSummary(),
|
|
1419
|
+
testOpts,
|
|
1420
|
+
emptyMaps(),
|
|
1421
|
+
[],
|
|
1422
|
+
emptyProfile
|
|
1423
|
+
);
|
|
1424
|
+
expect(result.topLevelEffects!.some(e => e.kind === 'sync-io')).toBe(true);
|
|
1425
|
+
});
|
|
1426
|
+
|
|
1427
|
+
it('detects top-level execSync', () => {
|
|
1428
|
+
const src = parse('const out = cp.execSync("ls");');
|
|
1429
|
+
const result = analyzeSourceFile(
|
|
1430
|
+
src,
|
|
1431
|
+
'pkg',
|
|
1432
|
+
emptyPackageSummary(),
|
|
1433
|
+
testOpts,
|
|
1434
|
+
emptyMaps(),
|
|
1435
|
+
[],
|
|
1436
|
+
emptyProfile
|
|
1437
|
+
);
|
|
1438
|
+
expect(result.topLevelEffects!.some(e => e.kind === 'exec-sync')).toBe(
|
|
1439
|
+
true
|
|
1440
|
+
);
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
it('detects top-level process.on listener', () => {
|
|
1444
|
+
const src = parse('process.on("uncaughtException", handler);');
|
|
1445
|
+
const result = analyzeSourceFile(
|
|
1446
|
+
src,
|
|
1447
|
+
'pkg',
|
|
1448
|
+
emptyPackageSummary(),
|
|
1449
|
+
testOpts,
|
|
1450
|
+
emptyMaps(),
|
|
1451
|
+
[],
|
|
1452
|
+
emptyProfile
|
|
1453
|
+
);
|
|
1454
|
+
expect(
|
|
1455
|
+
result.topLevelEffects!.some(e => e.kind === 'process-handler')
|
|
1456
|
+
).toBe(true);
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1459
|
+
it('does not collect effects for test files', () => {
|
|
1460
|
+
const src = parse('eval("1");', '/repo/src/test.test.ts');
|
|
1461
|
+
const result = analyzeSourceFile(
|
|
1462
|
+
src,
|
|
1463
|
+
'pkg',
|
|
1464
|
+
emptyPackageSummary(),
|
|
1465
|
+
testOpts,
|
|
1466
|
+
emptyMaps(),
|
|
1467
|
+
[],
|
|
1468
|
+
emptyProfile
|
|
1469
|
+
);
|
|
1470
|
+
expect(result.topLevelEffects).toBeUndefined();
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1473
|
+
it('does not flag function declarations as effects', () => {
|
|
1474
|
+
const src = parse('function f() { fs.readFileSync("/path"); }');
|
|
1475
|
+
const result = analyzeSourceFile(
|
|
1476
|
+
src,
|
|
1477
|
+
'pkg',
|
|
1478
|
+
emptyPackageSummary(),
|
|
1479
|
+
testOpts,
|
|
1480
|
+
emptyMaps(),
|
|
1481
|
+
[],
|
|
1482
|
+
emptyProfile
|
|
1483
|
+
);
|
|
1484
|
+
expect(result.topLevelEffects?.length ?? 0).toBe(0);
|
|
1485
|
+
});
|
|
1486
|
+
|
|
1487
|
+
it('skips regular import declarations', () => {
|
|
1488
|
+
const src = parse("import path from 'node:path';");
|
|
1489
|
+
const result = analyzeSourceFile(
|
|
1490
|
+
src,
|
|
1491
|
+
'pkg',
|
|
1492
|
+
emptyPackageSummary(),
|
|
1493
|
+
testOpts,
|
|
1494
|
+
emptyMaps(),
|
|
1495
|
+
[],
|
|
1496
|
+
emptyProfile
|
|
1497
|
+
);
|
|
1498
|
+
expect(
|
|
1499
|
+
result.topLevelEffects?.some(e => e.kind === 'side-effect-import')
|
|
1500
|
+
).toBeFalsy();
|
|
1501
|
+
});
|
|
1502
|
+
});
|
|
1503
|
+
|
|
1504
|
+
describe('collectPrototypePollutionSites (via analyzeSourceFile)', () => {
|
|
1505
|
+
it('detects Object.assign with 2+ args', () => {
|
|
1506
|
+
const src = parse('function f(a: any, b: any) { Object.assign(a, b); }');
|
|
1507
|
+
const result = analyzeSourceFile(
|
|
1508
|
+
src,
|
|
1509
|
+
'pkg',
|
|
1510
|
+
emptyPackageSummary(),
|
|
1511
|
+
testOpts,
|
|
1512
|
+
emptyMaps(),
|
|
1513
|
+
[],
|
|
1514
|
+
emptyProfile
|
|
1515
|
+
);
|
|
1516
|
+
expect(
|
|
1517
|
+
result.prototypePollutionSites!.some(s => s.kind === 'object-assign')
|
|
1518
|
+
).toBe(true);
|
|
1519
|
+
});
|
|
1520
|
+
|
|
1521
|
+
it('detects deep merge calls', () => {
|
|
1522
|
+
const src = parse(
|
|
1523
|
+
'function f(target: any, src: any) { merge(target, src); }'
|
|
1524
|
+
);
|
|
1525
|
+
const result = analyzeSourceFile(
|
|
1526
|
+
src,
|
|
1527
|
+
'pkg',
|
|
1528
|
+
emptyPackageSummary(),
|
|
1529
|
+
testOpts,
|
|
1530
|
+
emptyMaps(),
|
|
1531
|
+
[],
|
|
1532
|
+
emptyProfile
|
|
1533
|
+
);
|
|
1534
|
+
expect(
|
|
1535
|
+
result.prototypePollutionSites!.some(s => s.kind === 'deep-merge')
|
|
1536
|
+
).toBe(true);
|
|
1537
|
+
});
|
|
1538
|
+
|
|
1539
|
+
it('detects computed property writes', () => {
|
|
1540
|
+
const src = parse(
|
|
1541
|
+
'function f(obj: any, key: string, val: any) { obj[key] = val; }'
|
|
1542
|
+
);
|
|
1543
|
+
const result = analyzeSourceFile(
|
|
1544
|
+
src,
|
|
1545
|
+
'pkg',
|
|
1546
|
+
emptyPackageSummary(),
|
|
1547
|
+
testOpts,
|
|
1548
|
+
emptyMaps(),
|
|
1549
|
+
[],
|
|
1550
|
+
emptyProfile
|
|
1551
|
+
);
|
|
1552
|
+
expect(
|
|
1553
|
+
result.prototypePollutionSites!.some(
|
|
1554
|
+
s => s.kind === 'computed-property-write'
|
|
1555
|
+
)
|
|
1556
|
+
).toBe(true);
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1559
|
+
it('marks computed writes from internal iteration as guarded', () => {
|
|
1560
|
+
const src =
|
|
1561
|
+
parse(`function f(obj: Record<string, any>, source: Record<string, any>) {
|
|
1562
|
+
for (const key of Object.keys(source)) { obj[key] = source[key]; }
|
|
1563
|
+
}`);
|
|
1564
|
+
const result = analyzeSourceFile(
|
|
1565
|
+
src,
|
|
1566
|
+
'pkg',
|
|
1567
|
+
emptyPackageSummary(),
|
|
1568
|
+
testOpts,
|
|
1569
|
+
emptyMaps(),
|
|
1570
|
+
[],
|
|
1571
|
+
emptyProfile
|
|
1572
|
+
);
|
|
1573
|
+
const cpw = result.prototypePollutionSites!.filter(
|
|
1574
|
+
s => s.kind === 'computed-property-write'
|
|
1575
|
+
);
|
|
1576
|
+
expect(cpw.length).toBeGreaterThanOrEqual(1);
|
|
1577
|
+
expect(cpw.some(s => s.guarded)).toBe(true);
|
|
1578
|
+
});
|
|
1579
|
+
|
|
1580
|
+
it('marks computed writes with __proto__ guard as guarded', () => {
|
|
1581
|
+
const src = parse(`function f(obj: any, key: string, val: any) {
|
|
1582
|
+
if (key === '__proto__' || key === 'constructor') return;
|
|
1583
|
+
obj[key] = val;
|
|
1584
|
+
}`);
|
|
1585
|
+
const result = analyzeSourceFile(
|
|
1586
|
+
src,
|
|
1587
|
+
'pkg',
|
|
1588
|
+
emptyPackageSummary(),
|
|
1589
|
+
testOpts,
|
|
1590
|
+
emptyMaps(),
|
|
1591
|
+
[],
|
|
1592
|
+
emptyProfile
|
|
1593
|
+
);
|
|
1594
|
+
const cpw = result.prototypePollutionSites!.filter(
|
|
1595
|
+
s => s.kind === 'computed-property-write'
|
|
1596
|
+
);
|
|
1597
|
+
expect(cpw.some(s => s.guarded)).toBe(true);
|
|
1598
|
+
});
|
|
1599
|
+
|
|
1600
|
+
it('does not flag string literal property access', () => {
|
|
1601
|
+
const src = parse('function f(obj: any) { obj["known"] = 1; }');
|
|
1602
|
+
const result = analyzeSourceFile(
|
|
1603
|
+
src,
|
|
1604
|
+
'pkg',
|
|
1605
|
+
emptyPackageSummary(),
|
|
1606
|
+
testOpts,
|
|
1607
|
+
emptyMaps(),
|
|
1608
|
+
[],
|
|
1609
|
+
emptyProfile
|
|
1610
|
+
);
|
|
1611
|
+
const cpw = (result.prototypePollutionSites ?? []).filter(
|
|
1612
|
+
s => s.kind === 'computed-property-write'
|
|
1613
|
+
);
|
|
1614
|
+
expect(cpw.length).toBe(0);
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
it('does not collect for test files', () => {
|
|
1618
|
+
const src = parse(
|
|
1619
|
+
'function f(obj: any, key: string) { obj[key] = 1; }',
|
|
1620
|
+
'/repo/src/test.test.ts'
|
|
1621
|
+
);
|
|
1622
|
+
const result = analyzeSourceFile(
|
|
1623
|
+
src,
|
|
1624
|
+
'pkg',
|
|
1625
|
+
emptyPackageSummary(),
|
|
1626
|
+
testOpts,
|
|
1627
|
+
emptyMaps(),
|
|
1628
|
+
[],
|
|
1629
|
+
emptyProfile
|
|
1630
|
+
);
|
|
1631
|
+
expect(result.prototypePollutionSites).toBeUndefined();
|
|
1632
|
+
});
|
|
1633
|
+
});
|
|
1634
|
+
|
|
1635
|
+
describe('collectTestProfile (via analyzeSourceFile)', () => {
|
|
1636
|
+
const testFileName = '/repo/src/feature.test.ts';
|
|
1637
|
+
|
|
1638
|
+
function parseTest(code: string): ts.SourceFile {
|
|
1639
|
+
return ts.createSourceFile(
|
|
1640
|
+
testFileName,
|
|
1641
|
+
code,
|
|
1642
|
+
ts.ScriptTarget.ESNext,
|
|
1643
|
+
true
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
it('collects test blocks with assertion counts', () => {
|
|
1648
|
+
const src = parseTest(`describe('suite', () => {
|
|
1649
|
+
it('test', () => { expect(1).toBe(1); });
|
|
1650
|
+
});`);
|
|
1651
|
+
const result = analyzeSourceFile(
|
|
1652
|
+
src,
|
|
1653
|
+
'pkg',
|
|
1654
|
+
emptyPackageSummary(),
|
|
1655
|
+
testOpts,
|
|
1656
|
+
emptyMaps(),
|
|
1657
|
+
[],
|
|
1658
|
+
emptyProfile
|
|
1659
|
+
);
|
|
1660
|
+
expect(result.testProfile!.testBlocks.length).toBeGreaterThanOrEqual(1);
|
|
1661
|
+
expect(result.testProfile!.testBlocks[0].assertionCount).toBe(1);
|
|
1662
|
+
});
|
|
1663
|
+
|
|
1664
|
+
it('collects mock calls', () => {
|
|
1665
|
+
const src = parseTest(`test('mocked', () => { jest.mock('lodash'); });`);
|
|
1666
|
+
const result = analyzeSourceFile(
|
|
1667
|
+
src,
|
|
1668
|
+
'pkg',
|
|
1669
|
+
emptyPackageSummary(),
|
|
1670
|
+
testOpts,
|
|
1671
|
+
emptyMaps(),
|
|
1672
|
+
[],
|
|
1673
|
+
emptyProfile
|
|
1674
|
+
);
|
|
1675
|
+
expect(result.testProfile!.mockCalls.length).toBeGreaterThanOrEqual(1);
|
|
1676
|
+
});
|
|
1677
|
+
|
|
1678
|
+
it('collects setup calls (beforeAll, afterEach)', () => {
|
|
1679
|
+
const src = parseTest(`describe('s', () => {
|
|
1680
|
+
beforeAll(() => {});
|
|
1681
|
+
afterEach(() => {});
|
|
1682
|
+
it('t', () => {});
|
|
1683
|
+
});`);
|
|
1684
|
+
const result = analyzeSourceFile(
|
|
1685
|
+
src,
|
|
1686
|
+
'pkg',
|
|
1687
|
+
emptyPackageSummary(),
|
|
1688
|
+
testOpts,
|
|
1689
|
+
emptyMaps(),
|
|
1690
|
+
[],
|
|
1691
|
+
emptyProfile
|
|
1692
|
+
);
|
|
1693
|
+
expect(
|
|
1694
|
+
result.testProfile!.setupCalls.some(c => c.kind === 'beforeAll')
|
|
1695
|
+
).toBe(true);
|
|
1696
|
+
expect(
|
|
1697
|
+
result.testProfile!.setupCalls.some(c => c.kind === 'afterEach')
|
|
1698
|
+
).toBe(true);
|
|
1699
|
+
});
|
|
1700
|
+
|
|
1701
|
+
it('collects mutable state declarations (let at describe scope)', () => {
|
|
1702
|
+
const src = parseTest(`describe('s', () => {
|
|
1703
|
+
let counter = 0;
|
|
1704
|
+
it('t', () => { counter++; });
|
|
1705
|
+
});`);
|
|
1706
|
+
const result = analyzeSourceFile(
|
|
1707
|
+
src,
|
|
1708
|
+
'pkg',
|
|
1709
|
+
emptyPackageSummary(),
|
|
1710
|
+
testOpts,
|
|
1711
|
+
emptyMaps(),
|
|
1712
|
+
[],
|
|
1713
|
+
emptyProfile
|
|
1714
|
+
);
|
|
1715
|
+
expect(result.testProfile!.mutableStateDecls.length).toBeGreaterThanOrEqual(
|
|
1716
|
+
1
|
|
1717
|
+
);
|
|
1718
|
+
});
|
|
1719
|
+
|
|
1720
|
+
it('does not flag const at describe scope as mutable', () => {
|
|
1721
|
+
const src = parseTest(`describe('s', () => {
|
|
1722
|
+
const val = 42;
|
|
1723
|
+
it('t', () => { expect(val).toBe(42); });
|
|
1724
|
+
});`);
|
|
1725
|
+
const result = analyzeSourceFile(
|
|
1726
|
+
src,
|
|
1727
|
+
'pkg',
|
|
1728
|
+
emptyPackageSummary(),
|
|
1729
|
+
testOpts,
|
|
1730
|
+
emptyMaps(),
|
|
1731
|
+
[],
|
|
1732
|
+
emptyProfile
|
|
1733
|
+
);
|
|
1734
|
+
expect(result.testProfile!.mutableStateDecls.length).toBe(0);
|
|
1735
|
+
});
|
|
1736
|
+
|
|
1737
|
+
it('collects focused tests (it.only)', () => {
|
|
1738
|
+
const src = parseTest(`describe('s', () => {
|
|
1739
|
+
it.only('focused', () => { expect(1).toBe(1); });
|
|
1740
|
+
});`);
|
|
1741
|
+
const result = analyzeSourceFile(
|
|
1742
|
+
src,
|
|
1743
|
+
'pkg',
|
|
1744
|
+
emptyPackageSummary(),
|
|
1745
|
+
testOpts,
|
|
1746
|
+
emptyMaps(),
|
|
1747
|
+
[],
|
|
1748
|
+
emptyProfile
|
|
1749
|
+
);
|
|
1750
|
+
expect(result.testProfile!.focusedCalls.length).toBeGreaterThanOrEqual(1);
|
|
1751
|
+
});
|
|
1752
|
+
|
|
1753
|
+
it('collects timer controls (useFakeTimers, useRealTimers)', () => {
|
|
1754
|
+
const src = parseTest(`test('timers', () => {
|
|
1755
|
+
jest.useFakeTimers();
|
|
1756
|
+
jest.useRealTimers();
|
|
1757
|
+
});`);
|
|
1758
|
+
const result = analyzeSourceFile(
|
|
1759
|
+
src,
|
|
1760
|
+
'pkg',
|
|
1761
|
+
emptyPackageSummary(),
|
|
1762
|
+
testOpts,
|
|
1763
|
+
emptyMaps(),
|
|
1764
|
+
[],
|
|
1765
|
+
emptyProfile
|
|
1766
|
+
);
|
|
1767
|
+
expect(
|
|
1768
|
+
result.testProfile!.timerControls.some(
|
|
1769
|
+
t => t.kind === 'jest.useFakeTimers'
|
|
1770
|
+
)
|
|
1771
|
+
).toBe(true);
|
|
1772
|
+
expect(
|
|
1773
|
+
result.testProfile!.timerControls.some(
|
|
1774
|
+
t => t.kind === 'jest.useRealTimers'
|
|
1775
|
+
)
|
|
1776
|
+
).toBe(true);
|
|
1777
|
+
});
|
|
1778
|
+
|
|
1779
|
+
it('collects spy/stub calls', () => {
|
|
1780
|
+
const src = parseTest(`test('spy', () => {
|
|
1781
|
+
const spy = jest.spyOn(Date, 'now');
|
|
1782
|
+
});`);
|
|
1783
|
+
const result = analyzeSourceFile(
|
|
1784
|
+
src,
|
|
1785
|
+
'pkg',
|
|
1786
|
+
emptyPackageSummary(),
|
|
1787
|
+
testOpts,
|
|
1788
|
+
emptyMaps(),
|
|
1789
|
+
[],
|
|
1790
|
+
emptyProfile
|
|
1791
|
+
);
|
|
1792
|
+
expect(result.testProfile!.spyOrStubCalls!.length).toBeGreaterThanOrEqual(
|
|
1793
|
+
1
|
|
1794
|
+
);
|
|
1795
|
+
expect(result.testProfile!.spyOrStubCalls![0].kind).toBe('spy');
|
|
1796
|
+
});
|
|
1797
|
+
|
|
1798
|
+
it('collects mockRestore calls', () => {
|
|
1799
|
+
const src = parseTest(`test('restore', () => {
|
|
1800
|
+
const spy = jest.spyOn(Date, 'now');
|
|
1801
|
+
spy.mockRestore();
|
|
1802
|
+
});`);
|
|
1803
|
+
const result = analyzeSourceFile(
|
|
1804
|
+
src,
|
|
1805
|
+
'pkg',
|
|
1806
|
+
emptyPackageSummary(),
|
|
1807
|
+
testOpts,
|
|
1808
|
+
emptyMaps(),
|
|
1809
|
+
[],
|
|
1810
|
+
emptyProfile
|
|
1811
|
+
);
|
|
1812
|
+
expect(
|
|
1813
|
+
result.testProfile!.mockRestores!.some(r => r.kind === 'restore')
|
|
1814
|
+
).toBe(true);
|
|
1815
|
+
});
|
|
1816
|
+
|
|
1817
|
+
it('collects restoreAllMocks calls', () => {
|
|
1818
|
+
const src = parseTest(`afterEach(() => { jest.restoreAllMocks(); });`);
|
|
1819
|
+
const result = analyzeSourceFile(
|
|
1820
|
+
src,
|
|
1821
|
+
'pkg',
|
|
1822
|
+
emptyPackageSummary(),
|
|
1823
|
+
testOpts,
|
|
1824
|
+
emptyMaps(),
|
|
1825
|
+
[],
|
|
1826
|
+
emptyProfile
|
|
1827
|
+
);
|
|
1828
|
+
expect(
|
|
1829
|
+
result.testProfile!.mockRestores!.some(r => r.kind === 'restoreAll')
|
|
1830
|
+
).toBe(true);
|
|
1831
|
+
});
|
|
1832
|
+
|
|
1833
|
+
it('does not collect test profile for non-test files', () => {
|
|
1834
|
+
const src = parse('function f() {}');
|
|
1835
|
+
const result = analyzeSourceFile(
|
|
1836
|
+
src,
|
|
1837
|
+
'pkg',
|
|
1838
|
+
emptyPackageSummary(),
|
|
1839
|
+
testOpts,
|
|
1840
|
+
emptyMaps(),
|
|
1841
|
+
[],
|
|
1842
|
+
emptyProfile
|
|
1843
|
+
);
|
|
1844
|
+
expect(result.testProfile).toBeUndefined();
|
|
1845
|
+
});
|
|
1846
|
+
|
|
1847
|
+
it('collects test block names from string literals', () => {
|
|
1848
|
+
const src = parseTest(
|
|
1849
|
+
`it('should work correctly', () => { expect(true).toBe(true); });`
|
|
1850
|
+
);
|
|
1851
|
+
const result = analyzeSourceFile(
|
|
1852
|
+
src,
|
|
1853
|
+
'pkg',
|
|
1854
|
+
emptyPackageSummary(),
|
|
1855
|
+
testOpts,
|
|
1856
|
+
emptyMaps(),
|
|
1857
|
+
[],
|
|
1858
|
+
emptyProfile
|
|
1859
|
+
);
|
|
1860
|
+
expect(result.testProfile!.testBlocks[0].name).toBe(
|
|
1861
|
+
'should work correctly'
|
|
1862
|
+
);
|
|
1863
|
+
});
|
|
1864
|
+
});
|