octocode-cli 1.2.7 → 1.2.9
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 +42 -35
- package/out/octocode-cli.js +36 -11719
- package/package.json +36 -36
- package/skills/README.md +42 -114
- package/skills/{octocode-code-engineer → octocode-engineer}/.claude/settings.local.json +2 -1
- package/skills/octocode-engineer/README.md +99 -0
- package/skills/octocode-engineer/SKILL.md +499 -0
- package/skills/octocode-engineer/build.mjs +29 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/eslint.config.mjs +3 -13
- package/skills/{octocode-code-engineer → octocode-engineer}/package.json +28 -27
- package/skills/octocode-engineer/references/ast-reference.md +166 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/references/cli-reference.md +80 -6
- package/skills/octocode-engineer/references/externals.md +86 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/references/output-files.md +46 -6
- package/skills/octocode-engineer/references/quality-indicators.md +202 -0
- package/skills/octocode-engineer/references/tool-workflows.md +298 -0
- package/skills/octocode-engineer/references/validation-playbooks.md +99 -0
- package/skills/octocode-engineer/scripts/ast/search.js +45 -0
- package/skills/octocode-engineer/scripts/ast/tree-search.js +27 -0
- package/skills/octocode-engineer/scripts/index.js +173 -0
- package/skills/octocode-engineer/scripts/run.js +179 -0
- package/skills/octocode-engineer/src/analysis/dependencies.ts +378 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/discovery.test.ts +57 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/discovery.ts +43 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/search.test.ts +113 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/search.ts +64 -1
- package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/tree-sitter.test.ts +118 -2
- package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/tree-sitter.ts +65 -3
- package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/ts-analyzer.test.ts +281 -1
- package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/ts-analyzer.ts +173 -3
- package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/security.test.ts +73 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/security.ts +62 -4
- package/skills/octocode-engineer/src/detector-gating.test.ts +59 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/code-quality.ts +342 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/index.ts +8 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/index.test.ts +565 -11
- package/skills/octocode-engineer/src/index.ts +468 -0
- package/skills/octocode-engineer/src/pipeline/affected.test.ts +147 -0
- package/skills/octocode-engineer/src/pipeline/affected.ts +68 -0
- package/skills/octocode-engineer/src/pipeline/baseline.test.ts +276 -0
- package/skills/octocode-engineer/src/pipeline/baseline.ts +76 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/pipeline/cli.test.ts +300 -53
- package/skills/{octocode-code-engineer → octocode-engineer}/src/pipeline/cli.ts +180 -36
- package/skills/octocode-engineer/src/pipeline/config-loader.test.ts +264 -0
- package/skills/octocode-engineer/src/pipeline/config-loader.ts +109 -0
- package/skills/octocode-engineer/src/pipeline/create-options.ts +55 -0
- package/skills/octocode-engineer/src/pipeline/health-score.test.ts +65 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/pipeline/main.ts +130 -17
- package/skills/octocode-engineer/src/pipeline/progress.ts +51 -0
- package/skills/octocode-engineer/src/pipeline/reporters.test.ts +155 -0
- package/skills/octocode-engineer/src/pipeline/reporters.ts +64 -0
- package/skills/octocode-engineer/src/reporting/graph-features.test.ts +279 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/reporting/output-contract.test.ts +6 -0
- package/skills/octocode-engineer/src/reporting/summary-md.test.ts +1066 -0
- package/skills/octocode-engineer/src/reporting/summary-md.ts +1604 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/reporting/writer.ts +136 -13
- package/skills/octocode-engineer/src/run.ts +78 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/sanity.test.ts +1 -1
- package/skills/octocode-engineer/src/types/analysis.ts +25 -0
- package/skills/octocode-engineer/src/types/collectors.ts +134 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/types/constants.ts +75 -41
- package/skills/octocode-engineer/src/types/core.ts +203 -0
- package/skills/octocode-engineer/src/types/dependency.ts +215 -0
- package/skills/octocode-engineer/src/types/file-entry.ts +108 -0
- package/skills/octocode-engineer/src/types/findings.ts +105 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/types/index.ts +60 -30
- package/skills/octocode-engineer/src/types/tree-sitter.ts +38 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/tsconfig.json +1 -0
- package/skills/octocode-research/.octocode/scan/.cache/analysis-cache.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/architecture.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/ast-trees.txt +5566 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/code-quality.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/dead-code.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/file-inventory.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/findings.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/graph.md +189 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/security.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/summary.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/summary.md +265 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/architecture.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/ast-trees.txt +5555 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/code-quality.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/dead-code.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/file-inventory.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/findings.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/graph.md +190 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/security.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/summary.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/summary.md +265 -0
- package/skills/octocode-research/CHANGELOG.md +60 -0
- package/skills/octocode-research/README.md +102 -388
- package/skills/octocode-research/SKILL.md +169 -498
- package/skills/octocode-research/package.json +19 -31
- package/skills/octocode-research/references/PARALLEL_AGENT_PROTOCOL.md +19 -0
- package/skills/octocode-research/references/SESSION_MANAGEMENT.md +38 -0
- package/skills/octocode-research/scripts/server-init.js +1 -1
- package/skills/octocode-research/scripts/server.d.ts +2 -1
- package/skills/octocode-research/scripts/server.js +329 -233
- package/skills/octocode-research/src/__tests__/integration/promptsRoutes.test.ts +180 -0
- package/skills/octocode-research/src/__tests__/integration/serverHttp.test.ts +221 -0
- package/skills/octocode-research/src/__tests__/integration/serverLifecycle.test.ts +194 -0
- package/skills/octocode-research/src/__tests__/integration/toolsRoutes.test.ts +501 -0
- package/skills/octocode-research/src/__tests__/unit/readiness.test.ts +61 -0
- package/skills/octocode-research/src/__tests__/unit/resilience.test.ts +192 -0
- package/skills/octocode-research/src/__tests__/unit/responseFactory.test.ts +172 -0
- package/skills/octocode-research/src/__tests__/unit/responseParser.test.ts +288 -0
- package/skills/octocode-research/src/__tests__/unit/schemas.test.ts +509 -0
- package/skills/octocode-research/src/index.ts +4 -124
- package/skills/octocode-research/src/middleware/queryParser.ts +0 -26
- package/skills/octocode-research/src/routes/lsp.ts +58 -59
- package/skills/octocode-research/src/routes/package.ts +35 -65
- package/skills/octocode-research/src/routes/prompts.ts +3 -3
- package/skills/octocode-research/src/routes/tools.ts +8 -20
- package/skills/octocode-research/src/server-init.ts +30 -237
- package/skills/octocode-research/src/server.ts +50 -23
- package/skills/octocode-research/src/types/errorGuards.ts +9 -80
- package/skills/octocode-research/src/types/guards.ts +0 -28
- package/skills/octocode-research/src/types/mcp.ts +11 -66
- package/skills/octocode-research/src/types/responses.ts +11 -129
- package/skills/octocode-research/src/utils/circuitBreaker.ts +0 -21
- package/skills/octocode-research/src/utils/logger.ts +1 -97
- package/skills/octocode-research/src/utils/resilience.ts +2 -12
- package/skills/octocode-research/src/utils/responseFactory.ts +0 -42
- package/skills/octocode-research/src/utils/responseParser.ts +3 -25
- package/skills/octocode-research/src/utils/retry.ts +0 -63
- package/skills/octocode-research/src/utils/routeFactory.ts +1 -1
- package/skills/octocode-research/src/validation/httpPreprocess.ts +0 -3
- package/skills/octocode-research/src/validation/index.ts +0 -1
- package/skills/octocode-research/src/validation/schemas.ts +0 -63
- package/skills/octocode-research/src/validation/toolCallSchema.ts +3 -3
- package/skills/octocode-research/tsdown.config.ts +4 -0
- package/skills/octocode-research/vitest.config.ts +3 -0
- package/skills/octocode-code-engineer/.plan/VALIDATED_PLAN.md +0 -223
- package/skills/octocode-code-engineer/README.md +0 -178
- package/skills/octocode-code-engineer/SKILL.md +0 -418
- package/skills/octocode-code-engineer/minify-scripts.mjs +0 -32
- package/skills/octocode-code-engineer/references/agent-ast-reading-rfc.md +0 -95
- package/skills/octocode-code-engineer/references/architecture-techniques.md +0 -121
- package/skills/octocode-code-engineer/references/ast-search.md +0 -210
- package/skills/octocode-code-engineer/references/ast-tree-search.md +0 -151
- package/skills/octocode-code-engineer/references/concepts.md +0 -107
- package/skills/octocode-code-engineer/references/finding-categories.md +0 -128
- package/skills/octocode-code-engineer/references/improvement-roadmap.md +0 -304
- package/skills/octocode-code-engineer/references/playbooks.md +0 -204
- package/skills/octocode-code-engineer/references/present-results.md +0 -136
- package/skills/octocode-code-engineer/references/tool-workflows.md +0 -566
- package/skills/octocode-code-engineer/references/validate-investigate.md +0 -225
- package/skills/octocode-code-engineer/scripts/analysis/dependencies.js +0 -1
- package/skills/octocode-code-engineer/scripts/analysis/dependency-summary.js +0 -1
- package/skills/octocode-code-engineer/scripts/analysis/discovery.js +0 -1
- package/skills/octocode-code-engineer/scripts/analysis/graph-analytics.js +0 -1
- package/skills/octocode-code-engineer/scripts/analysis/semantic.js +0 -1
- package/skills/octocode-code-engineer/scripts/ast/helpers.js +0 -1
- package/skills/octocode-code-engineer/scripts/ast/metrics.js +0 -1
- package/skills/octocode-code-engineer/scripts/ast/search.js +0 -2
- package/skills/octocode-code-engineer/scripts/ast/tree-search.js +0 -2
- package/skills/octocode-code-engineer/scripts/ast/tree-sitter.js +0 -1
- package/skills/octocode-code-engineer/scripts/ast/ts-analyzer.js +0 -1
- package/skills/octocode-code-engineer/scripts/collectors/chains.js +0 -1
- package/skills/octocode-code-engineer/scripts/collectors/effects.js +0 -1
- package/skills/octocode-code-engineer/scripts/collectors/input-sources.js +0 -1
- package/skills/octocode-code-engineer/scripts/collectors/performance.js +0 -1
- package/skills/octocode-code-engineer/scripts/collectors/prototype-pollution.js +0 -1
- package/skills/octocode-code-engineer/scripts/collectors/security.js +0 -1
- package/skills/octocode-code-engineer/scripts/collectors/test-profile.js +0 -1
- package/skills/octocode-code-engineer/scripts/common/is-direct-run.js +0 -1
- package/skills/octocode-code-engineer/scripts/common/utils.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/code-quality.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/cohesion.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/coupling.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/cycle.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/dead-code.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/import-style.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/index.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/security.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/semantic.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/shared.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/test-quality.js +0 -1
- package/skills/octocode-code-engineer/scripts/index.js +0 -1
- package/skills/octocode-code-engineer/scripts/pipeline/cache.js +0 -1
- package/skills/octocode-code-engineer/scripts/pipeline/cli.js +0 -1
- package/skills/octocode-code-engineer/scripts/pipeline/main.js +0 -2
- package/skills/octocode-code-engineer/scripts/reporting/analysis.js +0 -1
- package/skills/octocode-code-engineer/scripts/reporting/summary-md.js +0 -1
- package/skills/octocode-code-engineer/scripts/reporting/writer.js +0 -1
- package/skills/octocode-code-engineer/scripts/types/constants.js +0 -1
- package/skills/octocode-code-engineer/scripts/types/index.js +0 -1
- package/skills/octocode-code-engineer/scripts/types/interfaces.js +0 -1
- package/skills/octocode-code-engineer/src/analysis/dependencies.ts +0 -406
- package/skills/octocode-code-engineer/src/index.ts +0 -403
- package/skills/octocode-code-engineer/src/reporting/summary-md.test.ts +0 -421
- package/skills/octocode-code-engineer/src/reporting/summary-md.ts +0 -714
- package/skills/octocode-code-engineer/src/types/interfaces.ts +0 -682
- package/skills/octocode-research/src/types/toolTypes.ts +0 -33
- package/skills/octocode-research/src/utils/logEmoji.ts +0 -103
- /package/skills/{octocode-code-engineer → octocode-engineer}/.octocode/rfc/RFC-code-engineer-weakness-fixes.md +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/architecture.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/ast-helpers.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/ast-search.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/base.css +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/block-navigation.js +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/cache.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/cli.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/clover.xml +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/collect-effects.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/collect-input-sources.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/collect-performance.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/collect-prototype-pollution.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/collect-security.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/collect-test-profile.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/coverage-final.json +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/dependencies.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/dependency-summary.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/discovery.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/favicon.png +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/graph-analytics.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/index.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/index.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/metrics.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/pipeline.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/prettify.css +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/prettify.js +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/report-analysis.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/report-writer.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/security-detectors.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/semantic-detectors.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/semantic.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/sort-arrow-sprite.png +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/sorter.js +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/summary-md.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/test-quality-detectors.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/tree-sitter-analyzer.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/ts-analyzer.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/types.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/utils.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/dependencies.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/dependency-summary.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/dependency-summary.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/graph-analytics.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/graph-analytics.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/semantic.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/semantic.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/helpers.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/helpers.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/metrics.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/metrics.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/tree-search.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/tree-search.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/chains.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/effects.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/effects.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/input-sources.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/input-sources.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/performance.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/performance.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/prototype-pollution.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/prototype-pollution.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/test-profile.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/test-profile.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/common/is-direct-run.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/common/is-direct-run.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/common/utils.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/common/utils.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/cohesion.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/coupling.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/cycle.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/dead-code.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/import-style.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/index.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/security.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/security.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/semantic.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/shared.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/test-quality.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/test-quality.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/pipeline/cache.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/pipeline/cache.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/pipeline/main.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/pipeline.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/reporting/analysis.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/reporting/analysis.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/vitest.config.ts +0 -0
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
# Validate & Investigate
|
|
2
|
-
|
|
3
|
-
## Core Principle: Detectors Signal, You Decide
|
|
4
|
-
|
|
5
|
-
Detectors use cheap structural AST metrics (loop depth, call count, fan-in, statement count) to flag **candidates**. They intentionally avoid hardcoded domain heuristics — no regex patterns, no method-name lists, no keyword matching. This keeps them fast, maintainable, and language-generic.
|
|
6
|
-
|
|
7
|
-
**You are the intelligence layer.** Use your tools to read the actual code, trace relationships, confirm or dismiss hypotheses, and explain your reasoning. A finding with `loops >= 2 && calls >= 5 && maxLoopDepth >= 2` is a structural signal for potential unbounded growth — read the function body to see if there's actually a `.push()` in a loop, or if it's a harmless traversal. A `god-function` flag based on MI < 10 is a signal — read the code to see if it's genuinely doing too many things or just long-but-focused.
|
|
8
|
-
|
|
9
|
-
**Your validation toolkit:**
|
|
10
|
-
- `localGetFileContent(matchString=...)` — read the code at the flagged location
|
|
11
|
-
- `ast/search.js -p 'pattern'` — structural AST proof (zero false positives)
|
|
12
|
-
- `lspFindReferences` / `lspCallHierarchy` — trace usage and call flow
|
|
13
|
-
- `lspGotoDefinition` — jump to definitions across files
|
|
14
|
-
- `localSearchCode` — fast text search for patterns and context
|
|
15
|
-
|
|
16
|
-
**Your decisions:**
|
|
17
|
-
- **Confirmed**: tool evidence supports the finding → present with `file:line` citations
|
|
18
|
-
- **Dismissed**: false positive → explain what you checked and why it doesn't hold
|
|
19
|
-
- **Uncertain**: need more data → say what's missing, lower confidence
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
**Validate before fixing.** For structurally obvious findings (empty-catch, switch-no-default, debugger), a single code read at `file:line` is sufficient. For semantic findings (dead exports, cycles, coupling, security sinks), always confirm with MCP or CLI tools.
|
|
24
|
-
|
|
25
|
-
For confidence tiers and the minimum validation required per tier, see the **Confidence Tiers** table in [SKILL.md](../SKILL.md).
|
|
26
|
-
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
## Investigation Loop
|
|
30
|
-
|
|
31
|
-
Before presenting a conclusion, adapt to the finding:
|
|
32
|
-
|
|
33
|
-
1. **Understand context** — read `summary.md` signals (Graph, AST, Confidence, Recommended Validation), then the finding itself (`file`, `lineStart`, `category`, `reason`, `impact`).
|
|
34
|
-
2. **Check `lspHints` first** — if the finding has `lspHints[]`, run those tool calls directly. They're pre-computed shortcuts to the fastest validation path.
|
|
35
|
-
3. **Read the code** — `localGetFileContent` at the flagged location. Look for the concrete behavior the detector suspected. This is where you apply intelligence — understanding context, intent, and whether the structural signal corresponds to real risk.
|
|
36
|
-
4. **Trace context** — use LSP or `localSearchCode` to understand callers, consumers, data flow.
|
|
37
|
-
5. **Correlate signals** — compare `summary.md`, `architecture.json`, `findings.json`, `file-inventory.json`.
|
|
38
|
-
6. **Decide** — confirmed / dismissed / uncertain, with evidence.
|
|
39
|
-
7. **Present** — what the scan flagged → what you found → your verdict with `file:line` citations.
|
|
40
|
-
|
|
41
|
-
If the scan looks ambiguous:
|
|
42
|
-
- use `--graph --graph-advanced` for SCC clusters, chokepoints, package chatter, and startup-risk hubs
|
|
43
|
-
- use `--flow` for `cfgFlags`, `flowTrace`, and richer evidence on path-sensitive findings
|
|
44
|
-
- if graph and AST signals disagree, say so explicitly and continue investigating rather than flattening them
|
|
45
|
-
|
|
46
|
-
---
|
|
47
|
-
|
|
48
|
-
## Tool Selection Guide
|
|
49
|
-
|
|
50
|
-
Pick the tool that answers the question fastest. When both CLI and MCP are available, prefer MCP for semantic questions (LSP) and CLI for re-scan and structural proof.
|
|
51
|
-
|
|
52
|
-
| Task | CLI option | Octocode MCP option |
|
|
53
|
-
|------|-----------|-------------------|
|
|
54
|
-
| Find all instances of a pattern | `ast/search.js -p 'pattern' --json` | `localSearchCode(pattern)` |
|
|
55
|
-
| Understand project layout | `ast/tree-search.js -k ...` | `localViewStructure(path)` |
|
|
56
|
-
| Find files by name / metadata | — | `localFindFiles(name, path)` |
|
|
57
|
-
| Read a specific code section | — | `localGetFileContent(file, matchString)` |
|
|
58
|
-
| Confirm no references exist | `ast/search.js -p 'import { sym } from $M'` → 0 hits | `lspFindReferences(lineHint)` → 0 refs |
|
|
59
|
-
| Trace call flow | — | `lspCallHierarchy(incoming/outgoing)` |
|
|
60
|
-
| Jump to definition | — | `lspGotoDefinition(lineHint)` |
|
|
61
|
-
| Count across repo | `ast/search.js --json \| jq length` | `localSearchCode(filesOnly=true)` |
|
|
62
|
-
| Re-scan after fix | `scripts/index.js --scope=file.ts` | — |
|
|
63
|
-
|
|
64
|
-
**LSP tips:**
|
|
65
|
-
- `lspFindReferences` for types, variables, all usages.
|
|
66
|
-
- `lspCallHierarchy` for function calls only.
|
|
67
|
-
- `lspCallHierarchy(depth=1)` + chain manually is faster than high depth.
|
|
68
|
-
- `lspGotoDefinition` requires `lineHint` — always run `localSearchCode` first to get it.
|
|
69
|
-
|
|
70
|
-
---
|
|
71
|
-
|
|
72
|
-
## CLI-Only Mode
|
|
73
|
-
|
|
74
|
-
When Octocode MCP is unavailable, use this chain:
|
|
75
|
-
|
|
76
|
-
```
|
|
77
|
-
1. node scripts/index.js --scope=file.ts --features=<category> → targeted rescan
|
|
78
|
-
2. node scripts/ast/search.js -p 'pattern' --root <dir> → structural search
|
|
79
|
-
3. node scripts/ast/search.js --preset <name> --root <dir> → use pre-built patterns
|
|
80
|
-
4. Read file at finding line range → manual inspection
|
|
81
|
-
5. Fix → rescan with same --scope → verify count drops
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Mark confidence explicitly: `high` = structural (empty-catch, switch-no-default), `medium` = semantic (dead-export, coupling), `low` = behavioral (security, data-flow).
|
|
85
|
-
|
|
86
|
-
Useful `ast/search.js` presets: `empty-catch`, `any-type`, `type-assertion`, `non-null-assertion`, `console-log`, `console-any`, `debugger`, `todo-fixme`, `switch-no-default`, `nested-ternary`.
|
|
87
|
-
|
|
88
|
-
---
|
|
89
|
-
|
|
90
|
-
## `lspHints` Validation
|
|
91
|
-
|
|
92
|
-
Most findings include `lspHints[]` with pre-computed validation instructions:
|
|
93
|
-
|
|
94
|
-
```json
|
|
95
|
-
{
|
|
96
|
-
"lspHints": [{
|
|
97
|
-
"tool": "lspFindReferences",
|
|
98
|
-
"symbolName": "deadExport",
|
|
99
|
-
"lineHint": 15,
|
|
100
|
-
"file": "packages/foo/src/utils.ts",
|
|
101
|
-
"expectedResult": "zero references confirms dead export"
|
|
102
|
-
}]
|
|
103
|
-
}
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
Use directly: `lspFindReferences(file, lineHint)` → compare with `expectedResult`. No `localSearchCode` step needed when the hint provides the exact position.
|
|
107
|
-
|
|
108
|
-
---
|
|
109
|
-
|
|
110
|
-
## `impact` Field
|
|
111
|
-
|
|
112
|
-
Most findings include an `impact` string explaining the real-world consequence:
|
|
113
|
-
|
|
114
|
-
```json
|
|
115
|
-
{ "impact": "Sequential awaits multiply latency by N iterations — parallelizing reduces total time to max(single-latency)." }
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
Use `impact` to prioritize which findings to address first, explain to stakeholders why a fix matters, and decide between fix-now vs accept-risk.
|
|
119
|
-
|
|
120
|
-
---
|
|
121
|
-
|
|
122
|
-
## Security Analysis
|
|
123
|
-
|
|
124
|
-
Security analysis combines **deterministic detection** (scanner AST patterns) with **agentic investigation** (agent uses tools to understand the project, trace flows, and assess risk). The scanner flags sinks; the agent maps the attack surface.
|
|
125
|
-
|
|
126
|
-
### Phase 1: Understand the Project Security Context
|
|
127
|
-
|
|
128
|
-
Before validating individual findings, understand what the project is and where sensitive operations live. Use tools to map the security-relevant landscape:
|
|
129
|
-
|
|
130
|
-
| Question | How to find it |
|
|
131
|
-
|----------|---------------|
|
|
132
|
-
| What does this project do? | `localViewStructure(depth=2)` → README, package.json, entry points |
|
|
133
|
-
| Where are HTTP/API entry points? | `localSearchCode("app.get\|app.post\|router\|createServer\|handler")` |
|
|
134
|
-
| Where is authentication? | `localSearchCode("auth\|session\|jwt\|token\|login\|passport\|cookie")` |
|
|
135
|
-
| Where is user data handled? | `localSearchCode("password\|email\|user\|profile\|credential\|ssn")` |
|
|
136
|
-
| Where are payments/billing? | `localSearchCode("payment\|billing\|charge\|stripe\|invoice\|price")` |
|
|
137
|
-
| Where is the database layer? | `localSearchCode("query\|prisma\|mongoose\|knex\|sequelize\|pool\|.execute")` |
|
|
138
|
-
| Where are external services? | `localSearchCode("fetch\|axios\|http.request\|grpc\|sdk\|client")` |
|
|
139
|
-
| Where are file system operations? | `localSearchCode("readFile\|writeFile\|createWriteStream\|unlink\|mkdir")` |
|
|
140
|
-
| Where are processes/commands? | `localSearchCode("exec\|spawn\|fork\|child_process")` |
|
|
141
|
-
| Where is logging? | `localSearchCode("console.log\|logger\|winston\|pino\|bunyan")` |
|
|
142
|
-
| Where are exports/exposure points? | `localSearchCode("export\|module.exports\|expose\|public")` in API/route files |
|
|
143
|
-
|
|
144
|
-
### Phase 2: Map Sensitive Flows
|
|
145
|
-
|
|
146
|
-
Use the project context to identify **critical data paths** the scanner can't see:
|
|
147
|
-
|
|
148
|
-
**Sensitive data flows** — trace with `lspCallHierarchy`:
|
|
149
|
-
- User input → validation → storage (passwords, PII, credentials)
|
|
150
|
-
- Database → serialization → API response (data leakage)
|
|
151
|
-
- Secrets/config → usage sites (env vars, key management)
|
|
152
|
-
- User data → logging/monitoring (accidental exposure)
|
|
153
|
-
- Internal data → external services (third-party leakage)
|
|
154
|
-
|
|
155
|
-
**Critical operation flows**:
|
|
156
|
-
- Payment/billing → charge → confirmation (financial integrity)
|
|
157
|
-
- Auth → session → permission check (access control)
|
|
158
|
-
- File upload → storage → serving (path traversal, content injection)
|
|
159
|
-
- User input → command/query construction (injection)
|
|
160
|
-
|
|
161
|
-
**How to trace a flow**:
|
|
162
|
-
1. `localSearchCode` → find the entry point (e.g., route handler with user input)
|
|
163
|
-
2. `lspCallHierarchy(outgoing)` → what does it call? Follow the chain
|
|
164
|
-
3. At each hop: does data pass through validation? Does it reach a sink?
|
|
165
|
-
4. `localGetFileContent` → read the actual code at each node to confirm
|
|
166
|
-
|
|
167
|
-
### Phase 3: Validate Scanner Findings
|
|
168
|
-
|
|
169
|
-
Security findings are **context-sensitive** — always trace data flow before acting.
|
|
170
|
-
|
|
171
|
-
**Taint tracing workflow** for each finding:
|
|
172
|
-
|
|
173
|
-
1. **Find the sink**: `localSearchCode` or `lspGotoDefinition`
|
|
174
|
-
2. **Trace callers**: `lspCallHierarchy(incoming)` — who calls the sink?
|
|
175
|
-
3. **Assess source**: HIGH = user input params (`req`, `input`, `body`, `args`), MEDIUM = passed through, LOW = internal/hardcoded
|
|
176
|
-
4. **Check sanitizers**: `localGetFileContent` — read code between source and sink
|
|
177
|
-
5. **Verdict**: confirmed / dismissed / needs-review
|
|
178
|
-
|
|
179
|
-
| Category | Source signal | Sink signal | Sanitizer check |
|
|
180
|
-
|----------|-------------|-------------|-----------------|
|
|
181
|
-
| `path-traversal-risk` | param: `path`, `file`, `dir` | `fs.readFile`, `path.resolve` | `normalize` + `startsWith` + `realpathSync` |
|
|
182
|
-
| `command-injection-risk` | param: `cmd`, `command`, `args` | `exec`, `execSync`, `spawn` | allowlist, `spawn` with array args |
|
|
183
|
-
| `prototype-pollution-risk` | computed key variable | `obj[key] = val` | `__proto__` guard, `Object.create(null)` |
|
|
184
|
-
| `hardcoded-secret` | string literal | auth / network calls | env var substitution |
|
|
185
|
-
| `unvalidated-input-sink` | `req`/`body`/`input` | eval / SQL / exec / fs-write | schema validation (zod, joi) |
|
|
186
|
-
| `sql-injection-risk` | template interpolation | `.query()`, `.execute()` | parameterized queries |
|
|
187
|
-
| `sensitive-data-logging` | password/token/secret params | `console.*` call | field-level redaction |
|
|
188
|
-
|
|
189
|
-
### Phase 4: Check Exposure Points
|
|
190
|
-
|
|
191
|
-
Use tools to verify what the system exposes:
|
|
192
|
-
|
|
193
|
-
| Exposure vector | Search pattern | Validate with |
|
|
194
|
-
|----------------|---------------|---------------|
|
|
195
|
-
| API responses | `localSearchCode("res.json\|res.send\|return.*response")` | Does response include user PII, tokens, internal IDs? |
|
|
196
|
-
| Error messages | `localSearchCode("error.message\|stack\|err")` in response handlers | Do errors leak stack traces, file paths, SQL? |
|
|
197
|
-
| Logs | `ast/search.js --preset console-any` | Do logs contain passwords, tokens, user data? |
|
|
198
|
-
| Environment | `localSearchCode("process.env")` | Are secrets loaded safely? Any defaults with real values? |
|
|
199
|
-
| Static assets | `localViewStructure` on public/static dirs | Sensitive files accessible? `.env`, configs, backups? |
|
|
200
|
-
| Third-party data | `localSearchCode` for SDK/API client usage | What user data is sent to external services? |
|
|
201
|
-
|
|
202
|
-
### False Positive Dismissal
|
|
203
|
-
|
|
204
|
-
- **`prototype-pollution-risk`**: key from `Object.keys()` on internal object, or target is `Object.create(null)` / `Map` / `Set` → dismiss
|
|
205
|
-
- **`hardcoded-secret`**: regex definition, UUID, placeholder (`YOUR_*`, `<key>`), test-only, error-message string → dismiss
|
|
206
|
-
- **`debug-log-leakage`**: inside test file, or gated by `LOG_LEVEL`/`DEBUG` env check → dismiss
|
|
207
|
-
- **`sensitive-data-logging`**: already redacted before logging, or test file → dismiss
|
|
208
|
-
- **`path-traversal-risk`**: normalize + prefix check + realpath → dismiss
|
|
209
|
-
- **`command-injection-risk`**: spawn with array args, no `shell: true` → downgrade
|
|
210
|
-
|
|
211
|
-
### Agentic Security: MCP/Tool Code
|
|
212
|
-
|
|
213
|
-
For agentic/MCP code, trace these critical paths:
|
|
214
|
-
|
|
215
|
-
1. **User prompt → tool arguments → file system**: path validation between arg parsing and fs calls
|
|
216
|
-
2. **User prompt → tool arguments → shell commands**: command allowlists between parsing and exec/spawn
|
|
217
|
-
3. **User prompt → tool arguments → network requests**: URL validation before fetch/http
|
|
218
|
-
4. **Tool argument schemas**: types, ranges, enums constraining inputs before sinks
|
|
219
|
-
|
|
220
|
-
### Recommended External Tools
|
|
221
|
-
|
|
222
|
-
For deep analysis beyond pattern detection:
|
|
223
|
-
- **Semgrep** — taint tracking, custom rules, cross-function data flow
|
|
224
|
-
- **CodeQL** — full semantic analysis, vulnerability databases
|
|
225
|
-
- **Snyk / npm audit** — dependency vulnerabilities (SCA)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import path from"node:path";import*as ts from"typescript";import{addToMapSet,isRelativeImport,isTestFile,normalizeDependencyValue,resolveImportTarget,toRepoPath}from"../common/utils.js";export function collectModuleDependencies(e,t,n){const i=path.dirname(t),o=new Set,r=new Set,s=new Set,a=[],l=[],d=[],p=e=>!!ts.canHaveModifiers(e)&&Boolean(ts.getModifiers(e)?.some(e=>e.kind===ts.SyntaxKind.ExportKeyword)),c=e=>{a.some(t=>t.name===e.name&&t.kind===e.kind)||a.push(e)},u=e=>{if(!e||"string"!=typeof e)return null;if(!isRelativeImport(e))return r.add(e),null;const t=resolveImportTarget(i,e);if(!t)return s.add(e),null;if(!t.startsWith(n))return r.add(e),null;const a=normalizeDependencyValue(path.relative(n,t));return o.add(a),a},m=t=>{const n=e.getLineAndCharacterOfPosition(t.getStart(e)),i=e.getLineAndCharacterOfPosition(t.getEnd());return{lineStart:n.line+1,lineEnd:i.line+1}},g=t=>{if(ts.isImportDeclaration(t)&&t.moduleSpecifier&&ts.isStringLiteral(t.moduleSpecifier)){const e=t.moduleSpecifier.text,n=u(e)??void 0,i=m(t),o=t.importClause;if(o&&(o.name&&l.push({sourceModule:e,resolvedModule:n,importedName:"default",localName:o.name.text,isTypeOnly:o.isTypeOnly,...i}),o.namedBindings))if(ts.isNamespaceImport(o.namedBindings))l.push({sourceModule:e,resolvedModule:n,importedName:"*",localName:o.namedBindings.name.text,isTypeOnly:o.isTypeOnly,...i});else for(const t of o.namedBindings.elements)l.push({sourceModule:e,resolvedModule:n,importedName:t.propertyName?.text??t.name.text,localName:t.name.text,isTypeOnly:o.isTypeOnly||t.isTypeOnly,...i})}if(ts.isExportDeclaration(t)&&t.moduleSpecifier&&ts.isStringLiteral(t.moduleSpecifier)){const n=t.moduleSpecifier.text,i=u(n)??void 0;if(t.exportClause&&ts.isNamedExports(t.exportClause))for(const o of t.exportClause.elements)d.push({sourceModule:n,resolvedModule:i,exportedAs:o.name.text,importedName:o.propertyName?.text??o.name.text,isStar:!1,isTypeOnly:t.isTypeOnly||o.isTypeOnly,lineStart:e.getLineAndCharacterOfPosition(o.getStart(e)).line+1,lineEnd:e.getLineAndCharacterOfPosition(o.getEnd()).line+1});else d.push({sourceModule:n,resolvedModule:i,exportedAs:"*",importedName:"*",isStar:!0,isTypeOnly:t.isTypeOnly,lineStart:e.getLineAndCharacterOfPosition(t.getStart(e)).line+1,lineEnd:e.getLineAndCharacterOfPosition(t.getEnd()).line+1})}if(ts.isExportAssignment(t)&&c({name:"default",kind:"value",isDefault:!0,lineStart:e.getLineAndCharacterOfPosition(t.getStart(e)).line+1,lineEnd:e.getLineAndCharacterOfPosition(t.getEnd()).line+1}),ts.isFunctionDeclaration(t)&&p(t)&&c({name:t.name?.text||"default",kind:"value",isDefault:!t.name,lineStart:e.getLineAndCharacterOfPosition(t.getStart(e)).line+1,lineEnd:e.getLineAndCharacterOfPosition(t.getEnd()).line+1}),ts.isClassDeclaration(t)&&p(t)&&c({name:t.name?.text||"default",kind:"value",isDefault:!t.name,lineStart:e.getLineAndCharacterOfPosition(t.getStart(e)).line+1,lineEnd:e.getLineAndCharacterOfPosition(t.getEnd()).line+1}),ts.isEnumDeclaration(t)&&p(t)&&c({name:t.name.text,kind:"value",lineStart:e.getLineAndCharacterOfPosition(t.getStart(e)).line+1,lineEnd:e.getLineAndCharacterOfPosition(t.getEnd()).line+1}),(ts.isTypeAliasDeclaration(t)||ts.isInterfaceDeclaration(t))&&p(t)&&c({name:t.name.text,kind:"type",lineStart:e.getLineAndCharacterOfPosition(t.getStart(e)).line+1,lineEnd:e.getLineAndCharacterOfPosition(t.getEnd()).line+1}),ts.isVariableStatement(t)&&p(t))for(const n of t.declarationList.declarations)ts.isIdentifier(n.name)&&c({name:n.name.text,kind:"value",lineStart:e.getLineAndCharacterOfPosition(n.getStart(e)).line+1,lineEnd:e.getLineAndCharacterOfPosition(n.getEnd()).line+1});if(ts.isExportDeclaration(t)&&!t.moduleSpecifier&&t.exportClause&&ts.isNamedExports(t.exportClause))for(const n of t.exportClause.elements)c({name:n.name.text,kind:n.isTypeOnly?"type":"unknown",lineStart:e.getLineAndCharacterOfPosition(n.getStart(e)).line+1,lineEnd:e.getLineAndCharacterOfPosition(n.getEnd()).line+1});if(ts.isCallExpression(t)&&ts.isIdentifier(t.expression)&&"require"===t.expression.text&&1===t.arguments.length&&ts.isStringLiteral(t.arguments[0])){const e=t.arguments[0].text,n=u(e)??void 0;l.push({sourceModule:e,resolvedModule:n,importedName:"*",localName:"require",isTypeOnly:!1,...m(t)})}ts.forEachChild(t,g)};return g(e),{internalDependencies:[...o].sort(),externalDependencies:[...r].sort(),unresolvedDependencies:[...s].sort(),declaredExports:a,importedSymbols:l,reExports:d}}export function trackDependencyEdge(e,t,n,i){addToMapSet(e.outgoing,t,n),addToMapSet(e.incoming,n,t),addToMapSet(i?e.incomingFromTests:e.incomingFromProduction,n,t)}export function collectDependencyProfile(e,t,n,i,o){const r=toRepoPath(t,i.root);o.files.add(r);const s=collectModuleDependencies(e,t,i.root),a=isTestFile(t);for(const e of s.internalDependencies){trackDependencyEdge(o,r,normalizeDependencyValue(e),a)}return s.externalDependencies.length>0&&o.externalCounts.set(r,new Set(s.externalDependencies)),s.unresolvedDependencies.length>0&&o.unresolvedCounts.set(r,new Set(s.unresolvedDependencies)),o.declaredExportsByFile.set(r,s.declaredExports),o.importedSymbolsByFile.set(r,s.importedSymbols),o.reExportsByFile.set(r,s.reExports),{...s,package:n,file:r}}export function dependencyProfileToRecord(e,t){const n=t.outgoing.get(e)||new Set,i=t.incoming.get(e)||new Set,o=t.incomingFromProduction.get(e)||new Set,r=t.incomingFromTests.get(e)||new Set,s=t.externalCounts.get(e)||new Set,a=t.unresolvedCounts.get(e)||new Set;return{file:e,outboundCount:n.size,inboundCount:i.size,inboundFromProduction:o.size,inboundFromTests:r.size,externalDependencyCount:s.size,unresolvedDependencyCount:a.size}}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{dependencyProfileToRecord}from"./dependencies.js";import{isTestFile}from"../common/utils.js";export function buildDependencySummary(e,o,t){const n=[...e.files].sort();let s=0,c=0;const r=[],i=[];for(const t of n){s+=e.outgoing.get(t)?.size||0;const n=dependencyProfileToRecord(t,e),l=o.get(t)||{score:1};r.push({file:t,count:n.outboundCount,score:l.score}),i.push({file:t,count:n.inboundCount,score:l.score}),c+=n.unresolvedDependencyCount}const l=n.filter(o=>0===(e.incoming.get(o)||new Set).size),u=n.filter(o=>0===(e.outgoing.get(o)||new Set).size),d=n.filter(e=>!isTestFile(e)).filter(o=>{const t=e.incomingFromProduction.get(o),n=e.incomingFromTests.get(o);return(!t||0===t.size)&&n&&n.size>0}).map(o=>({...dependencyProfileToRecord(o,e)})).sort((e,o)=>e.file.localeCompare(o.file)),a=n.map(t=>({...dependencyProfileToRecord(t,e),...o.get(t)||{}})).filter(e=>(e.score||0)>12||e.outboundCount>5||e.inboundCount>8).sort((e,o)=>(o.score||0)+.8*o.inboundCount+.4*o.outboundCount-((e.score||0)+.8*e.inboundCount+.4*e.outboundCount)).slice(0,150).map(e=>({...e,score:Math.round(e.score||0),riskBand:(e.score||0)>=60?"high":(e.score||0)>=30?"medium":"low"})),p=computeDependencyCycles(e),h=computeDependencyCriticalPaths(e,o,t);return{totalModules:n.length,totalEdges:s,unresolvedEdgeCount:c,externalDependencyFiles:[...e.externalCounts.keys()].length,rootsCount:l.length,leavesCount:u.length,roots:l.slice(0,20),leaves:u.slice(0,20),criticalModules:a.slice(0,20),testOnlyModules:d.slice(0,50),unresolvedSample:c>0?[...e.unresolvedCounts.keys()].slice(0,40):[],outgoingTop:r.sort((e,o)=>o.count-e.count).slice(0,20),inboundTop:i.sort((e,o)=>o.count-e.count).slice(0,20),cycles:p.slice(0,20),criticalPaths:h.slice(0,Math.max(1,t.deepLinkTopN))}}export function computeDependencyCycles(e){const o=[],t=new Set,n=new Set,s=[],c=new Set,r=e=>{const o=[...e];let t=o.slice();for(let e=1;e<o.length;e++){const n=[...o.slice(e),...o.slice(0,e)];n.join(" => ")<t.join(" => ")&&(t=n)}return t.join(" => ")},i=l=>{if(t.has(l))return;if(n.has(l))return;n.add(l),s.push(l);const u=e.outgoing.get(l)||new Set;for(const t of u){const n=s.indexOf(t);if(-1!==n){const e=[...s.slice(n),t],i=r(e);c.has(i)||(c.add(i),o.push({path:e,nodeCount:e.length-1}));continue}e.files.has(t)&&i(t)}s.pop(),n.delete(l),t.add(l)};for(const o of e.files)i(o);return o.sort((e,o)=>o.nodeCount-e.nodeCount)}export function computeDependencyCriticalPaths(e,o,t){const n=new Map,s=new Set,c=e=>{const t=o.get(e);return t?t.score:1},r=o=>{if(n.has(o))return n.get(o);if(s.has(o))return{path:[o],score:.5*c(o),containsCycle:!0};s.add(o);const t=e.outgoing.get(o)||new Set;let i={path:[o],score:c(o),containsCycle:!1};for(const n of t){if(!e.files.has(n))continue;const t=r(n),s=c(o)+t.score;(s>i.score||s===i.score&&t.path.length>i.path.length)&&(i={path:[o,...t.path],score:s,containsCycle:t.containsCycle})}return s.delete(o),n.set(o,i),i},i=[];for(const o of e.files){const e=r(o);i.push({start:o,path:e.path,score:Math.round(e.score),length:e.path.length,containsCycle:e.containsCycle})}return i.filter(e=>e.length>1).sort((e,o)=>{const t=o.score-e.score;return 0!==t?t:o.length-e.length}).slice(0,Math.max(1,t.deepLinkTopN))}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import fs from"node:fs";import path from"node:path";import{isTestFile}from"../common/utils.js";import{ALLOWED_EXTS}from"../types/index.js";export function collectFiles(e,n){const t=[],i=e=>{const s=fs.readdirSync(e,{withFileTypes:!0});s.sort((e,n)=>e.name.localeCompare(n.name));for(const o of s){if(n.ignoreDirs.has(o.name))continue;if(o.isSymbolicLink())continue;const s=path.join(e,o.name);if(o.isDirectory()){i(s);continue}if(!o.isFile())continue;if(o.name.endsWith(".d.ts"))continue;const r=path.extname(o.name);ALLOWED_EXTS.has(r)&&(!n.includeTests&&isTestFile(s)||t.push(s))}};return i(e),t}export function safeRead(e){try{return fs.readFileSync(e,"utf8")}catch{return null}}export function listWorkspacePackages(e,n){if(!fs.existsSync(n)||!fs.statSync(n).isDirectory())return[];const t=fs.readdirSync(n,{withFileTypes:!0}).filter(e=>e.isDirectory()).sort((e,n)=>e.name.localeCompare(n.name)),i=[];for(const e of t){const t=path.join(n,e.name),s=path.join(t,"package.json");if(fs.existsSync(s))try{const n=JSON.parse(fs.readFileSync(s,"utf8"));"string"==typeof n.name&&i.push({name:n.name,dir:t,folder:e.name})}catch{}}return i}export function fileSummaryWithFindings(e,n){return e.map(e=>({...e,issueIds:n.get(e.file)||[]}))}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export function packageKeyForFile(e){const t=e.replace(/\\/g,"/").replace(/^\.?\//,""),n=t.match(/^packages\/([^/]+)/);if(n)return`packages/${n[1]}`;const[o]=t.split("/");return o||"<root>"}export function computeSccClusters(e){const t=[...e.files].sort(),n=new Map,o=new Map,s=[],i=new Set;let r=0;const a=[],c=t=>{n.set(t,r),o.set(t,r),r+=1,s.push(t),i.add(t);const l=e.outgoing.get(t)||new Set;for(const s of l)e.files.has(s)&&(n.has(s)?i.has(s)&&o.set(t,Math.min(o.get(t),n.get(s))):(c(s),o.set(t,Math.min(o.get(t),o.get(s)))));if(o.get(t)!==n.get(t))return;const u=[];for(;s.length>0;){const e=s.pop();if(i.delete(e),u.push(e),e===t)break}const d=(e.outgoing.get(t)||new Set).has(t);if(u.length<=1&&!d)return;const g=new Set(u);let f=0,h=0,p=0;for(const t of u){for(const n of e.outgoing.get(t)||new Set)e.files.has(n)&&(g.has(n)?f+=1:p+=1);for(const n of e.incoming.get(t)||new Set)g.has(n)||(h+=1)}const m=[...u].sort((t,n)=>{const o=2*(e.incoming.get(t)||new Set).size+(e.outgoing.get(t)||new Set).size;return 2*(e.incoming.get(n)||new Set).size+(e.outgoing.get(n)||new Set).size-o}).slice(0,3);a.push({id:`scc-${a.length+1}`,files:u.sort(),nodeCount:u.length,edgeCount:f,entryEdges:h,exitEdges:p,hubFiles:m})};for(const e of t)n.has(e)||c(e);return a.sort((e,t)=>t.nodeCount-e.nodeCount||t.edgeCount-e.edgeCount)}export function computePackageGraphSummary(e){const t=new Map,n=new Map;for(const n of e.files){const e=packageKeyForFile(n);t.has(e)||t.set(e,{package:e,inbound:0,outbound:0,internalFiles:0}),t.get(e).internalFiles+=1}for(const[o,s]of e.outgoing.entries()){const i=packageKeyForFile(o);for(const o of s){if(!e.files.has(o))continue;const s=packageKeyForFile(o);if(i===s)continue;const r=`${i}=>${s}`;n.set(r,(n.get(r)||0)+1),t.get(i).outbound+=1,t.get(s).inbound+=1}}const o=[...n.entries()].map(([e,t])=>{const[n,o]=e.split("=>");return{from:n,to:o,edges:t}}).sort((e,t)=>t.edges-e.edges).slice(0,20);return{packageCount:t.size,edgeCount:[...n.values()].reduce((e,t)=>e+t,0),packages:[...t.values()].sort((e,t)=>t.inbound+t.outbound-(e.inbound+e.outbound)),hotspots:o}}function computeArticulationAndBridges(e){const t=[...e.files].sort(),n=new Map;for(const e of t)n.set(e,new Set);for(const[t,o]of e.outgoing.entries())for(const s of o)e.files.has(s)&&(n.get(t).add(s),n.get(s).add(t));const o=new Set,s=new Map,i=new Map,r=new Map,a=new Set,c=[];let l=0;const u=e=>{o.add(e),s.set(e,l),i.set(e,l),l+=1;let t=0;for(const l of n.get(e)||new Set)o.has(l)?l!==r.get(e)&&i.set(e,Math.min(i.get(e),s.get(l))):(t+=1,r.set(l,e),u(l),i.set(e,Math.min(i.get(e),i.get(l))),null==r.get(e)&&t>1&&a.add(e),null!=r.get(e)&&i.get(l)>=s.get(e)&&a.add(e),i.get(l)>s.get(e)&&c.push(e<l?{from:e,to:l}:{from:l,to:e}))};for(const e of t)o.has(e)||(r.set(e,null),u(e));return{articulationPoints:a,bridgeEdges:c}}export function computeChokepoints(e,t,n,o){const{articulationPoints:s,bridgeEdges:i}=computeArticulationAndBridges(e),r=new Set;for(const e of t.criticalPaths||[])for(const t of e.path)r.add(t);const a=new Map;for(const e of o)for(const t of e.files)a.set(t,(a.get(t)||0)+1);const c=new Map;for(const e of i)c.set(e.from,(c.get(e.from)||0)+1),c.set(e.to,(c.get(e.to)||0)+1);return[...e.files].map(t=>{const o=(e.incoming.get(t)||new Set).size,i=(e.outgoing.get(t)||new Set).size,l=s.has(t),u=c.get(t)||0,d=a.get(t)||0,g=r.has(t),f=n.get(t)?.score||0,h=Math.round(3*o+1.2*i+f/10+(l?12:0)+4*u+6*d+(g?8:0)),p=[];return o>=8&&p.push(`high fan-in (${o})`),i>=6&&p.push(`high fan-out (${i})`),l&&p.push("articulation point"),u>0&&p.push(`${u} bridge edge(s)`),d>0&&p.push(`in ${d} cycle cluster(s)`),g&&p.push("on critical path"),f>=20&&p.push(`high complexity risk (${f})`),{file:t,score:h,reasons:p,fanIn:o,fanOut:i,articulation:l,bridgeCount:u,cycleClusterCount:d,onCriticalPath:g}}).filter(e=>e.score>0&&e.reasons.length>0).sort((e,t)=>t.score-e.score).slice(0,40)}export function computeGraphAnalytics(e,t,n){const o=computeSccClusters(e),{articulationPoints:s,bridgeEdges:i}=computeArticulationAndBridges(e);return{sccClusters:o,chokepoints:computeChokepoints(e,t,n,o),packageGraphSummary:computePackageGraphSummary(e),articulationPoints:[...s].sort(),bridgeEdges:i}}function findImportLine(e,t,n){const o=e.importedSymbolsByFile.get(t)||[];for(const e of o)if(!n||e.resolvedModule===n)return{lineStart:e.lineStart||1,lineEnd:e.lineEnd||e.lineStart||1};return{lineStart:1,lineEnd:1}}export function buildAdvancedGraphFindings(e,t,n){const o=[];for(const n of e.sccClusters.slice(0,6)){if(n.nodeCount<3)continue;const e=n.hubFiles[0]||n.files[0],s=findImportLine(t,e);o.push({severity:n.nodeCount>=5?"high":"medium",category:"cycle-cluster",file:e,lineStart:s.lineStart,lineEnd:s.lineEnd,title:`Cycle cluster detected (${n.nodeCount} files)`,reason:`Strongly connected cluster ${n.id} has ${n.nodeCount} files, ${n.entryEdges} entry edge(s), and ${n.exitEdges} exit edge(s).`,files:n.files,suggestedFix:{strategy:"Break the cluster at one of the hub files and move shared contracts lower.",steps:["Inspect the hub files first.","Extract shared interfaces or types from the cluster.","Remove at least one high-traffic edge to split the SCC."]},impact:"Large cycle clusters spread change risk across multiple files and hide the true architectural boundary.",tags:["architecture","cycle","graph","change-risk"],ruleId:"architecture.cycle-cluster",confidence:"high",evidence:{clusterId:n.id,nodeCount:n.nodeCount,entryEdges:n.entryEdges,exitEdges:n.exitEdges,hubFiles:n.hubFiles}})}for(const n of e.chokepoints.slice(0,8)){if(n.fanIn<3||n.fanOut<3||n.score<18)continue;const e=findImportLine(t,n.file);o.push({severity:n.articulation||n.score>=30?"high":"medium",category:"broker-module",file:n.file,lineStart:e.lineStart,lineEnd:e.lineEnd,title:`Broker module chokepoint: ${n.file}`,reason:`Module concentrates dependency traffic (${n.reasons.join(", ")}).`,files:[n.file],suggestedFix:{strategy:"Split orchestration responsibilities and reduce fan-in/fan-out through narrower seams.",steps:["Identify which consumers rely on this file for unrelated concerns.","Extract narrower APIs or introduce an internal facade.","Move side-effectful or persistence-specific logic into dedicated modules."]},impact:"Broker modules silently become architecture bottlenecks — a small change cascades broadly.",tags:["architecture","graph","chokepoint","coupling"],ruleId:"architecture.broker-module",confidence:n.articulation?"high":"medium",evidence:{score:n.score,reasons:n.reasons,fanIn:n.fanIn,fanOut:n.fanOut}})}for(const n of e.chokepoints.filter(e=>e.articulation).slice(0,8)){const e=findImportLine(t,n.file);o.push({severity:n.bridgeCount>=2?"high":"medium",category:"bridge-module",file:n.file,lineStart:e.lineStart,lineEnd:e.lineEnd,title:`Bridge module detected: ${n.file}`,reason:`Module acts as a graph articulation point with ${n.bridgeCount} bridge edge(s).`,files:[n.file],suggestedFix:{strategy:"Reduce the amount of architecture that depends on this single bridge module.",steps:["Split unrelated responsibilities out of the bridge module.","Add lower-level contracts so adjacent subsystems do not all route through one file.","Prefer explicit package boundaries over central catch-all utilities."]},impact:"Bridge modules are structurally brittle — they become the single point where subsystem changes collide.",tags:["architecture","graph","bridge","fragility"],ruleId:"architecture.bridge-module",confidence:"high",evidence:{score:n.score,bridgeCount:n.bridgeCount,reasons:n.reasons}})}for(const t of e.packageGraphSummary.hotspots.slice(0,5))t.edges<4||o.push({severity:t.edges>=8?"high":"medium",category:"package-boundary-chatter",file:t.from,lineStart:1,lineEnd:1,title:`Heavy package chatter: ${t.from} -> ${t.to}`,reason:`Detected ${t.edges} cross-package dependency edge(s) between these package groups.`,files:[t.from,t.to],suggestedFix:{strategy:"Reduce cross-package chatter by consolidating APIs or introducing a narrower shared contract.",steps:["Map the symbols crossing this boundary most often.","Promote a smaller public API surface between the packages.","Move implementation detail imports behind a dedicated package boundary."]},impact:"High package chatter is a sign of architectural erosion — packages stop behaving like isolated subsystems.",tags:["architecture","packages","boundary","graph"],ruleId:"architecture.package-boundary-chatter",confidence:"medium",evidence:t});const s=new Map(e.chokepoints.map(e=>[e.file,e]));for(const e of n){const t=e.topLevelEffects||[];if(0===t.length)continue;const n=s.get(e.file);if(!n||n.fanIn<8||n.score<18)continue;const i=t[0];o.push({severity:n.fanIn>=20?"high":"medium",category:"startup-risk-hub",file:e.file,lineStart:i.lineStart,lineEnd:i.lineEnd,title:`Startup risk hub: ${e.file}`,reason:`Module performs ${t.length} import-time effect(s) and also behaves as a chokepoint (${n.reasons.join(", ")}).`,files:[e.file],suggestedFix:{strategy:"Move import-time work behind explicit initialization and reduce inbound dependency pressure.",steps:["Extract side effects into init() or lazy code paths.","Avoid importing this module from broad utility or entrypoint chains.","Keep only declarations and light configuration at module scope."]},impact:"Import-time side effects in a high fan-in hub create startup latency, hidden ordering bugs, and broad runtime blast radius.",tags:["architecture","startup","side-effects","graph"],ruleId:"architecture.startup-risk-hub",confidence:"high",evidence:{fanIn:n.fanIn,chokepointScore:n.score,topLevelEffects:t.map(e=>e.kind)}})}return o}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import fs from"node:fs";import path from"node:path";import*as ts from"typescript";import{isTestFile}from"../common/utils.js";function findTsConfig(e){const t=ts.findConfigFile(e,ts.sys.fileExists,"tsconfig.json");if(!t)return null;const n=ts.readConfigFile(t,ts.sys.readFile);return n.error?null:ts.parseJsonConfigFileContent(n.config,ts.sys,path.dirname(t))}export function createSemanticContext(e,t){const n=findTsConfig(t),r=n?.options??{target:ts.ScriptTarget.ES2022,module:ts.ModuleKind.ESNext,moduleResolution:ts.ModuleResolutionKind.Node10,strict:!0,esModuleInterop:!0,skipLibCheck:!0,allowJs:!0},i=new Set(e),o=new Map;for(const t of e)try{o.set(t,fs.readFileSync(t,"utf8"))}catch{}const s={getScriptFileNames:()=>[...i],getScriptVersion:()=>"1",getScriptSnapshot:e=>{const t=o.get(e);if(null!=t)return ts.ScriptSnapshot.fromString(t);try{const t=fs.readFileSync(e,"utf8");return ts.ScriptSnapshot.fromString(t)}catch{return}},getCurrentDirectory:()=>t,getCompilationSettings:()=>r,getDefaultLibFileName:ts.getDefaultLibFilePath,fileExists:ts.sys.fileExists,readFile:ts.sys.readFile,readDirectory:ts.sys.readDirectory,directoryExists:ts.sys.directoryExists,getDirectories:ts.sys.getDirectories},a=ts.createLanguageService(s,ts.createDocumentRegistry()),c=a.getProgram();return{service:a,checker:c.getTypeChecker(),program:c,root:t}}function getExportReferenceInfo(e,t,n,r,i){const o=e.program.getSourceFile(t);if(!o)return{count:-1,uniqueFiles:0};const s=findSymbolAtLine(o,n,r);if(!s)return{count:-1,uniqueFiles:0};const a=e.service.findReferences(t,s.getStart(o));if(!a)return{count:0,uniqueFiles:0};let c=0;const l=new Set;for(const e of a)for(const t of e.references)t.isDefinition||!i&&isTestFile(t.fileName)||(c++,l.add(t.fileName));return{count:c,uniqueFiles:l.size}}function findSymbolAtLine(e,t,n){const r=e.getPositionOfLineAndCharacter(Math.max(0,n-1),0),i=n<e.getLineAndCharacterOfPosition(e.getEnd()).line+1?e.getPositionOfLineAndCharacter(n,0):e.getEnd();let o;const s=n=>{o||(n.getStart(e)>=r&&n.getEnd()<=i&&ts.isIdentifier(n)&&n.text===t?o=n:ts.forEachChild(n,s))};return ts.forEachChild(e,s),o}function getInheritanceDepth(e,t){const n=[];let r=t,i=0;const o=new Set;for(;;){const t=r.getBaseTypes?.()??[];if(0===t.length)break;const s=t[0],a=e.typeToString(s);if(o.has(a))break;if(o.add(a),n.push(a),i++,r=s,i>20)break}return{depth:i,chain:n}}function collectExportReferences(e,t,n,r){const i=new Map,o=n.dependencyProfile?.declaredExports??[];for(const n of o){if(null==n.lineStart)continue;const o=getExportReferenceInfo(e,t,n.name,n.lineStart,r);i.set(n.name,{count:o.count,uniqueFiles:o.uniqueFiles,lineStart:n.lineStart,lineEnd:n.lineEnd??n.lineStart})}return i}function findFunctionNode(e,t,n,r){const i=e.getPositionOfLineAndCharacter(Math.max(0,n-1),0);let o;const s=n=>{if(!o){if(n.getStart(e)>=i&&n.getEnd()<=e.getPositionOfLineAndCharacter(Math.min(r,e.getLineAndCharacterOfPosition(e.getEnd()).line+1)-1,0)+200&&(ts.isFunctionDeclaration(n)||ts.isMethodDeclaration(n)||ts.isArrowFunction(n)||ts.isFunctionExpression(n))){if((n.name&&ts.isIdentifier(n.name)?n.name.text:"")===t||"<anonymous>"===t)return void(o=n)}ts.forEachChild(n,s)}};return ts.forEachChild(e,s),o}function collectUnusedParams(e,t,n,r){const i=[];for(const o of r.functions){if(!o.params||0===o.params)continue;if("<anonymous>"===o.name||""===o.name)continue;const r=findFunctionNode(n,o.name,o.lineStart,o.lineEnd);if(r&&(ts.isFunctionDeclaration(r)||ts.isMethodDeclaration(r)||ts.isArrowFunction(r)||ts.isFunctionExpression(r)))for(const s of r.parameters){if(!ts.isIdentifier(s.name))continue;const r=s.name.text;if(r.startsWith("_"))continue;const a=e.service.findReferences(t,s.name.getStart(n));let c=0;if(a)for(const e of a)for(const t of e.references)t.isDefinition||c++;0===c&&i.push({functionName:o.name,paramName:r,lineStart:o.lineStart,lineEnd:o.lineEnd})}}return i}function analyzeTypeHierarchy(e,t,n,r){let i=0,o=0;const s=n.dependencyProfile?.declaredExports??[],a=s=>{if(ts.isClassDeclaration(s)&&s.name){const o=e.checker.getTypeAtLocation(s),{depth:a,chain:c}=getInheritanceDepth(e.checker,o);if(a>0&&(r.typeHierarchies.push({name:s.name.text,depth:a,chain:c,lineStart:t.getLineAndCharacterOfPosition(s.getStart(t)).line+1}),a>r.typeHierarchyDepth&&(r.typeHierarchyDepth=a)),s.heritageClauses)for(const i of s.heritageClauses)if(i.token===ts.SyntaxKind.ImplementsKeyword)for(const o of i.types){const i=e.checker.getTypeAtLocation(o),a=e.checker.typeToString(i),c=e.checker.getTypeAtLocation(s),l=i.getProperties?.()??[],f=new Set((c.getProperties?.()??[]).map(e=>e.name)),u=l.filter(e=>!f.has(e.name)).map(e=>e.name),m=[];for(const t of l)if(f.has(t.name)){const n=c.getProperty?.(t.name);if(n){const r=e.checker.getTypeOfSymbolAtLocation(n,s);"any"===e.checker.typeToString(r)&&m.push(t.name)}}(u.length>0||m.length>0)&&r.interfaceImpls.push({interfaceName:a,className:s.name.text,classFile:n.file,classLine:t.getLineAndCharacterOfPosition(s.getStart(t)).line+1,missingMembers:u,anycastMembers:m})}s.modifiers?.some(e=>e.kind===ts.SyntaxKind.AbstractKeyword)&&i++;const l=new Map;let f=e.checker.getTypeAtLocation(s),u=0;for(;;){const e=f.getBaseTypes?.()??[];if(0===e.length)break;u++;const t=e[0];for(const e of t.getProperties?.()??[]){const t=e.getDeclarations?.()?.[0];t&&(ts.isMethodDeclaration(t)||ts.isMethodSignature(t))&&l.set(e.name,Math.max(l.get(e.name)??0,u))}if(f=t,u>20)break}for(const e of s.members)if(ts.isMethodDeclaration(e)&&e.name&&ts.isIdentifier(e.name)){const n=e.name.text,i=l.get(n);null!=i&&i>0&&r.overrideChains.push({methodName:n,className:s.name.text,depth:i,chain:[],lineStart:t.getLineAndCharacterOfPosition(e.getStart(t)).line+1})}}ts.isInterfaceDeclaration(s)&&(i++,o++),ts.isTypeAliasDeclaration(s)&&o++,ts.forEachChild(s,a)};return ts.forEachChild(t,a),o+=s.length,o>0?i/o:0}function analyzeImports(e,t,n,r){const i=[],o=[],s=r.dependencyProfile?.importedSymbols??[];for(const r of s){if(null==r.lineStart)continue;const s=findSymbolAtLine(n,r.localName,r.lineStart);if(!s)continue;const a=e.service.findReferences(t,s.getStart(n));let c=0;if(a)for(const e of a)for(const n of e.references)n.isDefinition||n.fileName===t&&c++;if(0===c&&i.push({name:r.localName,lineStart:r.lineStart}),r.resolvedModule){if(e.program.getSourceFile(path.resolve(e.root,r.resolvedModule))){const t=e.checker.getSymbolAtLocation(s);if(t){const n=t.flags&ts.SymbolFlags.Alias?e.checker.getAliasedSymbol(t):t,i=n.getDeclarations?.()?.[0];if(i&&ts.isClassDeclaration(i)){const e=i.modifiers?.some(e=>e.kind===ts.SyntaxKind.AbstractKeyword);e||o.push({name:r.localName,targetFile:r.resolvedModule,lineStart:r.lineStart})}}}}}return{unusedImports:i,concreteImports:o}}function detectLeakyReturns(e,t,n,r){const i=[],o=r.functions.filter(e=>r.dependencyProfile?.declaredExports?.some(t=>t.name===e.name));for(const r of o){const o=n.getPositionOfLineAndCharacter(Math.max(0,r.lineStart-1),0);let s;const a=e=>{s||((ts.isFunctionDeclaration(e)||ts.isMethodDeclaration(e))&&e.name&&ts.isIdentifier(e.name)&&e.name.text===r.name&&e.getStart(n)>=o?s=e:ts.forEachChild(e,a))};if(ts.forEachChild(n,a),s){const n=e.checker.getSignatureFromDeclaration(s);if(n){const o=e.checker.getReturnTypeOfSignature(n),s=o.symbol||o.aliasSymbol;if(s?.declarations?.[0]){const n=s.declarations[0].getSourceFile().fileName,a=path.relative(e.root,n);n===t||a.startsWith("node_modules")||n.includes("lib.")||i.push({functionName:r.name,returnType:e.checker.typeToString(o),sourceFile:a,lineStart:r.lineStart})}}}}return i}function detectNarrowableParams(e,t,n,r){const i=[],o=r.functions.filter(e=>r.dependencyProfile?.declaredExports?.some(t=>t.name===e.name));for(const r of o){if(!r.params||r.params<1)continue;const o=n.getPositionOfLineAndCharacter(Math.max(0,r.lineStart-1),0);let s;const a=e=>{s||(ts.isFunctionDeclaration(e)&&e.name?.text===r.name&&e.getStart(n)>=o?s=e:ts.forEachChild(e,a))};if(ts.forEachChild(n,a),s)for(const o of s.parameters){if(!ts.isIdentifier(o.name))continue;const a=e.checker.getTypeAtLocation(o);if(!(a.isUnion()||a.flags&(ts.TypeFlags.Any|ts.TypeFlags.Unknown)))continue;const c=e.checker.typeToString(a),l=e.service.findReferences(t,s.name.getStart(n));if(!l)continue;const f=[];let u=!0,m=0;for(const t of l)for(const n of t.references){if(n.isDefinition)continue;const t=e.program.getSourceFile(n.fileName);if(!t)continue;let r=t;const i=e=>{e.getStart(t)<=n.textSpan.start&&e.getEnd()>=n.textSpan.start+n.textSpan.length&&(r=e,ts.forEachChild(e,i))};let a;ts.forEachChild(t,i);let l=r;for(;l;){if(ts.isCallExpression(l)){a=l;break}l=l.parent}if(!a?.arguments)continue;const d=s.parameters.indexOf(o);if(d<0||d>=a.arguments.length){u=!1;continue}const p=e.checker.getTypeAtLocation(a.arguments[d]),h=e.checker.typeToString(p);f.push(h),m++,h!==c&&"any"!==h&&"unknown"!==h||(u=!1)}if(u&&m>=2&&f.length>0){const e=[...new Set(f)];e.length<=2&&i.push({functionName:r.name,paramName:o.name.text,declaredType:c,actualTypes:e,narrowedType:1===e.length?e[0]:e.join(" | "),lineStart:r.lineStart,lineEnd:r.lineEnd})}}}return i}export function analyzeSemanticProfile(e,t,n,r=!0){const i={file:n.file,referenceCountByExport:new Map,unusedParams:[],interfaceImpls:[],typeHierarchyDepth:0,typeHierarchies:[],overrideChains:[],abstractnessRatio:0,unusedImports:[],concreteImports:[],leakyReturns:[],narrowableParams:[]},o=e.program.getSourceFile(t);if(!o)return i;i.referenceCountByExport=collectExportReferences(e,t,n,r),i.unusedParams=collectUnusedParams(e,t,o,n),i.abstractnessRatio=analyzeTypeHierarchy(e,o,n,i);const s=analyzeImports(e,t,o,n);return i.unusedImports=s.unusedImports,i.concreteImports=s.concreteImports,i.leakyReturns=detectLeakyReturns(e,t,o,n),i.narrowableParams=detectNarrowableParams(e,t,o,n),i}export function collectAllAbsoluteFiles(e,t,n){const r=new Set;for(const t of e)r.add(path.resolve(n,t.file));for(const e of t.files)r.add(path.resolve(n,e));return[...r].filter(e=>{try{return fs.statSync(e).isFile()}catch{return!1}})}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import*as ts from"typescript";export function isFunctionLike(t){return ts.isFunctionDeclaration(t)||ts.isFunctionExpression(t)||ts.isArrowFunction(t)||ts.isMethodDeclaration(t)||ts.isConstructorDeclaration(t)||ts.isGetAccessor(t)||ts.isSetAccessor(t)}export function getFunctionName(t,e){if("name"in t&&t.name&&ts.isIdentifier(t.name))return t.name.getText(e);const s=t.parent;return s&&ts.isVariableDeclaration(s)&&s.name&&ts.isIdentifier(s.name)||s&&ts.isPropertyAssignment(s)&&ts.isIdentifier(s.name)||s&&ts.isPropertyDeclaration(s)&&s.name&&ts.isIdentifier(s.name)||s&&ts.isMethodDeclaration(s)&&s.name||s&&ts.isGetAccessor(s)&&s.name||s&&ts.isSetAccessor(s)&&s.name?s.name.getText(e):"<anonymous>"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import*as ts from"typescript";import{getLineAndCharacter}from"../common/utils.js";export function collectMetrics(t){const n={complexity:1,maxBranchDepth:0,maxLoopDepth:0,returns:0,awaits:0,calls:0,loops:0},e=(t,a,s)=>{switch(t.kind){case ts.SyntaxKind.IfStatement:case ts.SyntaxKind.WhileStatement:case ts.SyntaxKind.DoStatement:case ts.SyntaxKind.ForStatement:case ts.SyntaxKind.ForInStatement:case ts.SyntaxKind.ForOfStatement:case ts.SyntaxKind.SwitchStatement:case ts.SyntaxKind.CatchClause:n.complexity+=1,a+=1,n.maxBranchDepth=Math.max(n.maxBranchDepth,a);break;case ts.SyntaxKind.ConditionalExpression:n.complexity+=1;break;case ts.SyntaxKind.ReturnStatement:case ts.SyntaxKind.ThrowStatement:n.returns+=1;break;case ts.SyntaxKind.AwaitExpression:n.awaits+=1;break;case ts.SyntaxKind.CallExpression:n.calls+=1;break;default:t.kind!==ts.SyntaxKind.BinaryExpression||t.operatorToken.kind!==ts.SyntaxKind.AmpersandAmpersandToken&&t.operatorToken.kind!==ts.SyntaxKind.BarBarToken||(n.complexity+=1)}if(t.kind===ts.SyntaxKind.ForStatement||t.kind===ts.SyntaxKind.ForInStatement||t.kind===ts.SyntaxKind.ForOfStatement){const i=s+1;return n.loops+=1,n.maxLoopDepth=Math.max(n.maxLoopDepth,i),void ts.forEachChild(t,t=>e(t,a,i))}ts.forEachChild(t,t=>e(t,a,s))};return e(t,0,0),n}const HALSTEAD_OPERATOR_KINDS=new Set([ts.SyntaxKind.PlusToken,ts.SyntaxKind.MinusToken,ts.SyntaxKind.AsteriskToken,ts.SyntaxKind.SlashToken,ts.SyntaxKind.PercentToken,ts.SyntaxKind.AsteriskAsteriskToken,ts.SyntaxKind.PlusPlusToken,ts.SyntaxKind.MinusMinusToken,ts.SyntaxKind.EqualsToken,ts.SyntaxKind.PlusEqualsToken,ts.SyntaxKind.MinusEqualsToken,ts.SyntaxKind.AsteriskEqualsToken,ts.SyntaxKind.SlashEqualsToken,ts.SyntaxKind.EqualsEqualsToken,ts.SyntaxKind.EqualsEqualsEqualsToken,ts.SyntaxKind.ExclamationEqualsToken,ts.SyntaxKind.ExclamationEqualsEqualsToken,ts.SyntaxKind.LessThanToken,ts.SyntaxKind.GreaterThanToken,ts.SyntaxKind.LessThanEqualsToken,ts.SyntaxKind.GreaterThanEqualsToken,ts.SyntaxKind.AmpersandAmpersandToken,ts.SyntaxKind.BarBarToken,ts.SyntaxKind.ExclamationToken,ts.SyntaxKind.QuestionQuestionToken,ts.SyntaxKind.IfKeyword,ts.SyntaxKind.ElseKeyword,ts.SyntaxKind.ForKeyword,ts.SyntaxKind.WhileKeyword,ts.SyntaxKind.DoKeyword,ts.SyntaxKind.SwitchKeyword,ts.SyntaxKind.CaseKeyword,ts.SyntaxKind.ReturnKeyword,ts.SyntaxKind.ThrowKeyword,ts.SyntaxKind.NewKeyword,ts.SyntaxKind.DeleteKeyword,ts.SyntaxKind.TypeOfKeyword,ts.SyntaxKind.AwaitKeyword,ts.SyntaxKind.YieldKeyword,ts.SyntaxKind.DotToken,ts.SyntaxKind.OpenParenToken,ts.SyntaxKind.OpenBracketToken,ts.SyntaxKind.EqualsGreaterThanToken,ts.SyntaxKind.DotDotDotToken]);export function computeHalstead(t){const n=new Map,e=new Map,a=t=>{if(ts.isIdentifier(t)||ts.isPrivateIdentifier(t)){const n=t.text;e.set(n,(e.get(n)||0)+1)}else if(ts.isNumericLiteral(t)||ts.isStringLiteral(t)||ts.isNoSubstitutionTemplateLiteral(t)){const n=t.getText();e.set(n,(e.get(n)||0)+1)}else if(ts.isToken(t)&&HALSTEAD_OPERATOR_KINDS.has(t.kind)){const e=ts.SyntaxKind[t.kind];n.set(e,(n.get(e)||0)+1)}ts.forEachChild(t,a)};a(t);const s=n.size,i=e.size;let o=0;for(const t of n.values())o+=t;let r=0;for(const t of e.values())r+=t;const d=s+i,x=o+r,y=d>0?x*Math.log2(d):0,S=i>0?s/2*(r/i):0,K=y*S;return{operators:o,operands:r,distinctOperators:s,distinctOperands:i,vocabulary:d,length:x,volume:y,difficulty:S,effort:K,time:K/18,estimatedBugs:y/3e3}}export function computeMaintainabilityIndex(t,n,e){const a=Math.max(t,1),s=Math.max(e,1),i=171-5.2*Math.log(a)-.23*n-16.2*Math.log(s);return Math.max(0,100*i/171)}export function countLinesInNode(t,n){const e=getLineAndCharacter(t,n);return Math.max(1,e.lineEnd-e.lineStart+1)}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import fs from"node:fs";import path from"node:path";import{js as astJs,ts as astTs,tsx as astTsx}from"@ast-grep/napi";import{isDirectRun}from"../common/is-direct-run.js";import{ALLOWED_EXTS}from"../types/index.js";export const PRESETS={"empty-catch":{rule:{kind:"catch_clause",has:{kind:"statement_block",regex:"^\\{\\s*\\}$"}},description:"Empty catch blocks that silently swallow errors"},"console-log":{rule:{pattern:"console.log($$$ARGS)"},description:"console.log calls left in production code"},"console-any":{rule:{pattern:"console.$METHOD($$$ARGS)"},description:"Any console method call (log, warn, error, debug, etc.)"},debugger:{rule:{kind:"debugger_statement"},description:"Debugger statements left in code"},"todo-fixme":{rule:{kind:"comment",regex:"(?i)(TODO|FIXME|HACK|XXX|BUG)"},description:"TODO, FIXME, HACK, XXX, BUG comments"},"any-type":{rule:{kind:"predefined_type",regex:"^any$"},description:"Explicit `any` type annotations"},"type-assertion":{rule:{kind:"as_expression"},description:"TypeScript type assertions (as X)"},"non-null-assertion":{rule:{kind:"non_null_expression"},description:"Non-null assertions (x!)"},"fat-arrow-body":{rule:{kind:"arrow_function",has:{kind:"statement_block"}},description:"Arrow functions with statement block bodies (could be expression)"},"nested-ternary":{rule:{kind:"ternary_expression",has:{kind:"ternary_expression",stopBy:"end"}},description:"Nested ternary expressions (hard to read)"},"throw-string":{rule:{kind:"throw_statement",has:{kind:"string"}},description:"Throwing string literals instead of Error objects"},"switch-no-default":{rule:{kind:"switch_statement",not:{has:{kind:"switch_default",stopBy:"end"}}},description:"Switch statements without a default case"},"class-declaration":{rule:{kind:"class_declaration"},description:"All class declarations"},"async-function":{rule:{kind:"function_declaration",regex:"^async "},description:"Async function declarations"},"export-default":{rule:{kind:"export_statement",has:{field:"default"}},description:"Default exports"},"import-star":{rule:{kind:"import_statement",has:{kind:"namespace_import"}},description:"Namespace imports (import * as X)"}};function isTestFile(e){const t=path.basename(e);return/\.(test|spec)\.(ts|tsx|js|jsx|mjs|cjs)$/.test(t)||t.startsWith("test_")||e.includes("__tests__")}export function collectSearchFiles(e,t){const n=[],s=e=>{let r;try{r=fs.readdirSync(e,{withFileTypes:!0})}catch{return}r.sort((e,t)=>e.name.localeCompare(t.name));for(const o of r){if(t.ignoreDirs.has(o.name))continue;if(o.isSymbolicLink())continue;const r=path.join(e,o.name);if(o.isDirectory()){s(r);continue}if(!o.isFile())continue;if(o.name.endsWith(".d.ts"))continue;const i=path.extname(o.name);ALLOWED_EXTS.has(i)&&(!t.includeTests&&isTestFile(r)||n.push(r))}};return s(e),n}function parserForExt(e){switch(e){case".tsx":return astTsx;case".jsx":case".js":case".mjs":case".cjs":return astJs;default:return astTs}}function extractMetaVars(e,t){const n={};let s;const r=/\$\$\$([A-Z_][A-Z0-9_]*)/g,o=new Set;for(;null!==(s=r.exec(t));){const t=s[1];o.add(t);const r=e.getMultipleMatches(t);r.length>0&&(n[`$$$${t}`]=r.map(e=>e.text()).join(", "))}const i=/(?<!\$)\$([A-Z_][A-Z0-9_]*)(?!\$)/g;for(;null!==(s=i.exec(t));){const t=s[1];if(o.has(t))continue;const r=e.getMatch(t);r&&(n[`$${t}`]=r.text())}return n}function nodeToMatch(e,t,n){const s=e.range(),r={file:t,kind:String(e.kind()),text:e.text(),lineStart:s.start.line+1,lineEnd:s.end.line+1,columnStart:s.start.column,columnEnd:s.end.column};if(n){const t=extractMetaVars(e,n);Object.keys(t).length>0&&(r.metaVariables=t)}return r}export function searchFile(e,t,n,s,r){const o=parserForExt(path.extname(e));let i;try{i=o.parse(t).root().findAll(n)}catch{return[]}const a=[];for(const t of i){if(a.length>=r)break;a.push(nodeToMatch(t,e,s))}return a}export function runSearch(e,t,n){let s,r,o,i=null;if(t.preset){const e=PRESETS[t.preset];if(!e){const e=Object.keys(PRESETS).join(", ");throw new Error(`Unknown preset: "${t.preset}". Available: ${e}`)}s=e,r=`preset:${t.preset} — ${e.description}`,o="preset"}else if(t.rule)s=t.rule,r=`rule:${JSON.stringify(t.rule)}`,o="rule";else if(t.kind)s={rule:{kind:t.kind}},r=`kind:${t.kind}`,o="kind";else{if(!t.pattern)throw new Error("Must provide --pattern, --kind, --preset, or --rule");s=t.pattern,i=t.pattern,r=`pattern:${t.pattern}`,o="pattern"}const a=[],c=new Set,l=t.context>0?new Map:void 0;for(const r of e){if(a.length>=t.limit)break;let e;try{e=fs.readFileSync(r,"utf8")}catch{continue}const o=path.relative(n,r),p=searchFile(o,e,s,i,t.limit-a.length);p.length>0&&(c.add(o),a.push(...p),l&&l.set(o,e.split("\n")))}const p={query:r,queryType:o,totalMatches:a.length,totalFiles:c.size,matches:a};return l&&(p._sourceByFile=l),p}export function parseSearchArgs(e){const t={root:process.cwd(),pattern:null,kind:null,preset:null,rule:null,json:!1,limit:500,includeTests:!1,ignoreDirs:new Set([".git",".next",".yarn",".cache",".octocode","node_modules","dist","coverage","out"]),context:0};let n=!1;for(let s=0;s<e.length;s++){const r=e[s];if("--pattern"!==r&&"-p"!==r)if(r.startsWith("--pattern="))t.pattern=r.slice(10);else if("--kind"!==r&&"-k"!==r)if(r.startsWith("--kind="))t.kind=r.slice(7);else if("--preset"!==r)if(r.startsWith("--preset="))t.preset=r.slice(9);else{if("--rule"===r){const n=e[++s];try{t.rule=JSON.parse(n)}catch{throw new Error(`Invalid --rule JSON: ${n?.slice(0,100)??"(empty)"}`)}continue}"--root"!==r?r.startsWith("--root=")?t.root=path.resolve(r.slice(7)):"--json"!==r?"--limit"!==r?"--include-tests"!==r?"--context"!==r&&"-C"!==r?"--list-presets"!==r?"--help"!==r&&"-h"!==r||(printSearchHelp(),process.exit(0)):n=!0:t.context=parseInt(e[++s],10):t.includeTests=!0:t.limit=parseInt(e[++s],10):t.json=!0:t.root=path.resolve(e[++s])}else t.preset=e[++s];else t.kind=e[++s];else t.pattern=e[++s]}return Number.isNaN(t.limit)&&(t.limit=500),Number.isNaN(t.context)&&(t.context=0),{opts:t,listPresets:n}}function printSearchHelp(){console.log(`\nast-search — Structural code search powered by ast-grep\n\nUsage:\n node scripts/ast/search.js [options]\n\nSearch modes (pick one):\n --pattern, -p <code> Match code structurally (e.g. 'console.log($$$ARGS)')\n --kind, -k <kind> Match AST node kind (e.g. 'function_declaration')\n --preset <name> Use a built-in search preset (e.g. 'empty-catch')\n --rule <json> Raw ast-grep rule object as JSON\n\nOptions:\n --root <path> Search root directory (default: cwd)\n --json Output as JSON\n --limit N Max matches (default: 500)\n --include-tests Include test files\n --context, -C N Lines of context around matches (text output only)\n --list-presets Show available presets and exit\n --help, -h Show this message\n\nPattern wildcards:\n $NAME Match any single AST node\n $$$NAME Match zero or more nodes (variadic)\n\nExamples:\n node scripts/ast/search.js -p 'console.log($$$ARGS)' --root ./src\n node scripts/ast/search.js --preset empty-catch --root ./packages\n node scripts/ast/search.js -k function_declaration --json --limit 20\n node scripts/ast/search.js --preset todo-fixme --include-tests\n node scripts/ast/search.js -p 'if ($COND) { return $VAL }' --root ./src\n node scripts/ast/search.js --rule '{"rule":{"kind":"catch_clause"}}' --root ./src\n\nPresets:\n${Object.entries(PRESETS).map(([e,t])=>` ${e.padEnd(22)} ${t.description}`).join("\n")}\n`)}export function formatTextOutput(e,t,n){const s=[];s.push(`\n🔍 ${e.query}`),s.push(` ${e.totalMatches} matches across ${e.totalFiles} files\n`);const r=t.context,o=e._sourceByFile;let i="";for(const t of e.matches){if(t.file!==i&&(i=t.file,s.push(`\n── ${i} ──`)),r>0&&o){const e=o.get(t.file);if(e){const n=Math.max(0,t.lineStart-1-r),o=Math.min(e.length,t.lineEnd+r);for(let r=n;r<o;r++){const n=r+1,o=n>=t.lineStart&&n<=t.lineEnd?">":" ";s.push(` ${o} ${String(n).padStart(4)} | ${e[r]}`)}s.push("");continue}}const e=(t.text.length>200?t.text.slice(0,200)+"…":t.text).replace(/\n/g,"↵").replace(/\s+/g," ");if(s.push(` L${t.lineStart}:${t.columnStart} [${t.kind}] ${e}`),t.metaVariables&&Object.keys(t.metaVariables).length>0)for(const[e,n]of Object.entries(t.metaVariables)){const t=n.length>80?n.slice(0,80)+"…":n;s.push(` ${e} = ${t}`)}}return s.push(""),s.join("\n")}async function main(){const{opts:e,listPresets:t}=parseSearchArgs(process.argv.slice(2));if(t){if(e.json)console.log(JSON.stringify(PRESETS));else{console.log("\nAvailable presets:\n");for(const[e,t]of Object.entries(PRESETS))console.log(` ${e.padEnd(22)} ${t.description}`);console.log("")}return}e.pattern||e.kind||e.preset||e.rule||(console.error("Error: Must provide --pattern, --kind, --preset, or --rule"),console.error("Run with --help for usage information."),process.exit(1));const n=collectSearchFiles(e.root,e);0===n.length&&(console.error(`No files found in ${e.root}`),process.exit(1));const s=runSearch(n,e,e.root);e.json?console.log(JSON.stringify(s)):console.log(formatTextOutput(s,e,e.root))}isDirectRun(import.meta.url)&&main().catch(e=>{console.error(e),process.exit(1)});
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import fs from"node:fs";import path from"node:path";import{isDirectRun}from"../common/is-direct-run.js";function printAstTreeSearchHelp(){console.log("\nast-tree-search — Search generated ast-trees.txt output\n\nUsage:\n node scripts/ast/tree-search.js [options]\n\nOptions:\n --input, -i <path> Path to ast-trees.txt, a scan directory, or .octocode/scan (default: .octocode/scan)\n --pattern, -p <regex> Regex to match against AST tree lines\n --kind, -k <kind> Match a node kind (supports snake_case or PascalCase)\n --file <regex> Filter matches to section file paths that match the regex\n --section <regex> Filter matches to section headers that match the regex\n --limit <n> Max matches to return (default: 50, 0 = all)\n --context, -C <n> Context lines around each match (default: 0)\n --json Output matches as JSON\n --ignore-case Case-insensitive pattern matching\n --help, -h Show this message\n\nExamples:\n node scripts/ast/tree-search.js -i .octocode/scan -k function_declaration --limit 25\n node scripts/ast/tree-search.js -i .octocode/scan/2026-03-18T23-43-21-490Z -k ClassDeclaration --file 'src/index'\n node scripts/ast/tree-search.js -i .octocode/scan -p 'IfStatement|SwitchStatement' --section 'src/'\n")}function parseArgValue(e,t,n){const i=e.indexOf("=");return-1!==i?{value:e.slice(i+1),nextIndex:n}:{value:t[n+1]||"",nextIndex:n+1}}export function parseAstTreeSearchArgs(e){const t={input:".octocode/scan",pattern:null,kind:null,context:0,json:!1,ignoreCase:!1,limit:50,file:null,section:null};let n=!1;for(let i=0;i<e.length;i+=1){const s=e[i];if("--json"!==s)if("--ignore-case"!==s){if("--help"!==s&&"-h"!==s){if("--input"===s||"-i"===s||s.startsWith("--input=")){const n=parseArgValue(s,e,i);t.input=n.value,i=n.nextIndex;continue}if("--pattern"===s||"-p"===s||s.startsWith("--pattern=")){const n=parseArgValue(s,e,i);t.pattern=n.value,i=n.nextIndex;continue}if("--kind"===s||"-k"===s||s.startsWith("--kind=")){const n=parseArgValue(s,e,i);t.kind=n.value,i=n.nextIndex;continue}if("--file"===s||s.startsWith("--file=")){const n=parseArgValue(s,e,i);t.file=n.value,i=n.nextIndex;continue}if("--section"===s||s.startsWith("--section=")){const n=parseArgValue(s,e,i);t.section=n.value,i=n.nextIndex;continue}if("--limit"===s||s.startsWith("--limit=")){const n=parseArgValue(s,e,i),r=Number.parseInt(n.value,10);t.limit=Number.isFinite(r)?r:50,i=n.nextIndex;continue}if("--context"===s||"-C"===s||s.startsWith("--context=")){const n=parseArgValue(s,e,i),r=Number.parseInt(n.value,10);t.context=Number.isFinite(r)?r:0,i=n.nextIndex;continue}throw new Error(`Unknown argument: ${s}`)}n=!0}else t.ignoreCase=!0;else t.json=!0}return{opts:t,showHelp:n}}export function validateAstTreeSearchOptions(e){if(!e.pattern&&!e.kind)throw new Error("Must provide --pattern or --kind");if(!Number.isFinite(e.context)||e.context<0)throw new Error("--context must be a non-negative integer");if(!Number.isFinite(e.limit)||e.limit<0)throw new Error("--limit must be a non-negative integer")}function toSnakeCase(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1_$2").replace(/[-\s]+/g,"_").toLowerCase()}function toPascalCase(e){return e.split("_").filter(Boolean).map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join("")}function escapeRegExp(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function buildRegex(e,t,n){if(!e)return null;try{return new RegExp(e,t)}catch(e){throw new Error(`Invalid ${n} regex: ${e.message}`)}}function parseSectionFile(e){const t=e.indexOf(" — ");return-1===t?null:e.slice(t+3).trim()||null}export function resolveAstTreeInput(e){const t=path.resolve(e);if(!fs.existsSync(t))throw new Error(`Input does not exist: ${t}`);if(fs.statSync(t).isFile())return{requestedInput:t,inputFile:t,selectionMode:"direct-file"};const n=path.join(t,"ast-trees.txt");if(fs.existsSync(n)&&fs.statSync(n).isFile())return{requestedInput:t,inputFile:n,selectionMode:"scan-dir"};const i=fs.readdirSync(t,{withFileTypes:!0}).filter(e=>e.isDirectory()).map(e=>path.join(t,e.name,"ast-trees.txt")).filter(e=>fs.existsSync(e)&&fs.statSync(e).isFile()).sort((e,t)=>fs.statSync(t).mtimeMs-fs.statSync(e).mtimeMs);if(0===i.length)throw new Error(`No ast-trees.txt found under: ${t}`);return{requestedInput:t,inputFile:i[0],selectionMode:"latest-scan"}}export function searchAstTree(e,t){const n=t.ignoreCase?"i":"",i=buildRegex(t.pattern,n,"pattern"),s=buildRegex(t.file,n,"file"),r=buildRegex(t.section,n,"section");let o=null;if(t.kind){const e=toSnakeCase(t.kind),i=toPascalCase(e);o=new RegExp(`\\b(?:${escapeRegExp(e)}|${escapeRegExp(i)})\\b`,n)}const a=fs.readFileSync(e.inputFile,"utf8").split(/\r?\n/),c=[];let l="",u=null;for(let e=0;e<a.length;e+=1){const n=a[e];if(n.startsWith("## ")&&(l=n.slice(3).trim(),u=parseSectionFile(l)),i&&!i.test(n))continue;if(o&&!o.test(n))continue;if(r&&!r.test(l))continue;if(s&&!s.test(u??""))continue;const p=Math.max(0,e-t.context),h=Math.min(a.length,e+t.context+1);c.push({section:l,file:u,lineNumber:e+1,line:n,context:a.slice(p,h).map((e,t)=>({lineNumber:p+t+1,line:e}))})}const p=0===t.limit?c:c.slice(0,t.limit),h=new Set(c.map(e=>e.file).filter(Boolean)).size,f=[t.kind?`kind=${t.kind}`:null,t.pattern?`pattern=${t.pattern}`:null,t.file?`file=${t.file}`:null,t.section?`section=${t.section}`:null].filter(Boolean);return{requestedInput:e.requestedInput,inputFile:e.inputFile,selectionMode:e.selectionMode,query:f.join(", "),limit:t.limit,totalMatches:c.length,returnedMatches:p.length,truncated:p.length<c.length,uniqueFiles:h,matches:p}}export function formatAstTreeSearchOutput(e,t){const n=[];n.push(`\nAST tree search: ${e.query}`),n.push(`Requested input: ${e.requestedInput}`),n.push(`Selected AST file: ${e.inputFile} (${e.selectionMode})`),n.push(`Matches: ${e.totalMatches} total, showing ${e.returnedMatches}${e.truncated?` (limit ${e.limit})`:""}`),n.push(`Matched files: ${e.uniqueFiles}\n`);let i="";for(const s of e.matches){if(s.section!==i&&(i=s.section,n.push(`-- ${i||"(no section)"} --`)),t.context>0){for(const e of s.context){const t=e.lineNumber===s.lineNumber?">":" ";n.push(` ${t} ${String(e.lineNumber).padStart(4)} | ${e.line}`)}n.push("");continue}const e=s.file?` (${s.file})`:"";n.push(` L${s.lineNumber}${e} ${s.line}`)}return 0===e.totalMatches&&n.push("No matches found."),n.push(""),n.join("\n")}async function main(){try{const{opts:e,showHelp:t}=parseAstTreeSearchArgs(process.argv.slice(2));if(t)return void printAstTreeSearchHelp();validateAstTreeSearchOptions(e);const n=searchAstTree(resolveAstTreeInput(e.input),e);if(e.json)return void console.log(JSON.stringify(n,null,2));console.log(formatAstTreeSearchOutput(n,e))}catch(e){console.error(e.message),process.exit(1)}}isDirectRun(import.meta.url)&&main();
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import path from"node:path";import{buildTreeSitterTree,hashString,increment,makeTreeSitterFingerprint}from"../common/utils.js";import{TS_TREE_SITTER_CONTROL_TYPES,TS_TREE_SITTER_FUNCTION_TYPES}from"../types/index.js";let treeSitterRuntime=null;export function getTreeSitterRuntime(){return treeSitterRuntime}function hasLogicalOperator(e){for(const t of e.children)if(!t.isNamed&&("&&"===t.type||"||"===t.type))return!0;return!1}function collectTreeSitterMetrics(e,t){const n={complexity:1,maxBranchDepth:0,maxLoopDepth:0,returns:0,awaits:0,calls:0,loops:0,statements:0},r=(e,t,i)=>{if(n.statements+=1,["if_statement","while_statement","do_statement","for_statement","for_in_statement","for_of_statement","for_await_statement","switch_statement","catch_clause"].includes(e.type)&&(n.complexity+=1,t+=1,n.maxBranchDepth=Math.max(n.maxBranchDepth,t)),"conditional_expression"===e.type&&(n.complexity+=1),"binary_expression"===e.type&&hasLogicalOperator(e)&&(n.complexity+=1),"return_statement"!==e.type&&"throw_statement"!==e.type||(n.returns+=1),"await_expression"===e.type&&(n.awaits+=1),"call_expression"===e.type&&(n.calls+=1),["for_statement","for_in_statement","for_of_statement","for_await_statement","while_statement","do_statement"].includes(e.type)){const o=i+1;n.loops+=1,n.maxLoopDepth=Math.max(n.maxLoopDepth,o);for(const n of e.children)r(n,t,o);return}for(const n of e.children)r(n,t,i)};return r(e,0,0),n}function inferTreeSitterFunctionName(e,t){const n=e.namedChildren.find(e=>["identifier","property_identifier","type_identifier"].includes(e.type));if(n)return n.text;let r=e.parent;for(;r;){if("variable_declarator"===r.type){const e=r.namedChildren.find(e=>["identifier","property_identifier","array_pattern","object_pattern","shorthand_property_identifier_pattern"].includes(e.type));if(e&&"identifier"===e.type)return e.text;break}if("pair"===r.type){const e=r.namedChildren.find(e=>["identifier","string","shorthand_property_identifier_pattern","property_identifier"].includes(e.type));if(e)return e.text;break}if(["assignment_expression","method_definition","property_signature","public_field_definition"].includes(r.type)){const e=r.namedChildren.find(e=>["identifier","property_identifier","string","private_property_identifier"].includes(e.type));if(e)return e.text}if("statement_block"===r.type||"program"===r.type)break;r=r.parent}return"<anonymous>"}function countTreeSitterStatements(e){const t=e.namedChildren.find(e=>"statement_block"===e.type);return t?t.namedChildren.length:1}function countControlFlowBodyStatements(e){const t=e.namedChildren.find(e=>"statement_block"===e.type||"switch_body"===e.type);return t?t.namedChildren.length:e.namedChildren.length}function makeLocationFromTree(e,t,n){return{file:path.relative(t,n),lineStart:e.startPosition.row+1,lineEnd:e.endPosition.row+1,columnStart:e.startPosition.column+1,columnEnd:e.endPosition.column+1}}export function analyzeTreeSitterFile(e,t,n,r,i){if(!treeSitterRuntime?.available)return null;const o=path.extname(e),a=".tsx"===o||".jsx"===o?treeSitterRuntime.parserTsx:treeSitterRuntime.parserTs;if(!a)return null;const s=a.parse(t),l=path.relative(n.root,e),p={parseEngine:"tree-sitter",nodeCount:0,functions:[],flows:[]};if(n.emitTree){const e={size:8e3},r=buildTreeSitterTree(s.rootNode,t,n.treeDepth,e);r&&(p.tree=r)}const m=r=>{if(p.nodeCount+=1,TS_TREE_SITTER_FUNCTION_TYPES.has(r.type)){const o=makeLocationFromTree(r,n.root,e),a=collectTreeSitterMetrics(r,t),s=countTreeSitterStatements(r),m=inferTreeSitterFunctionName(r,t),c=r.childForFieldName("parameters"),d=c?c.namedChildren.length:0,u={kind:r.type,name:m,nameHint:m,file:l,lineStart:o.lineStart,lineEnd:o.lineEnd,columnStart:o.columnStart,columnEnd:o.columnEnd,statementCount:s,lengthLines:o.lineEnd-o.lineStart+1,params:d,complexity:a.complexity,maxBranchDepth:a.maxBranchDepth,maxLoopDepth:a.maxLoopDepth,returns:a.returns,awaits:a.awaits,calls:a.calls,loops:a.loops,cognitiveComplexity:0,source:"tree-sitter"};if(p.functions.push(u),i&&s>=n.minFunctionStatements){const e=r.namedChildren.find(e=>"statement_block"===e.type),t=e?makeTreeSitterFingerprint(e):hashString(l);increment(i.flowMap,`${t}|${r.type}`,{...u,hash:t,metrics:a})}}if(TS_TREE_SITTER_CONTROL_TYPES.has(r.type)){const t=makeLocationFromTree(r,n.root,e),o=countControlFlowBodyStatements(r),a={kind:r.type,file:l,lineStart:t.lineStart,lineEnd:t.lineEnd,columnStart:t.columnStart,columnEnd:t.columnEnd,statementCount:o};if(p.flows.push(a),i&&o>=n.minFlowStatements){const e=makeTreeSitterFingerprint(r);increment(i.controlMap,`${e}|${r.type}`,{...a,hash:e})}}for(const e of r.children)m(e)};return m(s.rootNode),p}export async function resolveTreeSitter(){if(null!==treeSitterRuntime)return treeSitterRuntime;try{const e=await import("tree-sitter"),t=await import("tree-sitter-typescript"),n=e.default||e,r=t.typescript||t.default?.typescript,i=t.tsx||t.default?.tsx;if(!n||!r)throw new Error("Tree-sitter or tree-sitter-typescript did not expose expected exports");const o=new n;o.setLanguage(r);const a=new n;return a.setLanguage(i||r),treeSitterRuntime={available:!0,parserTs:o,parserTsx:a},treeSitterRuntime}catch(e){return treeSitterRuntime={available:!1,parserTs:null,parserTsx:null,error:String(e?.message||e)},treeSitterRuntime}}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import path from"node:path";import*as ts from"typescript";import{getFunctionName,isFunctionLike}from"./helpers.js";import{collectMetrics,computeHalstead,computeMaintainabilityIndex,countLinesInNode}from"./metrics.js";import{collectMessageChains}from"../collectors/chains.js";import{collectTopLevelEffects}from"../collectors/effects.js";import{collectInputSourceProfile}from"../collectors/input-sources.js";import{collectPerformanceData}from"../collectors/performance.js";import{collectPrototypePollutionSites}from"../collectors/prototype-pollution.js";import{collectSecurityData}from"../collectors/security.js";import{collectTestProfile}from"../collectors/test-profile.js";import{buildNodeTree,getLineAndCharacter,hashString,increment,isTestFile,makeFingerprint}from"../common/utils.js";import{computeCognitiveComplexity}from"../detectors/index.js";import{TS_CONTROL_KINDS}from"../types/index.js";export{isFunctionLike,getFunctionName}from"./helpers.js";export{collectMetrics,computeHalstead,computeMaintainabilityIndex,countLinesInNode}from"./metrics.js";export function buildDependencyCriticality(t,e){if(!t||!Array.isArray(t.functions))return{file:t?.file||"<unknown>",complexityRisk:1,highComplexityFunctions:0,functionCount:0,flows:0,score:1};let n=0,i=0;for(const o of t.functions){const t=Number(o.complexity)||0;n+=t,t>=e.criticalComplexityThreshold&&(i+=1)}const o=t.flows?t.flows.length:0,s=Math.max(1,Math.round(.7*n+2*t.functions.length+.2*o));return{file:t.file,functionCount:t.functions.length,highComplexityFunctions:i,flows:o,complexitySum:n,complexityRisk:i,score:s}}function countControlFlowStatements(t){if(ts.isIfStatement(t)){const e=t.thenStatement;return ts.isBlock(e)?e.statements.length:1}if(ts.isSwitchStatement(t))return t.caseBlock.clauses.length;if(ts.isTryStatement(t))return t.tryBlock.statements.length;if(ts.isForStatement(t)||ts.isWhileStatement(t)||ts.isDoStatement(t)||ts.isForOfStatement(t)||ts.isForInStatement(t)){const e=t.statement;return ts.isBlock(e)?e.statements.length:1}return 1}function makeLocationFromTs(t,e,n){const i=getLineAndCharacter(e,t);return{file:path.relative(n,t.getSourceFile().fileName),lineStart:i.lineStart,lineEnd:i.lineEnd,columnStart:i.columnStart,columnEnd:i.columnEnd}}export function analyzeSourceFile(t,e,n,i,o,s,l){const r=t.fileName,a=path.relative(i.root,r);n.fileCount+=1,n.nodeCount+=1,n.kindCounts.SourceFile=(n.kindCounts.SourceFile||0)+1;const c={package:e,file:a,parseEngine:"typescript",nodeCount:0,kindCounts:{},functions:[],flows:[],dependencyProfile:l};if(i.emitTree){const n={size:8e3},o=buildNodeTree(t,t,i.treeDepth,n);o&&s.push({package:e,file:a,tree:o})}const m=TS_CONTROL_KINDS,u=[],p=[],f=[];let d=0;const h=[],S=[],y=[],C=new Set([0,1,-1,2,100]),x=e=>{c.nodeCount+=1,n.nodeCount+=1;const s=ts.SyntaxKind[e.kind]||"UNKNOWN";if(c.kindCounts[s]=(c.kindCounts[s]||0)+1,n.kindCounts[s]=(n.kindCounts[s]||0)+1,ts.isCatchClause(e)){if(0===e.block.statements.length){const n=getLineAndCharacter(t,e);u.push({file:a,lineStart:n.lineStart,lineEnd:n.lineEnd})}}if(ts.isSwitchStatement(e)){if(!e.caseBlock.clauses.some(t=>t.kind===ts.SyntaxKind.DefaultClause)){const n=getLineAndCharacter(t,e);p.push({file:a,lineStart:n.lineStart,lineEnd:n.lineEnd})}}if(e.kind===ts.SyntaxKind.AnyKeyword&&(d+=1),ts.isAsExpression(e)&&e.type.kind===ts.SyntaxKind.AnyKeyword&&(d+=1),ts.isAsExpression(e)){if(e.type.kind===ts.SyntaxKind.AnyKeyword){const n=getLineAndCharacter(t,e);h.push({file:a,lineStart:n.lineStart,lineEnd:n.lineEnd})}if(ts.isAsExpression(e.expression)&&e.expression.type.kind===ts.SyntaxKind.UnknownKeyword){const n=getLineAndCharacter(t,e);S.push({file:a,lineStart:n.lineStart,lineEnd:n.lineEnd})}}if(ts.isNonNullExpression(e)){const n=getLineAndCharacter(t,e);y.push({file:a,lineStart:n.lineStart,lineEnd:n.lineEnd})}if(ts.isNumericLiteral(e)){const n=Number(e.text);if(!C.has(n)){const i=e.parent,o=i&&ts.isVariableDeclaration(i)&&i.parent&&ts.isVariableDeclarationList(i.parent)&&0!==(i.parent.flags&ts.NodeFlags.Const),s=i&&ts.isEnumMember(i);if(!o&&!s){const i=getLineAndCharacter(t,e);f.push({value:n,file:a,lineStart:i.lineStart,lineEnd:i.lineEnd})}}}if(isFunctionLike(e)){const l=e,r=l.body,m=r&&ts.isBlock(r)?r.statements.length:1,u=makeLocationFromTs(e,t,i.root),p=getFunctionName(e,t),f=r?collectMetrics(r):{complexity:1,maxBranchDepth:0,maxLoopDepth:0,returns:0,awaits:0,calls:0,loops:0},d={kind:s,name:p,nameHint:p,file:a,lineStart:u.lineStart,lineEnd:u.lineEnd,columnStart:u.columnStart,columnEnd:u.columnEnd,statementCount:m,complexity:f.complexity,maxBranchDepth:f.maxBranchDepth,maxLoopDepth:f.maxLoopDepth,returns:f.returns,awaits:f.awaits,calls:f.calls,loops:f.loops,lengthLines:countLinesInNode(t,e),cognitiveComplexity:r?computeCognitiveComplexity(r):0};if(r&&(d.halstead=computeHalstead(r),d.maintainabilityIndex=computeMaintainabilityIndex(d.halstead.volume,f.complexity,d.lengthLines)),ts.isFunctionDeclaration(e)&&(d.declared=!0),m>=i.minFunctionStatements){const t=r?makeFingerprint(r):hashString(a);increment(o.flowMap,`${t}|${e.kind}`,{...d,hash:t,metrics:f})}l.parameters&&(d.params=l.parameters.length),c.functions.push(d),n.functions.push(d),n.functionCount+=1}if(m.has(e.kind)){const l=countControlFlowStatements(e),r=makeLocationFromTs(e,t,i.root),m={kind:s,file:a,lineStart:r.lineStart,lineEnd:r.lineEnd,columnStart:r.columnStart,columnEnd:r.columnEnd,statementCount:l};if(c.flows.push(m),n.flowCount+=1,l>=i.minFlowStatements){const t=makeFingerprint(e);increment(o.controlMap,`${t}|${e.kind}`,{...m,hash:t})}}ts.forEachChild(e,x)};return ts.forEachChild(t,x),c.emptyCatches=u,c.switchesWithoutDefault=p,c.anyCount=d,c.magicNumbers=f,c.typeAssertionEscapes={asAny:h,doubleAssertion:S,nonNull:y},analyzeAsyncPatterns(t,c),collectFileProfiles(t,a,c),c}function analyzeAsyncPatterns(t,e){const n=[],i=[];for(const o of e.functions){if(0===o.awaits)continue;const e=t.getPositionOfLineAndCharacter(Math.max(0,o.lineStart-1),0);let s;const l=n=>{if(!s){if(isFunctionLike(n)&&n.getStart(t)>=e){if(getLineAndCharacter(t,n).lineStart===o.lineStart)return void(s=n)}ts.forEachChild(n,l)}};if(ts.forEachChild(t,l),!s)continue;const r=s.modifiers?.some(t=>t.kind===ts.SyntaxKind.AsyncKeyword);if(!r)continue;let a=0,c=!1,m=!1;const u=t=>{ts.isAwaitExpression(t)&&a++,ts.isTryStatement(t)&&(c=!0),ts.isCallExpression(t)&&ts.isPropertyAccessExpression(t.expression)&&"catch"===t.expression.name.text&&(m=!0),isFunctionLike(t)&&t!==s||ts.forEachChild(t,u)};ts.forEachChild(s,u),0===a?n.push({name:o.name,lineStart:o.lineStart,lineEnd:o.lineEnd}):c||m||i.push({name:o.name,awaitCount:a,lineStart:o.lineStart,lineEnd:o.lineEnd})}e.asyncWithoutAwait=n,e.unprotectedAsync=i}function collectFileProfiles(t,e,n){if(collectSecurityData(t,e,n),isTestFile(e)||(collectInputSourceProfile(t,e,n),collectMessageChains(t,e,n)),collectPerformanceData(t,e,n),isTestFile(e)&&collectTestProfile(t,e,n),!isTestFile(e)){const i=collectTopLevelEffects(t,e);i.length>0&&(n.topLevelEffects=i);const o=collectPrototypePollutionSites(t);o.length>0&&(n.prototypePollutionSites=o)}}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import*as ts from"typescript";import{getLineAndCharacter}from"../common/utils.js";const MIN_CHAIN_DEPTH=4;function measureChain(s,e){let t=0,n=s;for(;ts.isPropertyAccessExpression(n)||ts.isElementAccessExpression(n);)t++,n=n.expression;if(t<4)return null;const i=s.parent;return ts.isPropertyAccessExpression(i)||ts.isElementAccessExpression(i)?null:{text:s.getText(e),depth:t}}export function collectMessageChains(s,e,t){const n=[],i=e=>{if(ts.isPropertyAccessExpression(e)||ts.isElementAccessExpression(e)){const t=measureChain(e,s);if(t){const i=getLineAndCharacter(s,e);n.push({chain:t.text.slice(0,80),depth:t.depth,lineStart:i.lineStart,lineEnd:i.lineEnd})}}ts.forEachChild(e,i)};ts.forEachChild(s,i),n.length>0&&(t.messageChains=n)}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import*as ts from"typescript";import{isFunctionLike}from"../ast/helpers.js";import{getLineAndCharacter}from"../common/utils.js";const SYNC_IO_TOP_LEVEL=new Set(["readFileSync","writeFileSync","existsSync","mkdirSync","readdirSync","statSync","lstatSync","unlinkSync","rmdirSync","renameSync","copyFileSync","accessSync","appendFileSync","chmodSync","chownSync","openSync","closeSync"]),EXEC_SYNC_TOP_LEVEL=new Set(["execSync","execFileSync","spawnSync"]);export function collectTopLevelEffects(e,i){const n=[];for(const i of e.statements)if(ts.isImportDeclaration(i)){if(!i.importClause){const t=i.moduleSpecifier,s=ts.isStringLiteral(t)?t.text:"<unknown>",r=getLineAndCharacter(e,i);n.push({kind:"side-effect-import",lineStart:r.lineStart,lineEnd:r.lineEnd,detail:`import '${s}'`,weight:3,confidence:"medium"})}}else if(!ts.isExportDeclaration(i)&&!ts.isExportAssignment(i)&&!(ts.isTypeAliasDeclaration(i)||ts.isInterfaceDeclaration(i)||ts.isEnumDeclaration(i)||ts.isModuleDeclaration(i)||isFunctionLike(i)||ts.isFunctionDeclaration(i)||ts.isClassDeclaration(i)))if(ts.isVariableStatement(i))for(const t of i.declarationList.declarations)t.initializer&&scanExpressionForEffects(t.initializer,e,n);else ts.isExpressionStatement(i)?scanExpressionForEffects(i.expression,e,n):(ts.isIfStatement(i)||ts.isForStatement(i)||ts.isWhileStatement(i)||ts.isDoStatement(i)||ts.isForOfStatement(i)||ts.isForInStatement(i)||ts.isSwitchStatement(i)||ts.isTryStatement(i))&&scanNodeForEffects(i,e,n);return n}function scanExpressionForEffects(e,i,n){if(ts.isAwaitExpression(e)){const t=getLineAndCharacter(i,e);return void n.push({kind:"top-level-await",lineStart:t.lineStart,lineEnd:t.lineEnd,detail:"top-level await",weight:4,confidence:"high"})}if(ts.isCallExpression(e))classifyCall(e,i,n);else{if(ts.isNewExpression(e)&&"Function"===e.expression.getText(i)){const t=getLineAndCharacter(i,e);return void n.push({kind:"eval",lineStart:t.lineStart,lineEnd:t.lineEnd,detail:"new Function()",weight:8,confidence:"high"})}ts.isBinaryExpression(e)&&e.operatorToken.kind===ts.SyntaxKind.EqualsToken&&ts.isCallExpression(e.right)&&classifyCall(e.right,i,n)}}function classifyCall(e,i,n){const t=e.expression.getText(i),s=getLineAndCharacter(i,e);if("eval"!==t&&"Function"!==t)if("setInterval"!==t&&"setTimeout"!==t){if(ts.isPropertyAccessExpression(e.expression)){const r=e.expression.name.getText(i),a=e.expression.expression.getText(i);if(EXEC_SYNC_TOP_LEVEL.has(r)||EXEC_SYNC_TOP_LEVEL.has(t))return void n.push({kind:"exec-sync",lineStart:s.lineStart,lineEnd:s.lineEnd,detail:t,weight:8,confidence:"high"});if(SYNC_IO_TOP_LEVEL.has(r))return void n.push({kind:"sync-io",lineStart:s.lineStart,lineEnd:s.lineEnd,detail:t,weight:5,confidence:"high"});if("process"===a&&("on"===r||"once"===r||"addListener"===r))return void n.push({kind:"process-handler",lineStart:s.lineStart,lineEnd:s.lineEnd,detail:`${t}()`,weight:4,confidence:"high"});if("addEventListener"===r||"on"===r||"addListener"===r)return void n.push({kind:"listener",lineStart:s.lineStart,lineEnd:s.lineEnd,detail:`${t}()`,weight:4,confidence:"medium"})}(ts.isCallExpression(e.expression)||"import"===t)&&(t.startsWith("import(")||ts.isCallExpression(e)&&e.expression.kind===ts.SyntaxKind.ImportKeyword)&&n.push({kind:"dynamic-import",lineStart:s.lineStart,lineEnd:s.lineEnd,detail:"dynamic import()",weight:3,confidence:"medium"})}else n.push({kind:"timer",lineStart:s.lineStart,lineEnd:s.lineEnd,detail:`${t}()`,weight:4,confidence:"high"});else n.push({kind:"eval",lineStart:s.lineStart,lineEnd:s.lineEnd,detail:`${t}()`,weight:8,confidence:"high"})}function scanNodeForEffects(e,i,n){if(!isFunctionLike(e)&&!ts.isClassDeclaration(e))if(ts.isCallExpression(e))classifyCall(e,i,n);else{if(ts.isAwaitExpression(e)){const t=getLineAndCharacter(i,e);return void n.push({kind:"top-level-await",lineStart:t.lineStart,lineEnd:t.lineEnd,detail:"top-level await",weight:4,confidence:"high"})}if(ts.isNewExpression(e)&&"Function"===e.expression.getText(i)){const t=getLineAndCharacter(i,e);return void n.push({kind:"eval",lineStart:t.lineStart,lineEnd:t.lineEnd,detail:"new Function()",weight:8,confidence:"high"})}ts.forEachChild(e,e=>scanNodeForEffects(e,i,n))}}export function findParentBlock(e){let i=e.parent;for(;i;){if(ts.isBlock(i)||ts.isSourceFile(i))return i;i=i.parent}return null}export function blockContainsCall(e,i,n){let t=!1;const s=e=>{t||(ts.isCallExpression(e)&&e.expression.getText(i)===n?t=!0:ts.forEachChild(e,s))};return ts.forEachChild(e,s),t}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import*as ts from"typescript";import{getFunctionName,isFunctionLike}from"../ast/helpers.js";import{getLineAndCharacter}from"../common/utils.js";const HIGH_CONFIDENCE_PARAM=/^(req|request|body|rawBody|formData|payload|query|headers|params)$/i,MEDIUM_CONFIDENCE_PARAM=/^(input|event|message)$/i,SOURCE_PARAM_PATTERNS=/^(req|request|body|input|payload|data|params|query|headers|event|message|ctx|context|args|rawBody|formData)/i;function getParamConfidence(e){let t=!1;for(const n of e){if(HIGH_CONFIDENCE_PARAM.test(n))return"high";MEDIUM_CONFIDENCE_PARAM.test(n)&&(t=!0)}return t?"medium":"low"}const SINK_CALL_PATTERNS=[{pattern:/^eval$/,kind:"eval"},{pattern:/^Function$/,kind:"eval"},{pattern:/\.exec(Sync)?$/,kind:"exec"},{pattern:/^child_process\.(exec|spawn|fork)/,kind:"exec"},{pattern:/^execSync$|^spawnSync$/,kind:"exec"},{pattern:/^cp\.exec$|^cp\.spawn$/,kind:"exec"},{pattern:/\.innerHTML$|\.outerHTML$/,kind:"innerHTML"},{pattern:/dangerouslySetInnerHTML/,kind:"innerHTML"},{pattern:/\.query$|\.execute$/,kind:"sql"},{pattern:/\.redirect$/,kind:"redirect"},{pattern:/\.send$|\.json$|\.write$/,kind:"response"},{pattern:/fs\.(writeFile|appendFile)/,kind:"fs-write"},{pattern:/writeFileSync|appendFileSync/,kind:"fs-write"},{pattern:/fs\.(readFile|readFileSync|createReadStream)/,kind:"fs-read"},{pattern:/readFileSync|readFile/,kind:"fs-read"},{pattern:/path\.(resolve|join)/,kind:"path-resolve"},{pattern:/^fetch$/,kind:"ssrf"},{pattern:/^(http|https)\.(request|get)/,kind:"ssrf"},{pattern:/axios\.(get|post|put|delete|request)/,kind:"ssrf"}],SCHEMA_VALIDATOR_PATTERNS=/\.(validate|parse|safeParse|parseAsync|check|verify)\s*\(/,VALIDATOR_LIB_PATTERNS=/^(z|zod|Joi|yup|ajv|validator|superstruct|io-ts)\./;export function collectInputSourceProfile(e,t,n){const s=[],r=t=>{if(!isFunctionLike(t))return void ts.forEachChild(t,r);const n=t,i=n.parameters,a=[];for(const t of i){const n=t.name.getText(e);SOURCE_PARAM_PATTERNS.test(n)&&a.push(n)}if(0===a.length)return void ts.forEachChild(t,r);const o=n.body;if(!o)return void ts.forEachChild(t,r);const c=new Set;let d=!1;const p=[],f=new Set(a),l=n=>{if(!isFunctionLike(n)||n===t){if(ts.isCallExpression(n)){const t=n.expression.getText(e);for(const e of SINK_CALL_PATTERNS)if(e.pattern.test(t)){c.add(e.kind);break}(SCHEMA_VALIDATOR_PATTERNS.test(t)||VALIDATOR_LIB_PATTERNS.test(t))&&(d=!0);for(const s of n.arguments){const r=s.getText(e);for(const s of f)if(r===s||r.startsWith(s+".")||r.startsWith(s+"[")){const s=getLineAndCharacter(e,n);p.push({callee:t,lineStart:s.lineStart});break}}}if(ts.isTypeOfExpression(n)){const t=n.expression.getText(e);f.has(t)&&(d=!0)}if(ts.isPrefixUnaryExpression(n)&&n.operator===ts.SyntaxKind.ExclamationToken){const t=n.operand.getText(e);f.has(t)&&(d=!0)}if(ts.isIfStatement(n)||ts.isConditionalExpression(n)){const t=(ts.isIfStatement(n)?n.expression:n.condition).getText(e);for(const e of f)if(t.includes(e)){d=!0;break}}if(ts.isCallExpression(n)&&n.expression.getText(e).endsWith("instanceof")&&(d=!0),ts.isBinaryExpression(n)&&n.operatorToken.kind===ts.SyntaxKind.InstanceOfKeyword){const t=n.left.getText(e);f.has(t)&&(d=!0)}ts.forEachChild(n,l)}};if(ts.forEachChild(o,l),ts.isTemplateExpression(o)||ts.isBlock(o)){const t=o.getText(e);for(const e of f)if(t.includes(e+"?.")){d=!0;break}}const u=getLineAndCharacter(e,t),h=getFunctionName(t,e);s.push({functionName:h,lineStart:u.lineStart,lineEnd:u.lineEnd,sourceParams:a,hasSinkInBody:c.size>0,sinkKinds:[...c],hasValidation:d,callsWithInputArgs:p,paramConfidence:getParamConfidence(a)}),ts.forEachChild(t,r)};ts.forEachChild(e,r),n.inputSources=s}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import*as ts from"typescript";import{blockContainsCall,findParentBlock}from"./effects.js";import{isFunctionLike}from"../ast/helpers.js";import{getLineAndCharacter}from"../common/utils.js";const SYNC_IO_METHODS=new Set(["readFileSync","writeFileSync","existsSync","mkdirSync","readdirSync","statSync","lstatSync","unlinkSync","rmdirSync","renameSync","copyFileSync","accessSync","appendFileSync","chmodSync","chownSync","openSync","closeSync","execSync","execFileSync","spawnSync"]);export function collectPerformanceData(e,n,t){const i=[],s=[],r=[],a=[],c=[],l=t=>{if(ts.isAwaitExpression(t)&&(e=>{let n=e.parent;for(;n;){if(ts.isForStatement(n)||ts.isWhileStatement(n)||ts.isDoStatement(n)||ts.isForOfStatement(n)||ts.isForInStatement(n))return!0;if(isFunctionLike(n))return!1;n=n.parent}return!1})(t)){const s=getLineAndCharacter(e,t);i.push({file:n,lineStart:s.lineStart,lineEnd:s.lineEnd})}if(ts.isCallExpression(t)&&ts.isPropertyAccessExpression(t.expression)){const i=t.expression.name.getText(e);if(SYNC_IO_METHODS.has(i)){const n=getLineAndCharacter(e,t);s.push({name:i,lineStart:n.lineStart,lineEnd:n.lineEnd})}if("addEventListener"===i||"on"===i||"addListener"===i){const i=getLineAndCharacter(e,t);a.push({file:n,lineStart:i.lineStart,lineEnd:i.lineEnd})}if("removeEventListener"===i||"off"===i||"removeListener"===i){const i=getLineAndCharacter(e,t);c.push({file:n,lineStart:i.lineStart,lineEnd:i.lineEnd})}}if(ts.isCallExpression(t)){const n=t.expression.getText(e);if("setInterval"===n||"setTimeout"===n){const i=getLineAndCharacter(e,t),s="setInterval"===n?"clearInterval":"clearTimeout",a=findParentBlock(t),c=!!a&&blockContainsCall(a,e,s);r.push({kind:n,lineStart:i.lineStart,lineEnd:i.lineEnd,hasCleanup:c})}}ts.forEachChild(t,l)};ts.forEachChild(e,l),t.awaitInLoopLocations=i,t.syncIoCalls=s,t.timerCalls=r,t.listenerRegistrations=a,t.listenerRemovals=c}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import*as ts from"typescript";import{findParentBlock}from"./effects.js";import{isFunctionLike}from"../ast/helpers.js";import{getLineAndCharacter}from"../common/utils.js";const DEEP_MERGE_NAMES=new Set(["merge","deepMerge","deepAssign","extend","deepExtend","defaults","defaultsDeep","assign","mixin"]);function isKeyFromInternalIteration(e,t){const n=e.argumentExpression;if(!n||!ts.isIdentifier(n))return!1;const r=n.getText(t);let s=e.parent;for(;s;){if(ts.isForOfStatement(s)||ts.isForInStatement(s)){const e=s.initializer;if(e){if(e.getText(t).includes(r)){const e=s.expression.getText(t);if(/Object\.(keys|values|entries|getOwnPropertyNames)\(/.test(e)||/\.keys\(\)|\.values\(\)|\.entries\(\)/.test(e)||/Array\.from\(/.test(e))return!0}}}if(isFunctionLike(s))break;s=s.parent}return!1}function hasProtoKeyGuard(e,t){const n=findParentBlock(e);if(!n)return!1;const r=n.getText(t);return/__proto__|constructor|prototype/.test(r)&&(r.includes("===")||r.includes("!==")||r.includes("includes(")||r.includes("hasOwnProperty"))}function isTargetSafeObject(e,t){const n=e.expression.getText(t);let r=e.parent;for(;r;){if(ts.isBlock(r)||ts.isSourceFile(r)){const e=r.getText(t),s=new RegExp(`${n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}\\s*=\\s*Object\\.create\\(null\\)`),i=new RegExp(`${n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}\\s*=\\s*new\\s+(Map|Set)\\b`);if(s.test(e)||i.test(e))return!0;break}r=r.parent}return!1}export function collectPrototypePollutionSites(e){const t=[],n=r=>{if(ts.isCallExpression(r)){const n=r.expression.getText(e);if("Object.assign"===n&&r.arguments.length>=2){const n=getLineAndCharacter(e,r);t.push({kind:"object-assign",detail:"Object.assign() merges properties without __proto__ guard",lineStart:n.lineStart,lineEnd:n.lineEnd,guarded:!1})}const s=n.split(".").pop()||"";if(DEEP_MERGE_NAMES.has(s)&&r.arguments.length>=1){const n=getLineAndCharacter(e,r);t.push({kind:"deep-merge",detail:`${s}() deep-merges without prototype guard`,lineStart:n.lineStart,lineEnd:n.lineEnd,guarded:!1})}}if(ts.isElementAccessExpression(r)&&r.argumentExpression&&!ts.isStringLiteral(r.argumentExpression)&&!ts.isNumericLiteral(r.argumentExpression)&&r.parent&&ts.isBinaryExpression(r.parent)&&r.parent.operatorToken.kind===ts.SyntaxKind.EqualsToken&&r.parent.left===r){const n=isKeyFromInternalIteration(r,e)||hasProtoKeyGuard(r,e)||isTargetSafeObject(r,e),s=getLineAndCharacter(e,r);t.push({kind:"computed-property-write",detail:`Dynamic bracket assignment: ${r.getText(e).slice(0,40)}`,lineStart:s.lineStart,lineEnd:s.lineEnd,guarded:n})}ts.forEachChild(r,n)};return ts.forEachChild(e,n),t}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import*as ts from"typescript";import{getLineAndCharacter}from"../common/utils.js";const SENSITIVE_LOG_PATTERNS=[/password/i,/passwd/i,/\bsecret\b/i,/\btoken\b/i,/\bauth\b/i,/credential/i,/credit.?card/i,/\bssn\b/i,/social.?security/i,/api[_-]?key/i,/private[_-]?key/i,/access[_-]?key/i,/\bsession\b/i],CONSOLE_LOG_METHODS=new Set(["log","debug","trace","info","warn","error","dir","table"]),SECRET_PATTERNS=[/password\s*[:=]\s*['"`]/i,/api[_-]?key\s*[:=]\s*['"`]/i,/secret\s*[:=]\s*['"`]/i,/token\s*[:=]\s*['"`]/i,/-----BEGIN.*KEY/,/private[_-]?key\s*[:=]\s*['"`]/i,/auth[_-]?token\s*[:=]\s*['"`]/i],SQL_KEYWORDS=/\b(SELECT|INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|TRUNCATE)\b/i,PLACEHOLDER_PATTERN=/^(YOUR_|REPLACE_ME|<[a-z_-]+>|\$\{|{{)/i,UUID_PATTERN=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;function isInsideRegexLiteral(e){let t=e.parent;for(;t;){if(ts.isRegularExpressionLiteral(t))return!0;if(ts.isNewExpression(t)&&"RegExp"===t.expression.getText(e.getSourceFile()))return!0;t=t.parent}return!1}function isPlaceholderOrUuid(e){return PLACEHOLDER_PATTERN.test(e)||UUID_PATTERN.test(e)}const METADATA_PROP_NAMES=new Set(["suggestedFix","strategy","steps","reason","impact","expectedResult","title"]);function isInsideMetadataProperty(e){let t=e.parent;for(;t;){if(ts.isPropertyAssignment(t)&&ts.isIdentifier(t.name)&&METADATA_PROP_NAMES.has(t.name.text))return!0;t=t.parent}return!1}function computeShannonEntropy(e){const t=new Map;for(const n of e)t.set(n,(t.get(n)||0)+1);let n=0;for(const i of t.values()){const t=i/e.length;t>0&&(n-=t*Math.log2(t))}return n}export function collectSecurityData(e,t,n){const i=[],s=[],r=[],a=[],o=[],l=n=>{if(ts.isDebuggerStatement(n)){const t=getLineAndCharacter(e,n);a.push({method:"debugger",lineStart:t.lineStart,lineEnd:t.lineEnd,hasSensitiveArg:!1})}if(ts.isCallExpression(n)){const t=n.expression;if(ts.isPropertyAccessExpression(t)){const i=t.expression.getText(e),s=t.name.getText(e);if("console"===i&&CONSOLE_LOG_METHODS.has(s)){const t=getLineAndCharacter(e,n),i=n.arguments.map(t=>t.getText(e)).join(" "),r=SENSITIVE_LOG_PATTERNS.some(e=>e.test(i));a.push({method:s,lineStart:t.lineStart,lineEnd:t.lineEnd,hasSensitiveArg:r,argSnippet:i.slice(0,80)})}}}if(ts.isCallExpression(n)){const r=n.expression.getText(e);if("eval"===r||"Function"===r){const s=getLineAndCharacter(e,n);i.push({file:t,lineStart:s.lineStart,lineEnd:s.lineEnd})}if("new Function"===r){const s=getLineAndCharacter(e,n);i.push({file:t,lineStart:s.lineStart,lineEnd:s.lineEnd})}if(("setTimeout"===r||"setInterval"===r)&&n.arguments.length>0){const s=n.arguments[0];if(ts.isStringLiteral(s)||ts.isNoSubstitutionTemplateLiteral(s)){const s=getLineAndCharacter(e,n);i.push({file:t,lineStart:s.lineStart,lineEnd:s.lineEnd})}}if("document.write"===r||"document.writeln"===r){const i=getLineAndCharacter(e,n);s.push({file:t,lineStart:i.lineStart,lineEnd:i.lineEnd})}}if(ts.isNewExpression(n)&&"Function"===n.expression.getText(e)){const s=getLineAndCharacter(e,n);i.push({file:t,lineStart:s.lineStart,lineEnd:s.lineEnd})}if(ts.isBinaryExpression(n)&&n.operatorToken.kind===ts.SyntaxKind.EqualsToken&&ts.isPropertyAccessExpression(n.left)){const i=n.left.name.getText(e);if("innerHTML"===i||"outerHTML"===i){const i=getLineAndCharacter(e,n);s.push({file:t,lineStart:i.lineStart,lineEnd:i.lineEnd})}}if(ts.isJsxAttribute(n)&&"dangerouslySetInnerHTML"===n.name.getText(e)){const i=getLineAndCharacter(e,n);s.push({file:t,lineStart:i.lineStart,lineEnd:i.lineEnd})}if((ts.isStringLiteral(n)||ts.isNoSubstitutionTemplateLiteral(n))&&!isInsideMetadataProperty(n)&&!isInsideRegexLiteral(n)){const t=n.text;if(!isPlaceholderOrUuid(t)){for(const i of SECRET_PATTERNS)if(i.test(t)){const i=getLineAndCharacter(e,n);r.push({lineStart:i.lineStart,lineEnd:i.lineEnd,kind:"hardcoded-secret",snippet:t.slice(0,40),context:"literal"});break}if(t.length>=20&&computeShannonEntropy(t)>4.5){const t=getLineAndCharacter(e,n);r.push({lineStart:t.lineStart,lineEnd:t.lineEnd,kind:"hardcoded-secret",context:"literal"})}}}if(ts.isRegularExpressionLiteral(n)){const t=n.getText(e);for(const i of SECRET_PATTERNS)if(i.test(t)){const i=getLineAndCharacter(e,n);r.push({lineStart:i.lineStart,lineEnd:i.lineEnd,kind:"hardcoded-secret",snippet:t.slice(0,40),context:"regex-definition"});break}}if(ts.isTemplateExpression(n)&&!isInsideMetadataProperty(n)){const t=n.getText(e);if(SQL_KEYWORDS.test(t)&&n.templateSpans.length>0){const i=getLineAndCharacter(e,n);r.push({lineStart:i.lineStart,lineEnd:i.lineEnd,kind:"sql-injection",snippet:t.slice(0,60)})}}if(ts.isRegularExpressionLiteral(n)){const t=n.text,i=getLineAndCharacter(e,n);o.push({lineStart:i.lineStart,lineEnd:i.lineEnd,pattern:t})}ts.forEachChild(n,l)};ts.forEachChild(e,l),n.evalUsages=i,n.unsafeHtmlAssignments=s,n.suspiciousStrings=r,n.consoleLogs=a,n.regexLiterals=o}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import*as ts from"typescript";import{getLineAndCharacter}from"../common/utils.js";const ASSERTION_PATTERNS=new Set(["expect","assert","should"]),MOCK_PATTERNS=["jest.mock","vi.mock","sinon.stub","jest.spyOn","vi.spyOn","sinon.mock"],RESTORE_PATTERNS=new Set(["jest.restoreAllMocks","vi.restoreAllMocks"]),SETUP_PATTERNS=new Set(["beforeAll","beforeEach","afterAll","afterEach"]),FOCUSED_PATTERNS=new Set(["it.only","test.only","describe.only","it.skip","test.skip","describe.skip","it.todo","test.todo"]),USE_FAKE_TIMER_PATTERNS=new Set(["jest.useFakeTimers","vi.useFakeTimers"]),USE_REAL_TIMER_PATTERNS=new Set(["jest.useRealTimers","vi.useRealTimers"]);function getSpyOrStubKind(e,t){if(!ts.isPropertyAccessExpression(e.expression))return;const n=e.expression.name.getText(t),s=e.expression.expression.getText(t);return"jest"!==s&&"vi"!==s||"spyOn"!==n?"sinon"!==s||"stub"!==n&&"mock"!==n?void 0:"stub":"spy"}function getMockControlTarget(e,t){let n=e;for(;n.parent;){const e=n.parent;if(ts.isVariableDeclaration(e)&&e.initializer===n&&ts.isIdentifier(e.name))return e.name.getText(t);if(ts.isBinaryExpression(e)&&e.operatorToken.kind===ts.SyntaxKind.EqualsToken&&e.right===n)return e.left.getText(t).trim();n=e}}function getMockRestoreTarget(e,t){if(ts.isPropertyAccessExpression(e.expression))return e.expression.expression.getText(t).trim()}export function collectTestProfile(e,t,n){const s=[],i=[],r=[],o=[],l=[],a=[],c=[],E=[],d=(n,S,T)=>{if(ts.isCallExpression(n)){const o=n.expression.getText(e);if(FOCUSED_PATTERNS.has(o)){const t=getLineAndCharacter(e,n);l.push({kind:o,lineStart:t.lineStart,lineEnd:t.lineEnd})}if(("it"===o||"test"===o||"it.only"===o||"test.only"===o)&&n.arguments.length>=2){const t=n.arguments[0],i=ts.isStringLiteral(t)?t.text:o,r=n.arguments[1],l=getLineAndCharacter(e,n);let a=0;const c=t=>{if(ts.isCallExpression(t)){const n=t.expression.getText(e);(ASSERTION_PATTERNS.has(n.split(".")[0])||n.includes(".to.")||n.includes(".should"))&&a++}ts.forEachChild(t,c)};return ts.forEachChild(r,c),s.push({name:i,lineStart:l.lineStart,lineEnd:l.lineEnd,assertionCount:a}),void ts.forEachChild(n,e=>d(e,S,!0))}if(MOCK_PATTERNS.some(e=>o===e||o.startsWith(e+"("))){const s=getLineAndCharacter(e,n);i.push({file:t,lineStart:s.lineStart,lineEnd:s.lineEnd});const r=getSpyOrStubKind(n,e);r&&E.push({kind:r,file:t,lineStart:s.lineStart,lineEnd:s.lineEnd,target:getMockControlTarget(n,e)})}if(USE_FAKE_TIMER_PATTERNS.has(o)){const t=getLineAndCharacter(e,n);a.push({kind:o,lineStart:t.lineStart,lineEnd:t.lineEnd})}if(USE_REAL_TIMER_PATTERNS.has(o)){const t=getLineAndCharacter(e,n);a.push({kind:o,lineStart:t.lineStart,lineEnd:t.lineEnd})}if(RESTORE_PATTERNS.has(o)){const s=getLineAndCharacter(e,n);c.push({kind:"restoreAll",file:t,lineStart:s.lineStart,lineEnd:s.lineEnd})}else if(o.endsWith(".mockRestore")){const s=getLineAndCharacter(e,n);c.push({kind:"restore",file:t,lineStart:s.lineStart,lineEnd:s.lineEnd,target:getMockRestoreTarget(n,e)})}if(SETUP_PATTERNS.has(o)){const t=getLineAndCharacter(e,n);r.push({kind:o,lineStart:t.lineStart})}if("describe"===o||"describe.only"===o)return void ts.forEachChild(n,e=>d(e,!0,T))}if(S&&!T&&ts.isVariableStatement(n)){const s=n.declarationList;if(s.flags&ts.NodeFlags.Let||!(s.flags&ts.NodeFlags.Const)){const s=getLineAndCharacter(e,n);o.push({file:t,lineStart:s.lineStart,lineEnd:s.lineEnd})}}ts.forEachChild(n,e=>d(e,S,T))};ts.forEachChild(e,e=>d(e,!1,!1)),n.testProfile={testBlocks:s,mockCalls:i,setupCalls:r,mutableStateDecls:o,focusedCalls:l,timerControls:a,mockRestores:c,spyOrStubCalls:E}}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import path from"node:path";import{fileURLToPath}from"node:url";export function isDirectRun(o,r=process.argv[1]){return!!r&&fileURLToPath(o)===path.resolve(r)}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import crypto from"node:crypto";import fs from"node:fs";import path from"node:path";import*as ts from"typescript";import{IMPORT_RESOLVE_EXTS}from"../types/index.js";export function canonicalScriptKind(t){switch(t){case".tsx":return ts.ScriptKind.TSX;case".jsx":return ts.ScriptKind.JSX;case".js":case".mjs":case".cjs":return ts.ScriptKind.JS;default:return ts.ScriptKind.TS}}export function hashString(t){return crypto.createHash("sha1").update(t).digest("hex").slice(0,16)}export function normalizeNodeKind(t){switch(t){case ts.SyntaxKind.Identifier:return"ID";case ts.SyntaxKind.StringLiteral:case ts.SyntaxKind.NoSubstitutionTemplateLiteral:case ts.SyntaxKind.TemplateMiddle:case ts.SyntaxKind.TemplateHead:return"STR";case ts.SyntaxKind.NumericLiteral:return"NUM";case ts.SyntaxKind.BigIntLiteral:return"BIGINT";case ts.SyntaxKind.TrueKeyword:case ts.SyntaxKind.FalseKeyword:return"BOOL";case ts.SyntaxKind.NullKeyword:return"NULL";default:return ts.SyntaxKind[t]||"UNKNOWN"}}export function makeFingerprint(t,e=new WeakMap){if(e.has(t))return e.get(t);const n=[],r=t=>{n.push(normalizeNodeKind(t.kind)),ts.forEachChild(t,r)};r(t);const s=hashString(n.join("|"));return e.set(t,s),s}export function makeTreeSitterFingerprint(t){const e=[],n=t=>{e.push(t.type);for(const e of t.children)n(e)};return n(t),hashString(e.join("|"))}export function getLineAndCharacter(t,e){const n=t.getLineAndCharacterOfPosition(e.getStart(t)),r=t.getLineAndCharacterOfPosition(e.getEnd());return{lineStart:n.line+1,lineEnd:r.line+1,columnStart:n.character+1,columnEnd:r.character+1}}export function buildNodeTree(t,e,n,r,s=new WeakSet){if(!t||r.size<=0)return null;r.size-=1;const i=getLineAndCharacter(e,t),o={kind:ts.SyntaxKind[t.kind]||"UNKNOWN",startLine:i.lineStart,endLine:i.lineEnd,children:[]};return n<=0||s.has(t)?(o.truncated=!0,o):(s.add(t),ts.forEachChild(t,t=>{if(r.size<=0)return;const i=buildNodeTree(t,e,n-1,r,s);i&&o.children.push(i)}),o)}export function buildTreeSitterTree(t,e,n,r,s=new WeakSet){if(!t||r.size<=0)return null;r.size-=1;const i={kind:t.type,startLine:t.startPosition.row+1,endLine:t.endPosition.row+1,children:[]};if(n<=0)return i.truncated=!0,i;if(s.has(t))return i.truncated=!0,i;s.add(t);for(const o of t.children){if(r.size<=0)break;const t=buildTreeSitterTree(o,e,n-1,r,s);t&&i.children.push(t)}return i}export function renderNodeText(t,e=0){const n=" ".repeat(e),r=t.startLine===t.endLine?`${t.startLine}`:`${t.startLine}:${t.endLine}`,s=t.truncated?" ...":"";let i=`${n}${t.kind}[${r}]${s}\n`;for(const n of t.children)i+=renderNodeText(n,e+1);return i}export function renderTreesText(t,e){const n=[`# AST Trees — ${e}`,""];for(const e of t)n.push(`## ${e.package} — ${e.file}`),n.push(renderNodeText(e.tree));return n.join("\n")}export function isTestFile(t){return/(?:^|[\\/])(?:__tests__|__test__|tests)(?:[\\/]|$)/.test(t)||/(?:\.test|_test|\.spec)\.(?:ts|tsx|js|jsx|mjs|cjs)$/.test(t)}export function toRepoPath(t,e){return path.relative(e,t).replace(/\\/g,"/")}export function normalizeDependencyValue(t){return path.normalize(t).replace(/\\/g,"/")}export function addToMapSet(t,e,n){t.has(e)||t.set(e,new Set),t.get(e).add(n)}export function isRelativeImport(t){return t.startsWith("./")||t.startsWith("../")||t.startsWith(".\\")||t.startsWith("..\\")}export function resolveImportTarget(t,e){const n=e.replace(/[?#].*$/,""),r=path.resolve(t,n),s=[],i=path.extname(r),o={".js":[".ts",".tsx"],".jsx":[".tsx"],".mjs":[".ts",".tsx"],".cjs":[".ts",".tsx"]};if(i){s.push(r);const t=o[i];if(t){const e=r.slice(0,-i.length);for(const n of t){const t=`${e}${n}`;s.push(t)}}}else{for(const t of IMPORT_RESOLVE_EXTS)s.push(`${r}${t}`);for(const t of IMPORT_RESOLVE_EXTS)s.push(path.join(r,`index${t}`))}for(const t of s)if(fs.existsSync(t))return t;return null}export function increment(t,e,n){t.has(e)||t.set(e,[]),t.get(e).push(n)}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import*as ts from"typescript";import{canAddFinding}from"./shared.js";import{isTestFile}from"../common/utils.js";export function detectDuplicateFunctionBodies(e){const t=[];for(const i of e){const e=i.locations[0],n=`Same ${i.kind} body shape appears in ${i.occurrences} places (${i.filesCount} file${i.filesCount>1?"s":""}).`,s=i.occurrences>=6?"high":i.occurrences>=3?"medium":"low";if(!canAddFinding(t))break;t.push({...e,severity:s,category:"duplicate-function-body",title:`Deduplicate function body: ${i.signature}`,reason:n,files:i.locations.map(e=>`${e.file}:${e.lineStart}-${e.lineEnd}`),suggestedFix:{strategy:"Create a shared helper function once and replace duplicate call sites.",steps:["Extract one function to a dedicated utility module.","Keep behavior unchanged by passing function-specific differences as params.","Replace duplicated blocks with calls to the shared helper.","Add/extend tests around each entry point that previously used duplicates."]},impact:"Lower maintenance cost and reduce regression risk when behavior changes.",tags:["duplication","maintainability","dryness"],lspHints:[{tool:"lspGotoDefinition",symbolName:i.signature,lineHint:e.lineStart,file:e.file,expectedResult:"navigate to one instance to compare implementations side-by-side"}]})}return t}export function detectDuplicateFlowStructures(e,t){const i=[];for(const n of e){if(n.occurrences<t)continue;const e=n.locations[0],s=`${n.kind} structure appears ${n.occurrences} times across ${n.filesCount} file(s).`,a=n.occurrences>=10?"high":"medium";if(!canAddFinding(i))break;i.push({...e,severity:a,category:"duplicate-flow-structure",title:`Extract repeated flow structure: ${n.kind}`,reason:s,files:n.locations.map(e=>`${e.file}:${e.lineStart}-${e.lineEnd}`),suggestedFix:{strategy:"Extract a reusable flow helper around the repeated structure.",steps:["Create one clear helper that accepts varying inputs as parameters.","Call helper from each repeated site.","Keep variable names aligned and add local adapter logic where needed.","Document expected invariants for the shared flow."]},impact:"Reduces duplicate control branches and normalizes edge-case handling.",tags:["duplication","control-flow","dryness"]})}return i}export function detectFunctionOptimization(e,t){const i=[];for(const n of e)for(const e of n.functions){const n=[];if(e.complexity>=t&&n.push(`Cyclomatic-like complexity is high (>=${t}).`),e.maxBranchDepth>=7&&n.push("Branch depth is very deep and hard to reason about."),e.maxLoopDepth>=4&&n.push("Nested loops are high and likely expensive."),e.statementCount>=24&&n.push("Function body is large and may be doing multiple responsibilities."),0===n.length)continue;const s=e.complexity>=t||e.maxBranchDepth>=7||e.maxLoopDepth>=4;i.push({...e,severity:s?"high":"medium",category:"function-optimization",title:`Potential function refactor: ${e.name}`,reason:n.join(" "),files:[`${e.file}:${e.lineStart}-${e.lineEnd}`],suggestedFix:{strategy:"Refactor for readability and testability.",steps:["Split into smaller subroutines with single responsibilities.","Convert deeply nested branches into guard clauses when safe.","Replace loops with intent-specific helpers if one loop owns most lines.","Add unit coverage for each extracted piece before deleting old logic."]},impact:"Cleaner flow, easier review and safer refactors.",tags:["complexity","readability","refactor"],lspHints:[{tool:"lspCallHierarchy",symbolName:e.name,lineHint:e.lineStart,file:e.file,expectedResult:`inspect callers and callees to plan safe decomposition of ${e.name}`}]})}return i}export function computeCognitiveComplexity(e){let t=0;const i=(e,n)=>{let s=0,a=!1;switch(e.kind){case ts.SyntaxKind.IfStatement:case ts.SyntaxKind.ForStatement:case ts.SyntaxKind.ForInStatement:case ts.SyntaxKind.ForOfStatement:case ts.SyntaxKind.WhileStatement:case ts.SyntaxKind.DoStatement:case ts.SyntaxKind.CatchClause:case ts.SyntaxKind.ConditionalExpression:case ts.SyntaxKind.SwitchStatement:s=1,a=!0}if(e.kind!==ts.SyntaxKind.BinaryExpression||e.operatorToken.kind!==ts.SyntaxKind.AmpersandAmpersandToken&&e.operatorToken.kind!==ts.SyntaxKind.BarBarToken&&e.operatorToken.kind!==ts.SyntaxKind.QuestionQuestionToken||(s=1),e.kind===ts.SyntaxKind.IfStatement&&e.parent&&ts.isIfStatement(e.parent)&&e.parent.elseStatement===e&&(s=1,a=!1),a)return t+=s+n,void ts.forEachChild(e,e=>i(e,n+1));t+=s,ts.forEachChild(e,e=>i(e,n))};return i(e,0),t}export function detectCognitiveComplexity(e,t=15){const i=[];for(const n of e)if(!isTestFile(n.file))for(const e of n.functions)e.cognitiveComplexity>t&&i.push({severity:e.cognitiveComplexity>25?"high":"medium",category:"cognitive-complexity",file:n.file,lineStart:e.lineStart,lineEnd:e.lineEnd,title:`High cognitive complexity: ${e.name} (${e.cognitiveComplexity})`,reason:`Function cognitive complexity is ${e.cognitiveComplexity} (threshold: ${t}). Nested branches compound reading difficulty.`,files:[`${n.file}:${e.lineStart}-${e.lineEnd}`],suggestedFix:{strategy:"Reduce nesting and simplify control flow.",steps:["Convert nested branches into early returns / guard clauses.","Extract deeply nested blocks into named helper functions.","Replace complex boolean chains with named predicates."]},impact:"Lower cognitive complexity directly correlates with fewer bugs and faster code reviews.",tags:["complexity","readability","nesting"],lspHints:[{tool:"lspCallHierarchy",symbolName:e.name,lineHint:e.lineStart,file:n.file,expectedResult:`understand call graph before simplifying ${e.name}`}]});return i}export function detectExcessiveParameters(e,t=5){const i=[];for(const n of e)if(!isTestFile(n.file))for(const e of n.functions)null==e.params||e.params<=t||i.push({severity:e.params>7?"high":"medium",category:"excessive-parameters",file:n.file,lineStart:e.lineStart,lineEnd:e.lineEnd,title:`Excessive parameters: ${e.name} (${e.params} params)`,reason:`Function has ${e.params} parameters (threshold: ${t}). High parameter counts make call sites hard to read and signal the function may be doing too much.`,files:[`${n.file}:${e.lineStart}-${e.lineEnd}`],suggestedFix:{strategy:"Introduce a parameter object or split the function.",steps:["Group related parameters into an options/config object.","Use destructuring at the function signature for clarity.","Consider splitting into smaller, focused functions if params serve different concerns."]},impact:"Improves call-site readability and makes the API easier to evolve.",tags:["api-design","readability","refactor"]});return i}export function detectEmptyCatchBlocks(e){const t=[];for(const i of e)if(!isTestFile(i.file)&&i.emptyCatches&&0!==i.emptyCatches.length)for(const e of i.emptyCatches)t.push({severity:"medium",category:"empty-catch",file:i.file,lineStart:e.lineStart,lineEnd:e.lineEnd,title:"Empty catch block silently swallows errors",reason:`Catch block at line ${e.lineStart} has no statements — errors are silently ignored.`,files:[`${i.file}:${e.lineStart}-${e.lineEnd}`],suggestedFix:{strategy:"Log, re-throw, or handle the error explicitly.",steps:["Add error logging (console.error or a logger) at minimum.","Re-throw if the caller should handle the error.","Add a comment explaining why swallowing is intentional, if it truly is."]},impact:"Prevents silent failures that are extremely hard to debug in production.",tags:["error-handling","reliability","silent-failure"]});return t}export function detectSwitchNoDefault(e){const t=[];for(const i of e)if(!isTestFile(i.file)&&i.switchesWithoutDefault&&0!==i.switchesWithoutDefault.length)for(const e of i.switchesWithoutDefault)t.push({severity:"low",category:"switch-no-default",file:i.file,lineStart:e.lineStart,lineEnd:e.lineEnd,title:"Switch statement missing default case",reason:`Switch at line ${e.lineStart} has no default clause — unexpected values fall through silently.`,files:[`${i.file}:${e.lineStart}-${e.lineEnd}`],suggestedFix:{strategy:"Add a default case with error handling or exhaustive check.",steps:["Add a default clause that throws an unreachable error for exhaustiveness.","Or log a warning for unexpected values.","In TypeScript, use `never` type assertion for compile-time exhaustive checks."]},impact:"Catches unexpected values early and prevents silent logic bugs.",tags:["control-flow","exhaustiveness","safety"]});return t}export function detectUnsafeAny(e,t=5){const i=[];for(const n of e)if(!isTestFile(n.file)&&!(null==n.anyCount||n.anyCount<=t)){if(!canAddFinding(i))break;i.push({severity:n.anyCount>10?"high":"medium",category:"unsafe-any",file:n.file,lineStart:1,lineEnd:1,title:`Excessive \`any\` usage: ${n.file} (${n.anyCount} occurrences)`,reason:`File uses \`any\` type ${n.anyCount} times (threshold: ${t}). Each \`any\` disables type checking and allows silent runtime errors.`,files:[n.file],suggestedFix:{strategy:"Replace `any` with specific types, `unknown`, or generics.",steps:["Replace `any` with `unknown` and add type guards where needed.","Use generics for functions that operate on multiple types.","Define proper interfaces for complex data shapes.","Use `as const` assertions instead of `as any` where possible."]},impact:"Restores TypeScript safety and catches bugs at compile time instead of runtime.",tags:["type-safety","reliability","typescript"]})}return i}export function detectHighHalsteadEffort(e,t=5e5,i=2){const n=[];for(const s of e)if(!isTestFile(s.file))for(const e of s.functions){if(!e.halstead)continue;const{effort:a,estimatedBugs:o,volume:r}=e.halstead;if(a<=t&&o<=i)continue;const l=[];a>t&&l.push(`effort=${Math.round(a)} (threshold: ${t})`),o>i&&l.push(`estimatedBugs=${o.toFixed(2)} (threshold: ${i})`),n.push({severity:a>2*t||o>5?"high":"medium",category:"halstead-effort",file:s.file,lineStart:e.lineStart,lineEnd:e.lineEnd,title:`High Halstead complexity: ${e.name}`,reason:`Function has high implementation complexity: ${l.join("; ")}. Volume=${Math.round(r)}.`,files:[`${s.file}:${e.lineStart}-${e.lineEnd}`],suggestedFix:{strategy:"Reduce operator/operand count by extracting helpers and simplifying expressions.",steps:["Extract complex sub-expressions into named intermediate variables.","Split into smaller functions with fewer unique operators/operands.","Replace imperative loops with declarative array methods where clearer."]},impact:"Lower Halstead effort correlates with fewer bugs and faster comprehension.",tags:["complexity","maintainability","effort"]})}return n}export function detectLowMaintainability(e,t=20){const i=[];for(const n of e)if(!isTestFile(n.file))for(const e of n.functions)null==e.maintainabilityIndex||e.maintainabilityIndex>=t||i.push({severity:e.maintainabilityIndex<10?"critical":"high",category:"low-maintainability",file:n.file,lineStart:e.lineStart,lineEnd:e.lineEnd,title:`Low maintainability: ${e.name} (MI=${e.maintainabilityIndex.toFixed(1)})`,reason:`Maintainability Index is ${e.maintainabilityIndex.toFixed(1)} (threshold: ${t}, scale 0-100). Combines Halstead volume, cyclomatic complexity, and lines of code.`,files:[`${n.file}:${e.lineStart}-${e.lineEnd}`],suggestedFix:{strategy:"Reduce complexity, shorten the function, and simplify expressions.",steps:["Split into smaller functions to reduce LOC and cyclomatic complexity.","Extract complex expressions to reduce Halstead volume.","Convert nested logic to early returns and guard clauses.","Consider if parts of the function belong in separate modules."]},impact:"Higher MI directly predicts lower maintenance cost and defect rate.",tags:["maintainability","complexity","technical-debt"]});return i}export function detectTypeAssertionEscape(e){const t=[];for(const i of e){if(isTestFile(i.file))continue;const e=i.typeAssertionEscapes;if(!e)continue;const n=e.asAny.length+e.doubleAssertion.length+e.nonNull.length;if(0===n)continue;const s=[];e.asAny.length>0&&s.push(`${e.asAny.length} \`as any\``),e.doubleAssertion.length>0&&s.push(`${e.doubleAssertion.length} double-assertion`),e.nonNull.length>0&&s.push(`${e.nonNull.length} non-null \`!\``);const a=[...e.asAny,...e.doubleAssertion,...e.nonNull].map(e=>e.lineStart),o=Math.min(...a);if(!canAddFinding(t))break;t.push({severity:e.asAny.length+e.doubleAssertion.length>3?"high":"medium",category:"type-assertion-escape",file:i.file,lineStart:o,lineEnd:o,title:`Type-safety escapes in ${i.file} (${n})`,reason:`Found ${s.join(", ")}. Each assertion bypasses TypeScript's type checker.`,files:[i.file],suggestedFix:{strategy:"Replace type assertions with proper type guards or narrow types.",steps:["Replace `as any` with `unknown` and add runtime type checks.","Replace `as unknown as T` with proper generic constraints.","Replace `!` assertions with explicit null checks."]},impact:"Type assertions silence the compiler — runtime errors go undetected.",tags:["type-safety","assertions","code-quality"]})}return t}export function detectMissingErrorBoundary(e){const t=[];for(const i of e)if(!isTestFile(i.file)&&i.unprotectedAsync)for(const e of i.unprotectedAsync){const n=e.awaitCount>=4?"high":e.awaitCount>=2?"medium":"low";t.push({severity:n,category:"missing-error-boundary",file:i.file,lineStart:e.lineStart,lineEnd:e.lineEnd,title:`Missing error boundary: ${e.name} (${e.awaitCount} awaits, no try-catch)`,reason:`Async function "${e.name}" has ${e.awaitCount} await(s) but no try-catch. Rejected promises propagate as unhandled rejections.`,files:[i.file],suggestedFix:{strategy:"Wrap await calls in try-catch or add a .catch() handler.",steps:["Add a try-catch block around the await expressions.","Handle errors appropriately (log, return default, re-throw with context).","If the caller handles errors, document it with a comment."]},impact:"Unhandled promise rejections crash Node.js processes and cause silent failures in browsers.",tags:["error-handling","async","reliability"],lspHints:[{tool:"lspCallHierarchy",symbolName:e.name,lineHint:e.lineStart,file:i.file,expectedResult:"check if callers wrap this in try-catch or .catch() — if so, the boundary may exist upstream"}]})}return t}export function detectPromiseMisuse(e){const t=[];for(const i of e)if(!isTestFile(i.file)&&i.asyncWithoutAwait)for(const e of i.asyncWithoutAwait)t.push({severity:"medium",category:"promise-misuse",file:i.file,lineStart:e.lineStart,lineEnd:e.lineEnd,title:`Unnecessary async: ${e.name} has no await`,reason:`Function "${e.name}" is declared \`async\` but never uses \`await\`. The \`async\` keyword adds unnecessary Promise wrapping.`,files:[i.file],suggestedFix:{strategy:"Remove the async keyword or add the missing await.",steps:["If the function does not need to be async, remove the `async` keyword.","If an `await` was forgotten, add it to the appropriate call.","Verify callers handle the return value correctly after the change."]},impact:"Unnecessary async wrapping adds microtask overhead and misleads readers.",tags:["async","performance","clarity"]});return t}export function detectAwaitInLoop(e){const t=[];for(const i of e)if(!isTestFile(i.file))for(const e of i.awaitInLoopLocations||[])t.push({severity:"high",category:"await-in-loop",file:i.file,lineStart:e.lineStart,lineEnd:e.lineEnd,title:"await inside loop — sequential async execution",reason:"Each await runs serially. For N iterations this takes N * latency instead of max(latency). Use Promise.all() or Promise.allSettled() for parallel execution.",files:[i.file],suggestedFix:{strategy:"Collect promises and await them in parallel with Promise.all().",steps:["Collect all async operations into an array of promises.","Use await Promise.all(promises) or Promise.allSettled(promises).","If order matters or rate limiting is needed, use a batching utility."]},impact:"Sequential awaits multiply latency by N iterations — parallelizing can reduce total time to max(single-latency).",tags:["performance","async","n-plus-one"],lspHints:[{tool:"lspGotoDefinition",symbolName:"await",lineHint:e.lineStart,file:i.file,expectedResult:"navigate to the awaited call to check if parallelization is safe"}]});return t}export function detectSyncIo(e){const t=[];for(const i of e)if(!isTestFile(i.file))for(const e of i.syncIoCalls||[])t.push({severity:"medium",category:"sync-io",file:i.file,lineStart:e.lineStart,lineEnd:e.lineEnd,title:`Synchronous I/O: ${e.name}`,reason:`${e.name} blocks the event loop. In server or UI code this degrades responsiveness for all concurrent operations.`,files:[i.file],suggestedFix:{strategy:"Replace with async equivalent.",steps:[`Replace ${e.name} with its async counterpart (e.g. fs.promises.readFile).`,"Sync I/O is acceptable in CLI scripts, build tools, or one-time init code."]},impact:"Synchronous I/O blocks the event loop, stalling all concurrent requests until the operation completes.",tags:["performance","blocking","io"],lspHints:[{tool:"lspCallHierarchy",symbolName:e.name,lineHint:e.lineStart,file:i.file,expectedResult:"find callers to assess if this sync I/O is in a hot path"}]});return t}export function detectUnclearedTimers(e){const t=[];for(const i of e)if(!isTestFile(i.file))for(const e of i.timerCalls||[])"setInterval"!==e.kind||e.hasCleanup||t.push({severity:"medium",category:"uncleared-timer",file:i.file,lineStart:e.lineStart,lineEnd:e.lineEnd,title:"setInterval without clearInterval in scope",reason:"setInterval without cleanup runs indefinitely, causing memory leaks and unexpected behavior after component unmount or scope exit.",files:[i.file],suggestedFix:{strategy:"Store the timer ID and call clearInterval in cleanup.",steps:["Assign the return value: const id = setInterval(...).","Call clearInterval(id) in cleanup (useEffect return, componentWillUnmount, or scope exit)."]},impact:"Uncleared intervals run indefinitely, leaking memory and CPU cycles after their scope is no longer relevant.",tags:["performance","memory-leak","timer"]});return t}export function detectListenerLeakRisk(e){const t=[];for(const i of e){if(isTestFile(i.file))continue;const e=i.listenerRegistrations||[],n=i.listenerRemovals||[];e.length>0&&0===n.length&&t.push({severity:"medium",category:"listener-leak-risk",file:i.file,lineStart:e[0].lineStart,lineEnd:e[e.length-1].lineEnd,title:`${e.length} event listener(s) added without any removal`,reason:"addEventListener/on without corresponding removeEventListener/off risks memory leaks if the target outlives the subscriber.",files:[i.file],suggestedFix:{strategy:"Add corresponding listener removal in cleanup.",steps:["Store the handler reference in a variable.","Call removeEventListener/off in cleanup (unmount, dispose, close).","Or use AbortController signal for automatic cleanup."]},impact:"Listener references prevent garbage collection of the subscriber, causing memory growth proportional to event-target lifetime.",tags:["performance","memory-leak","events"]})}return t}export function detectUnboundedCollection(e){const t=[];for(const i of e)if(!isTestFile(i.file))for(const e of i.functions)e.loops>=2&&e.calls>=5&&e.maxLoopDepth>=2&&t.push({severity:"low",category:"unbounded-collection",file:i.file,lineStart:e.lineStart,lineEnd:e.lineEnd,title:`Potential unbounded collection growth in ${e.name}`,reason:`Function "${e.name}" has ${e.loops} loops nested ${e.maxLoopDepth} levels deep with ${e.calls} calls — structural signal for unbounded growth. Validate with tools: read the function body and check for collection mutations (.push, .add, .set) inside loops.`,files:[i.file],suggestedFix:{strategy:"Add size limits, pagination, or streaming.",steps:["Add a maximum size check before adding to collections.","Use pagination or streaming for large datasets.","Consider using generators for lazy evaluation."]},impact:"Unbounded collection growth inside nested loops can cause out-of-memory crashes under large input.",tags:["performance","memory","collection"]});return t}export function detectSimilarFunctionBodies(e,t=.85){const i=[],n=[];for(const t of e.values())for(const e of t)isTestFile(e.file)||n.push(e);const s=new Map;for(const e of n){const t=`${e.kind}|${Math.round(e.statementCount/3)}`;s.has(t)||s.set(t,[]),s.get(t).push(e)}for(const[,e]of s)if(!(e.length<2||e.length>50))for(let n=0;n<e.length;n++)for(let s=n+1;s<e.length;s++){const a=e[n],o=e[s];if(a.hash===o.hash)continue;if(a.file===o.file&&a.lineStart===o.lineStart)continue;if(Math.min(a.statementCount,o.statementCount)/Math.max(a.statementCount,o.statementCount)<.8)continue;const r=computeMetricSimilarity(a,o);r>=t&&i.push({severity:r>=.95?"high":"medium",category:"similar-function-body",file:a.file,lineStart:a.lineStart,lineEnd:a.lineEnd,title:`Similar function: ${a.name} (${(100*r).toFixed(0)}% similar to ${o.name} in ${o.file})`,reason:`"${a.name}" and "${o.name}" have ${(100*r).toFixed(0)}% structural similarity. Near-duplicates diverge over time and should be consolidated.`,files:[a.file,o.file],suggestedFix:{strategy:"Extract shared logic into a parameterized helper.",steps:[`Compare ${a.file}:${a.lineStart} with ${o.file}:${o.lineStart}.`,"Identify the varying parts and extract them as parameters.","Create a shared function and call it from both locations."]},impact:"Near-clone functions diverge over time, causing inconsistent behavior and multiplied maintenance cost.",tags:["duplication","maintainability","near-clone"]})}return i}function computeMetricSimilarity(e,t){const i=[[e.metrics.complexity,t.metrics.complexity],[e.metrics.maxBranchDepth,t.metrics.maxBranchDepth],[e.metrics.maxLoopDepth,t.metrics.maxLoopDepth],[e.metrics.returns,t.metrics.returns],[e.metrics.awaits,t.metrics.awaits],[e.metrics.calls,t.metrics.calls],[e.metrics.loops,t.metrics.loops],[e.statementCount,t.statementCount]];let n=0;for(const[e,t]of i){const i=Math.max(e,t,1);n+=1-Math.abs(e-t)/i}return n/i.length}export function detectMessageChains(e){const t=[];for(const i of e){if(!i.messageChains||0===i.messageChains.length)continue;const e=new Map;for(const t of i.messageChains){const i=e.get(t.lineStart);(!i||t.depth>i.depth)&&e.set(t.lineStart,t)}for(const n of e.values()){const e=n.depth>=6?"high":"medium";t.push({severity:e,category:"message-chain",file:i.file,lineStart:n.lineStart,lineEnd:n.lineEnd,title:`Message chain of depth ${n.depth}: ${n.chain.slice(0,50)}`,reason:`A property-access chain of ${n.depth} steps violates the Law of Demeter — the caller navigates through ${n.depth-1} intermediate objects to reach its target. Deep chains tightly couple the caller to internal object structure, making refactoring brittle.`,files:[i.file],suggestedFix:{strategy:"Apply the Law of Demeter — talk only to immediate friends.",steps:["Identify the root object and the final method/property being used.","Add a delegating method to the root object (Tell, Don't Ask).","Replace the chain with a single call on the immediate object.","If the chain crosses module boundaries, consider whether the intermediate objects should be passed directly."]},impact:"Deep property chains tightly couple code to internal object structure. When intermediate objects change, every chain accessing them must be updated.",tags:["coupling","law-of-demeter","maintainability"],lspHints:[{tool:"lspGotoDefinition",symbolName:n.chain.split(".")[0],lineHint:n.lineStart,file:i.file,expectedResult:"find the type of the root object to understand what intermediate types the chain traverses"}]})}}return t}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{findImportLine,isLikelyEntrypoint}from"./shared.js";import{canAddFinding}from"./shared.js";import{isTestFile}from"../common/utils.js";export function detectGodModules(e,t,i=500,o=20){const n=[];for(const s of e){if(isTestFile(s.file))continue;const e=s.functions.reduce((e,t)=>e+t.statementCount,0),r=(t.declaredExportsByFile.get(s.file)||[]).length,l=[];if(e>i&&l.push(`${e} statements (threshold: ${i})`),r>o&&l.push(`${r} exports (threshold: ${o})`),0!==l.length){if(!canAddFinding(n))break;n.push({severity:"high",category:"god-module",file:s.file,lineStart:1,lineEnd:1,title:`God module: ${s.file}`,reason:`Module is excessively large: ${l.join("; ")}.`,files:[s.file],suggestedFix:{strategy:"Split module into focused sub-modules with single responsibilities.",steps:["Identify distinct functional groups within the module.","Extract each group into a dedicated module.","Create a barrel if backward compatibility is needed.","Update imports incrementally."]},impact:"Smaller modules are easier to understand, test, and maintain.",tags:["complexity","responsibility","size"],lspHints:[{tool:"lspFindReferences",symbolName:s.file.split("/").pop()||s.file,lineHint:1,file:s.file,expectedResult:"identify consumer clusters to guide module splitting strategy"}]})}}return n}function folderOf(e){const t=e.replace(/\\/g,"/"),i=t.lastIndexOf("/");return-1===i?".":t.slice(0,i)}export function detectMegaFolders(e,t=25,i=.25){const o=[],n=e.filter(e=>!isTestFile(e.file));if(0===n.length)return o;const s=new Map;for(const e of n){const t=folderOf(e.file);s.has(t)||s.set(t,[]),s.get(t).push(e)}const r=[...s.entries()].map(([e,t])=>({folder:e,entries:t,count:t.length})).filter(({count:e})=>e>=t&&e/n.length>=i).sort((e,t)=>t.count-e.count);for(const e of r){const t=e.count/n.length,i=t>=.5||e.count>=50?"high":"medium",s=e.entries.map(e=>e.file).sort().slice(0,8),r=e.entries[0]?.file??e.folder;if(!canAddFinding(o))break;o.push({severity:i,category:"mega-folder",file:r,lineStart:1,lineEnd:1,title:`Mega folder: ${e.folder} (${e.count} files)`,reason:`${e.folder} contains ${e.count} production files (${(100*t).toFixed(1)}% of the codebase), which usually indicates mixed responsibilities and weak module boundaries.`,files:s,suggestedFix:{strategy:"Map the import graph, identify domain clusters, then restructure with an automated migration script.",steps:["Extract the local import graph (rg/localSearchCode) and group files into clusters by what imports what.","Design target directories that follow the data flow (e.g., types → parsing → analysis → detection → reporting → orchestration).","Write a disposable migration script that maps old basenames to { dir, name } targets, moves files, and rewrites all relative import paths atomically.","Validate after each phase: tsc --noEmit, eslint --fix, test suite.","Move shared primitives into a dedicated common/ folder to avoid cross-domain coupling."]},impact:"Improves navigability, ownership boundaries, and change isolation.",tags:["architecture","modularity","folder-structure","maintainability"],evidence:{folderPath:e.folder,fileCount:e.count,totalProductionFiles:n.length,concentration:t},lspHints:[{tool:"lspGotoDefinition",symbolName:e.folder,lineHint:1,file:r,expectedResult:"inventory representative modules in this folder before planning decomposition"}]})}return o}export function detectGodFunctions(e,t=100,i=10){const o=[];for(const n of e)if(!isTestFile(n.file))for(const e of n.functions){const s=e.statementCount>t,r=void 0!==e.maintainabilityIndex&&e.maintainabilityIndex<i&&e.lengthLines>30;if(s||r){const l=r&&void 0!==e.maintainabilityIndex?` MI=${e.maintainabilityIndex.toFixed(1)} (threshold: ${i}).`:"",a=s?`${e.statementCount} statements (threshold: ${t}).`:"";o.push({severity:"high",category:"god-function",file:n.file,lineStart:e.lineStart,lineEnd:e.lineEnd,title:`God function: ${e.name}`,reason:`Function "${e.name}" triggers god-function detection. ${a}${l}`.trim(),files:[`${n.file}:${e.lineStart}-${e.lineEnd}`],suggestedFix:{strategy:"Break down into smaller, focused functions.",steps:["Identify logical steps within the function.","Extract each step into a named helper.","Keep the original as a high-level orchestrator.","Test each extracted function independently."]},impact:"Improves readability, testability, and maintenance.",tags:["complexity","responsibility","size"],lspHints:[{tool:"lspCallHierarchy",symbolName:e.name,lineHint:e.lineStart,file:n.file,expectedResult:`map callers and callees to identify safe extraction boundaries for ${e.name}`}]})}}return o}export function detectLowCohesion(e,t=3){const i=[];for(const o of e.files){if(isTestFile(o)||isLikelyEntrypoint(o))continue;const n=e.declaredExportsByFile.get(o);if(!n||n.length<t)continue;const s=new Set(n.map(e=>e.name)),r=new Map;for(const[t,i]of e.importedSymbolsByFile.entries())for(const e of i)e.resolvedModule===o&&s.has(e.importedName)&&(r.has(e.importedName)||r.set(e.importedName,new Set),r.get(e.importedName).add(t));const l=[...r.keys()];if(l.length<2)continue;const a=new Map;for(const e of l)a.set(e,new Set);for(const t of e.importedSymbolsByFile.values()){const e=t.filter(e=>e.resolvedModule===o&&s.has(e.importedName)).map(e=>e.importedName);for(let t=0;t<e.length;t++)for(let i=t+1;i<e.length;i++)a.get(e[t])?.add(e[i]),a.get(e[i])?.add(e[t])}const c=new Set;let d=0;for(const e of l){if(c.has(e))continue;d++;const t=[e];for(;t.length>0;){const e=t.pop();if(!c.has(e)){c.add(e);for(const i of a.get(e)||[])c.has(i)||t.push(i)}}}d>1&&i.push({severity:d>=4?"high":"medium",category:"low-cohesion",file:o,lineStart:1,lineEnd:1,title:`Low cohesion: ${o} (LCOM=${d})`,reason:`Module exports ${l.length} consumed symbols that form ${d} independent groups. Consumers never import symbols across groups — the module serves unrelated purposes.`,files:[o],suggestedFix:{strategy:`Split into ${d} focused modules, one per cohesion group.`,steps:["Identify which exports belong to each independent group.","Create a new module for each group with a descriptive name.","Move exports and their dependencies to the appropriate module.","Update consumer imports to point to the new modules."]},impact:"Higher cohesion = easier navigation, focused testing, and smaller change blast radius.",tags:["cohesion","responsibility","architecture"]})}return i}export function computeHotFiles(e,t,i,o=20){const n=new Set;for(const e of t.cycles)for(const t of e.path)n.add(t);const s=new Set;for(const e of t.criticalPaths)for(const t of e.path)s.add(t);const r=[];for(const t of e.files){if(isTestFile(t))continue;const o=(e.incoming.get(t)||new Set).size,l=(e.outgoing.get(t)||new Set).size,a=i.get(t),c=a?.score??0,d=(e.declaredExportsByFile.get(t)||[]).length,f=n.has(t),p=s.has(t),u=Math.round(3*o+.5*c+1.5*d+.5*l+(f?20:0)+(p?10:0));u>0&&r.push({file:t,riskScore:u,fanIn:o,fanOut:l,complexityScore:c,exportCount:d,inCycle:f,onCriticalPath:p})}return r.sort((e,t)=>t.riskScore-e.riskScore),r.slice(0,o)}export function detectUntestedCriticalCode(e,t,i,o=40){const n=[],s=new Set,r=(t,i,o)=>{if(s.has(t))return;if(s.add(t),isTestFile(t))return;if((t=>{const i=e.incomingFromTests.get(t);return!!i&&i.size>0})(t))return;const r=i>=60;canAddFinding(n)&&n.push({severity:r?"critical":"high",category:"untested-critical-code",file:t,lineStart:1,lineEnd:1,title:`Untested critical code: ${t}`,reason:`High-risk file has no test imports. ${o.join("; ")} (risk score: ${i}).`,files:[t],suggestedFix:{strategy:"Add test coverage for this critical module.",steps:["Create a test file that imports and exercises the public API of this module.","Focus on the highest-complexity functions and exported behaviors first.","Add integration tests if this module sits on a critical dependency path.","Consider property-based tests for complex data transformations."]},impact:"Untested critical code is the highest-risk area for regressions and undetected bugs.",tags:["testing","coverage","change-risk","critical"]})};for(const e of t){const t=[];t.push(`fan-in=${e.fanIn}, fan-out=${e.fanOut}, complexity=${e.complexityScore}`),e.inCycle&&t.push("in dependency cycle"),e.onCriticalPath&&t.push("on critical dependency path"),r(e.file,e.riskScore,t)}for(const[e,t]of i){if(t.score<o)continue;const i=[`high complexity score (${t.score}), ${t.highComplexityFunctions} high-complexity functions`];r(e,t.score,i)}return n.sort((e,t)=>{const i={critical:4,high:3,medium:2,low:1,info:0};return(i[t.severity]||0)-(i[e.severity]||0)}),n.slice(0,25)}export function detectFeatureEnvy(e,t=.6,i=5){const o=[];for(const[n,s]of e.importedSymbolsByFile.entries()){if(isTestFile(n))continue;if(!e.files.has(n))continue;const r=s.filter(e=>e.resolvedModule&&!e.isTypeOnly);if(r.length<i)continue;const l=new Map;for(const e of r)e.resolvedModule&&l.set(e.resolvedModule,(l.get(e.resolvedModule)||0)+1);for(const[s,a]of l){const l=a/r.length;if(l>=t&&a>=i){const t=findImportLine(e,n,s);o.push({severity:l>.8?"high":"medium",category:"feature-envy",file:n,lineStart:t.lineStart,lineEnd:t.lineEnd,title:`Feature envy: ${n} → ${s}`,reason:`Module imports ${a}/${r.length} symbols (${(100*l).toFixed(0)}%) from "${s}". This suggests the logic may belong in or closer to the target module.`,files:[n,s],suggestedFix:{strategy:"Move dependent logic to the target module or extract a shared module.",steps:["Identify which functions/logic in this file use the imported symbols.","Move that logic to the target module if it belongs there.","If shared, extract a dedicated module that both can import from.","Reduce the import surface by passing data instead of importing behaviors."]},impact:"Misplaced logic increases coupling and makes changes ripple across module boundaries.",tags:["coupling","responsibility","misplaced-logic"],lspHints:[{tool:"lspCallHierarchy",symbolName:n.split("/").pop()||n,lineHint:t.lineStart,file:n,expectedResult:`trace which functions use imports from ${s} to decide what to move`},{tool:"lspGotoDefinition",symbolName:s.split("/").pop()||s,lineHint:t.lineStart,file:n,expectedResult:"inspect target module to evaluate if logic belongs there"}]})}}}return o}
|