@opensip-cli/checks-typescript 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +8 -0
- package/README.md +31 -0
- package/dist/__tests__/all-checks-execute.test.d.ts +12 -0
- package/dist/__tests__/all-checks-execute.test.d.ts.map +1 -0
- package/dist/__tests__/all-checks-execute.test.js +846 -0
- package/dist/__tests__/all-checks-execute.test.js.map +1 -0
- package/dist/__tests__/behavior-fixtures-2.test.d.ts +9 -0
- package/dist/__tests__/behavior-fixtures-2.test.d.ts.map +1 -0
- package/dist/__tests__/behavior-fixtures-2.test.js +625 -0
- package/dist/__tests__/behavior-fixtures-2.test.js.map +1 -0
- package/dist/__tests__/behavior-fixtures-3.test.d.ts +7 -0
- package/dist/__tests__/behavior-fixtures-3.test.d.ts.map +1 -0
- package/dist/__tests__/behavior-fixtures-3.test.js +658 -0
- package/dist/__tests__/behavior-fixtures-3.test.js.map +1 -0
- package/dist/__tests__/behavior-fixtures-4.test.d.ts +8 -0
- package/dist/__tests__/behavior-fixtures-4.test.d.ts.map +1 -0
- package/dist/__tests__/behavior-fixtures-4.test.js +590 -0
- package/dist/__tests__/behavior-fixtures-4.test.js.map +1 -0
- package/dist/__tests__/behavior-fixtures-5.test.d.ts +7 -0
- package/dist/__tests__/behavior-fixtures-5.test.d.ts.map +1 -0
- package/dist/__tests__/behavior-fixtures-5.test.js +548 -0
- package/dist/__tests__/behavior-fixtures-5.test.js.map +1 -0
- package/dist/__tests__/behavior-fixtures-6.test.d.ts +18 -0
- package/dist/__tests__/behavior-fixtures-6.test.d.ts.map +1 -0
- package/dist/__tests__/behavior-fixtures-6.test.js +1700 -0
- package/dist/__tests__/behavior-fixtures-6.test.js.map +1 -0
- package/dist/__tests__/behavior-fixtures.test.d.ts +10 -0
- package/dist/__tests__/behavior-fixtures.test.d.ts.map +1 -0
- package/dist/__tests__/behavior-fixtures.test.js +812 -0
- package/dist/__tests__/behavior-fixtures.test.js.map +1 -0
- package/dist/__tests__/branch-fixtures-2.test.d.ts +6 -0
- package/dist/__tests__/branch-fixtures-2.test.d.ts.map +1 -0
- package/dist/__tests__/branch-fixtures-2.test.js +1369 -0
- package/dist/__tests__/branch-fixtures-2.test.js.map +1 -0
- package/dist/__tests__/branch-fixtures-3.test.d.ts +7 -0
- package/dist/__tests__/branch-fixtures-3.test.d.ts.map +1 -0
- package/dist/__tests__/branch-fixtures-3.test.js +877 -0
- package/dist/__tests__/branch-fixtures-3.test.js.map +1 -0
- package/dist/__tests__/branch-fixtures.test.d.ts +6 -0
- package/dist/__tests__/branch-fixtures.test.d.ts.map +1 -0
- package/dist/__tests__/branch-fixtures.test.js +1072 -0
- package/dist/__tests__/branch-fixtures.test.js.map +1 -0
- package/dist/__tests__/checks.test.d.ts +2 -0
- package/dist/__tests__/checks.test.d.ts.map +1 -0
- package/dist/__tests__/checks.test.js +39 -0
- package/dist/__tests__/checks.test.js.map +1 -0
- package/dist/__tests__/fixture-coverage.allowlist.d.ts +19 -0
- package/dist/__tests__/fixture-coverage.allowlist.d.ts.map +1 -0
- package/dist/__tests__/fixture-coverage.allowlist.js +27 -0
- package/dist/__tests__/fixture-coverage.allowlist.js.map +1 -0
- package/dist/__tests__/fixture-coverage.test.d.ts +13 -0
- package/dist/__tests__/fixture-coverage.test.d.ts.map +1 -0
- package/dist/__tests__/fixture-coverage.test.js +57 -0
- package/dist/__tests__/fixture-coverage.test.js.map +1 -0
- package/dist/__tests__/no-bootstrap-tool-import.test.d.ts +2 -0
- package/dist/__tests__/no-bootstrap-tool-import.test.d.ts.map +1 -0
- package/dist/__tests__/no-bootstrap-tool-import.test.js +75 -0
- package/dist/__tests__/no-bootstrap-tool-import.test.js.map +1 -0
- package/dist/__tests__/phantom-dependency-detection.test.d.ts +12 -0
- package/dist/__tests__/phantom-dependency-detection.test.d.ts.map +1 -0
- package/dist/__tests__/phantom-dependency-detection.test.js +112 -0
- package/dist/__tests__/phantom-dependency-detection.test.js.map +1 -0
- package/dist/__tests__/typescript-frontend.test.d.ts +8 -0
- package/dist/__tests__/typescript-frontend.test.d.ts.map +1 -0
- package/dist/__tests__/typescript-frontend.test.js +57 -0
- package/dist/__tests__/typescript-frontend.test.js.map +1 -0
- package/dist/checks/architecture/circular-import-detection.d.ts +14 -0
- package/dist/checks/architecture/circular-import-detection.d.ts.map +1 -0
- package/dist/checks/architecture/circular-import-detection.js +55 -0
- package/dist/checks/architecture/circular-import-detection.js.map +1 -0
- package/dist/checks/architecture/contracts-schema-consistency.d.ts +11 -0
- package/dist/checks/architecture/contracts-schema-consistency.d.ts.map +1 -0
- package/dist/checks/architecture/contracts-schema-consistency.js +75 -0
- package/dist/checks/architecture/contracts-schema-consistency.js.map +1 -0
- package/dist/checks/architecture/drizzle-orm-migration-guardrails.d.ts +12 -0
- package/dist/checks/architecture/drizzle-orm-migration-guardrails.d.ts.map +1 -0
- package/dist/checks/architecture/drizzle-orm-migration-guardrails.js +92 -0
- package/dist/checks/architecture/drizzle-orm-migration-guardrails.js.map +1 -0
- package/dist/checks/architecture/index.d.ts +10 -0
- package/dist/checks/architecture/index.d.ts.map +1 -0
- package/dist/checks/architecture/index.js +10 -0
- package/dist/checks/architecture/index.js.map +1 -0
- package/dist/checks/architecture/missing-type-exports.d.ts +13 -0
- package/dist/checks/architecture/missing-type-exports.d.ts.map +1 -0
- package/dist/checks/architecture/missing-type-exports.js +245 -0
- package/dist/checks/architecture/missing-type-exports.js.map +1 -0
- package/dist/checks/architecture/module-coupling-fan-out.d.ts +20 -0
- package/dist/checks/architecture/module-coupling-fan-out.d.ts.map +1 -0
- package/dist/checks/architecture/module-coupling-fan-out.js +120 -0
- package/dist/checks/architecture/module-coupling-fan-out.js.map +1 -0
- package/dist/checks/architecture/no-bootstrap-tool-import.d.ts +38 -0
- package/dist/checks/architecture/no-bootstrap-tool-import.d.ts.map +1 -0
- package/dist/checks/architecture/no-bootstrap-tool-import.js +95 -0
- package/dist/checks/architecture/no-bootstrap-tool-import.js.map +1 -0
- package/dist/checks/architecture/package-json-exports-field.d.ts +10 -0
- package/dist/checks/architecture/package-json-exports-field.d.ts.map +1 -0
- package/dist/checks/architecture/package-json-exports-field.js +56 -0
- package/dist/checks/architecture/package-json-exports-field.js.map +1 -0
- package/dist/checks/architecture/phantom-dependency-detection.d.ts +22 -0
- package/dist/checks/architecture/phantom-dependency-detection.d.ts.map +1 -0
- package/dist/checks/architecture/phantom-dependency-detection.js +330 -0
- package/dist/checks/architecture/phantom-dependency-detection.js.map +1 -0
- package/dist/checks/architecture/tsconfig-extends-validation.d.ts +10 -0
- package/dist/checks/architecture/tsconfig-extends-validation.d.ts.map +1 -0
- package/dist/checks/architecture/tsconfig-extends-validation.js +78 -0
- package/dist/checks/architecture/tsconfig-extends-validation.js.map +1 -0
- package/dist/checks/index.d.ts +6 -0
- package/dist/checks/index.d.ts.map +1 -0
- package/dist/checks/index.js +6 -0
- package/dist/checks/index.js.map +1 -0
- package/dist/checks/quality/api/api-contract-validation.d.ts +15 -0
- package/dist/checks/quality/api/api-contract-validation.d.ts.map +1 -0
- package/dist/checks/quality/api/api-contract-validation.js +316 -0
- package/dist/checks/quality/api/api-contract-validation.js.map +1 -0
- package/dist/checks/quality/api/api-response-validation.d.ts +14 -0
- package/dist/checks/quality/api/api-response-validation.d.ts.map +1 -0
- package/dist/checks/quality/api/api-response-validation.js +209 -0
- package/dist/checks/quality/api/api-response-validation.js.map +1 -0
- package/dist/checks/quality/api/fastify-route-validation.d.ts +14 -0
- package/dist/checks/quality/api/fastify-route-validation.d.ts.map +1 -0
- package/dist/checks/quality/api/fastify-route-validation.js +298 -0
- package/dist/checks/quality/api/fastify-route-validation.js.map +1 -0
- package/dist/checks/quality/api/fastify-schema-coverage.d.ts +11 -0
- package/dist/checks/quality/api/fastify-schema-coverage.d.ts.map +1 -0
- package/dist/checks/quality/api/fastify-schema-coverage.js +261 -0
- package/dist/checks/quality/api/fastify-schema-coverage.js.map +1 -0
- package/dist/checks/quality/api/index.d.ts +5 -0
- package/dist/checks/quality/api/index.d.ts.map +1 -0
- package/dist/checks/quality/api/index.js +5 -0
- package/dist/checks/quality/api/index.js.map +1 -0
- package/dist/checks/quality/code-structure/duplicate-utility-functions.d.ts +32 -0
- package/dist/checks/quality/code-structure/duplicate-utility-functions.d.ts.map +1 -0
- package/dist/checks/quality/code-structure/duplicate-utility-functions.js +451 -0
- package/dist/checks/quality/code-structure/duplicate-utility-functions.js.map +1 -0
- package/dist/checks/quality/code-structure/index.d.ts +3 -0
- package/dist/checks/quality/code-structure/index.d.ts.map +1 -0
- package/dist/checks/quality/code-structure/index.js +3 -0
- package/dist/checks/quality/code-structure/index.js.map +1 -0
- package/dist/checks/quality/code-structure/no-any-types.d.ts +13 -0
- package/dist/checks/quality/code-structure/no-any-types.d.ts.map +1 -0
- package/dist/checks/quality/code-structure/no-any-types.js +116 -0
- package/dist/checks/quality/code-structure/no-any-types.js.map +1 -0
- package/dist/checks/quality/data-integrity/__tests__/null-safety-fp.test.d.ts +15 -0
- package/dist/checks/quality/data-integrity/__tests__/null-safety-fp.test.d.ts.map +1 -0
- package/dist/checks/quality/data-integrity/__tests__/null-safety-fp.test.js +51 -0
- package/dist/checks/quality/data-integrity/__tests__/null-safety-fp.test.js.map +1 -0
- package/dist/checks/quality/data-integrity/array-validation.d.ts +16 -0
- package/dist/checks/quality/data-integrity/array-validation.d.ts.map +1 -0
- package/dist/checks/quality/data-integrity/array-validation.js +508 -0
- package/dist/checks/quality/data-integrity/array-validation.js.map +1 -0
- package/dist/checks/quality/data-integrity/database-index-coverage.d.ts +14 -0
- package/dist/checks/quality/data-integrity/database-index-coverage.d.ts.map +1 -0
- package/dist/checks/quality/data-integrity/database-index-coverage.js +235 -0
- package/dist/checks/quality/data-integrity/database-index-coverage.js.map +1 -0
- package/dist/checks/quality/data-integrity/database-schema-validation.d.ts +16 -0
- package/dist/checks/quality/data-integrity/database-schema-validation.d.ts.map +1 -0
- package/dist/checks/quality/data-integrity/database-schema-validation.js +328 -0
- package/dist/checks/quality/data-integrity/database-schema-validation.js.map +1 -0
- package/dist/checks/quality/data-integrity/in-memory-repository-detection.d.ts +14 -0
- package/dist/checks/quality/data-integrity/in-memory-repository-detection.d.ts.map +1 -0
- package/dist/checks/quality/data-integrity/in-memory-repository-detection.js +157 -0
- package/dist/checks/quality/data-integrity/in-memory-repository-detection.js.map +1 -0
- package/dist/checks/quality/data-integrity/index.d.ts +8 -0
- package/dist/checks/quality/data-integrity/index.d.ts.map +1 -0
- package/dist/checks/quality/data-integrity/index.js +8 -0
- package/dist/checks/quality/data-integrity/index.js.map +1 -0
- package/dist/checks/quality/data-integrity/missing-input-validation.d.ts +12 -0
- package/dist/checks/quality/data-integrity/missing-input-validation.d.ts.map +1 -0
- package/dist/checks/quality/data-integrity/missing-input-validation.js +180 -0
- package/dist/checks/quality/data-integrity/missing-input-validation.js.map +1 -0
- package/dist/checks/quality/data-integrity/null-safety.d.ts +33 -0
- package/dist/checks/quality/data-integrity/null-safety.d.ts.map +1 -0
- package/dist/checks/quality/data-integrity/null-safety.js +766 -0
- package/dist/checks/quality/data-integrity/null-safety.js.map +1 -0
- package/dist/checks/quality/data-integrity/numeric-validation.d.ts +12 -0
- package/dist/checks/quality/data-integrity/numeric-validation.d.ts.map +1 -0
- package/dist/checks/quality/data-integrity/numeric-validation.js +409 -0
- package/dist/checks/quality/data-integrity/numeric-validation.js.map +1 -0
- package/dist/checks/quality/frontend/a11y-form-labels.d.ts +14 -0
- package/dist/checks/quality/frontend/a11y-form-labels.d.ts.map +1 -0
- package/dist/checks/quality/frontend/a11y-form-labels.js +93 -0
- package/dist/checks/quality/frontend/a11y-form-labels.js.map +1 -0
- package/dist/checks/quality/frontend/a11y-semantic-html.d.ts +14 -0
- package/dist/checks/quality/frontend/a11y-semantic-html.d.ts.map +1 -0
- package/dist/checks/quality/frontend/a11y-semantic-html.js +88 -0
- package/dist/checks/quality/frontend/a11y-semantic-html.js.map +1 -0
- package/dist/checks/quality/frontend/index.d.ts +4 -0
- package/dist/checks/quality/frontend/index.d.ts.map +1 -0
- package/dist/checks/quality/frontend/index.js +4 -0
- package/dist/checks/quality/frontend/index.js.map +1 -0
- package/dist/checks/quality/frontend/test-only-frontend-modules.d.ts +13 -0
- package/dist/checks/quality/frontend/test-only-frontend-modules.d.ts.map +1 -0
- package/dist/checks/quality/frontend/test-only-frontend-modules.js +159 -0
- package/dist/checks/quality/frontend/test-only-frontend-modules.js.map +1 -0
- package/dist/checks/quality/incomplete-regex-escaping.d.ts +13 -0
- package/dist/checks/quality/incomplete-regex-escaping.d.ts.map +1 -0
- package/dist/checks/quality/incomplete-regex-escaping.js +207 -0
- package/dist/checks/quality/incomplete-regex-escaping.js.map +1 -0
- package/dist/checks/quality/index.d.ts +11 -0
- package/dist/checks/quality/index.d.ts.map +1 -0
- package/dist/checks/quality/index.js +11 -0
- package/dist/checks/quality/index.js.map +1 -0
- package/dist/checks/quality/linting/index.d.ts +2 -0
- package/dist/checks/quality/linting/index.d.ts.map +1 -0
- package/dist/checks/quality/linting/index.js +2 -0
- package/dist/checks/quality/linting/index.js.map +1 -0
- package/dist/checks/quality/linting/typescript-frontend.d.ts +25 -0
- package/dist/checks/quality/linting/typescript-frontend.d.ts.map +1 -0
- package/dist/checks/quality/linting/typescript-frontend.js +159 -0
- package/dist/checks/quality/linting/typescript-frontend.js.map +1 -0
- package/dist/checks/quality/observability/index.d.ts +5 -0
- package/dist/checks/quality/observability/index.d.ts.map +1 -0
- package/dist/checks/quality/observability/index.js +5 -0
- package/dist/checks/quality/observability/index.js.map +1 -0
- package/dist/checks/quality/observability/logger-event-name-format.d.ts +12 -0
- package/dist/checks/quality/observability/logger-event-name-format.d.ts.map +1 -0
- package/dist/checks/quality/observability/logger-event-name-format.js +124 -0
- package/dist/checks/quality/observability/logger-event-name-format.js.map +1 -0
- package/dist/checks/quality/observability/no-hardcoded-correlation-id.d.ts +5 -0
- package/dist/checks/quality/observability/no-hardcoded-correlation-id.d.ts.map +1 -0
- package/dist/checks/quality/observability/no-hardcoded-correlation-id.js +77 -0
- package/dist/checks/quality/observability/no-hardcoded-correlation-id.js.map +1 -0
- package/dist/checks/quality/observability/observability-coverage/__tests__/analyzer.test.d.ts +11 -0
- package/dist/checks/quality/observability/observability-coverage/__tests__/analyzer.test.d.ts.map +1 -0
- package/dist/checks/quality/observability/observability-coverage/__tests__/analyzer.test.js +107 -0
- package/dist/checks/quality/observability/observability-coverage/__tests__/analyzer.test.js.map +1 -0
- package/dist/checks/quality/observability/observability-coverage/__tests__/logger-detector.test.d.ts +12 -0
- package/dist/checks/quality/observability/observability-coverage/__tests__/logger-detector.test.d.ts.map +1 -0
- package/dist/checks/quality/observability/observability-coverage/__tests__/logger-detector.test.js +94 -0
- package/dist/checks/quality/observability/observability-coverage/__tests__/logger-detector.test.js.map +1 -0
- package/dist/checks/quality/observability/observability-coverage/analyzer.d.ts +13 -0
- package/dist/checks/quality/observability/observability-coverage/analyzer.d.ts.map +1 -0
- package/dist/checks/quality/observability/observability-coverage/analyzer.js +117 -0
- package/dist/checks/quality/observability/observability-coverage/analyzer.js.map +1 -0
- package/dist/checks/quality/observability/observability-coverage/index.d.ts +4 -0
- package/dist/checks/quality/observability/observability-coverage/index.d.ts.map +1 -0
- package/dist/checks/quality/observability/observability-coverage/index.js +4 -0
- package/dist/checks/quality/observability/observability-coverage/index.js.map +1 -0
- package/dist/checks/quality/observability/observability-coverage/logger-detector.d.ts +29 -0
- package/dist/checks/quality/observability/observability-coverage/logger-detector.d.ts.map +1 -0
- package/dist/checks/quality/observability/observability-coverage/logger-detector.js +111 -0
- package/dist/checks/quality/observability/observability-coverage/logger-detector.js.map +1 -0
- package/dist/checks/quality/observability/observability-coverage/types.d.ts +64 -0
- package/dist/checks/quality/observability/observability-coverage/types.d.ts.map +1 -0
- package/dist/checks/quality/observability/observability-coverage/types.js +6 -0
- package/dist/checks/quality/observability/observability-coverage/types.js.map +1 -0
- package/dist/checks/quality/observability/pii-exposure-in-logs.d.ts +22 -0
- package/dist/checks/quality/observability/pii-exposure-in-logs.d.ts.map +1 -0
- package/dist/checks/quality/observability/pii-exposure-in-logs.js +212 -0
- package/dist/checks/quality/observability/pii-exposure-in-logs.js.map +1 -0
- package/dist/checks/quality/observability/pii-exposure-in-logs.test.d.ts +11 -0
- package/dist/checks/quality/observability/pii-exposure-in-logs.test.d.ts.map +1 -0
- package/dist/checks/quality/observability/pii-exposure-in-logs.test.js +46 -0
- package/dist/checks/quality/observability/pii-exposure-in-logs.test.js.map +1 -0
- package/dist/checks/quality/patterns/__tests__/toctou-fp.test.d.ts +14 -0
- package/dist/checks/quality/patterns/__tests__/toctou-fp.test.d.ts.map +1 -0
- package/dist/checks/quality/patterns/__tests__/toctou-fp.test.js +61 -0
- package/dist/checks/quality/patterns/__tests__/toctou-fp.test.js.map +1 -0
- package/dist/checks/quality/patterns/async-waterfall-detection.d.ts +26 -0
- package/dist/checks/quality/patterns/async-waterfall-detection.d.ts.map +1 -0
- package/dist/checks/quality/patterns/async-waterfall-detection.js +410 -0
- package/dist/checks/quality/patterns/async-waterfall-detection.js.map +1 -0
- package/dist/checks/quality/patterns/dispose-pattern-completeness.d.ts +13 -0
- package/dist/checks/quality/patterns/dispose-pattern-completeness.d.ts.map +1 -0
- package/dist/checks/quality/patterns/dispose-pattern-completeness.js +220 -0
- package/dist/checks/quality/patterns/dispose-pattern-completeness.js.map +1 -0
- package/dist/checks/quality/patterns/error-handling-quality.d.ts +17 -0
- package/dist/checks/quality/patterns/error-handling-quality.d.ts.map +1 -0
- package/dist/checks/quality/patterns/error-handling-quality.js +335 -0
- package/dist/checks/quality/patterns/error-handling-quality.js.map +1 -0
- package/dist/checks/quality/patterns/index.d.ts +10 -0
- package/dist/checks/quality/patterns/index.d.ts.map +1 -0
- package/dist/checks/quality/patterns/index.js +10 -0
- package/dist/checks/quality/patterns/index.js.map +1 -0
- package/dist/checks/quality/patterns/lifecycle-cleanup-enforcement.d.ts +16 -0
- package/dist/checks/quality/patterns/lifecycle-cleanup-enforcement.d.ts.map +1 -0
- package/dist/checks/quality/patterns/lifecycle-cleanup-enforcement.js +205 -0
- package/dist/checks/quality/patterns/lifecycle-cleanup-enforcement.js.map +1 -0
- package/dist/checks/quality/patterns/result-pattern-consistency.d.ts +16 -0
- package/dist/checks/quality/patterns/result-pattern-consistency.d.ts.map +1 -0
- package/dist/checks/quality/patterns/result-pattern-consistency.js +328 -0
- package/dist/checks/quality/patterns/result-pattern-consistency.js.map +1 -0
- package/dist/checks/quality/patterns/silent-early-returns.d.ts +23 -0
- package/dist/checks/quality/patterns/silent-early-returns.d.ts.map +1 -0
- package/dist/checks/quality/patterns/silent-early-returns.js +266 -0
- package/dist/checks/quality/patterns/silent-early-returns.js.map +1 -0
- package/dist/checks/quality/patterns/stream-buffer-size-limits.d.ts +13 -0
- package/dist/checks/quality/patterns/stream-buffer-size-limits.d.ts.map +1 -0
- package/dist/checks/quality/patterns/stream-buffer-size-limits.js +163 -0
- package/dist/checks/quality/patterns/stream-buffer-size-limits.js.map +1 -0
- package/dist/checks/quality/patterns/throws-documentation.d.ts +23 -0
- package/dist/checks/quality/patterns/throws-documentation.d.ts.map +1 -0
- package/dist/checks/quality/patterns/throws-documentation.js +519 -0
- package/dist/checks/quality/patterns/throws-documentation.js.map +1 -0
- package/dist/checks/quality/patterns/toctou-race-condition.d.ts +48 -0
- package/dist/checks/quality/patterns/toctou-race-condition.d.ts.map +1 -0
- package/dist/checks/quality/patterns/toctou-race-condition.js +639 -0
- package/dist/checks/quality/patterns/toctou-race-condition.js.map +1 -0
- package/dist/checks/quality/stubbed-implementation-detection.d.ts +24 -0
- package/dist/checks/quality/stubbed-implementation-detection.d.ts.map +1 -0
- package/dist/checks/quality/stubbed-implementation-detection.js +355 -0
- package/dist/checks/quality/stubbed-implementation-detection.js.map +1 -0
- package/dist/checks/quality/unused-config-options.d.ts +12 -0
- package/dist/checks/quality/unused-config-options.d.ts.map +1 -0
- package/dist/checks/quality/unused-config-options.js +245 -0
- package/dist/checks/quality/unused-config-options.js.map +1 -0
- package/dist/checks/resilience/__tests__/callback-invocation-safe.test.d.ts +2 -0
- package/dist/checks/resilience/__tests__/callback-invocation-safe.test.d.ts.map +1 -0
- package/dist/checks/resilience/__tests__/callback-invocation-safe.test.js +79 -0
- package/dist/checks/resilience/__tests__/callback-invocation-safe.test.js.map +1 -0
- package/dist/checks/resilience/__tests__/context-leakage-fp.test.d.ts +12 -0
- package/dist/checks/resilience/__tests__/context-leakage-fp.test.d.ts.map +1 -0
- package/dist/checks/resilience/__tests__/context-leakage-fp.test.js +34 -0
- package/dist/checks/resilience/__tests__/context-leakage-fp.test.js.map +1 -0
- package/dist/checks/resilience/__tests__/context-mutation.test.d.ts +11 -0
- package/dist/checks/resilience/__tests__/context-mutation.test.d.ts.map +1 -0
- package/dist/checks/resilience/__tests__/context-mutation.test.js +54 -0
- package/dist/checks/resilience/__tests__/context-mutation.test.js.map +1 -0
- package/dist/checks/resilience/callback-invocation-safe.d.ts +34 -0
- package/dist/checks/resilience/callback-invocation-safe.d.ts.map +1 -0
- package/dist/checks/resilience/callback-invocation-safe.js +247 -0
- package/dist/checks/resilience/callback-invocation-safe.js.map +1 -0
- package/dist/checks/resilience/context-leakage.d.ts +25 -0
- package/dist/checks/resilience/context-leakage.d.ts.map +1 -0
- package/dist/checks/resilience/context-leakage.js +435 -0
- package/dist/checks/resilience/context-leakage.js.map +1 -0
- package/dist/checks/resilience/context-mutation.d.ts +21 -0
- package/dist/checks/resilience/context-mutation.d.ts.map +1 -0
- package/dist/checks/resilience/context-mutation.js +368 -0
- package/dist/checks/resilience/context-mutation.js.map +1 -0
- package/dist/checks/resilience/detached-promises.d.ts +40 -0
- package/dist/checks/resilience/detached-promises.d.ts.map +1 -0
- package/dist/checks/resilience/detached-promises.js +646 -0
- package/dist/checks/resilience/detached-promises.js.map +1 -0
- package/dist/checks/resilience/index.d.ts +7 -0
- package/dist/checks/resilience/index.d.ts.map +1 -0
- package/dist/checks/resilience/index.js +7 -0
- package/dist/checks/resilience/index.js.map +1 -0
- package/dist/checks/resilience/no-raw-fetch.d.ts +11 -0
- package/dist/checks/resilience/no-raw-fetch.d.ts.map +1 -0
- package/dist/checks/resilience/no-raw-fetch.js +110 -0
- package/dist/checks/resilience/no-raw-fetch.js.map +1 -0
- package/dist/checks/resilience/no-unbounded-concurrency.d.ts +11 -0
- package/dist/checks/resilience/no-unbounded-concurrency.d.ts.map +1 -0
- package/dist/checks/resilience/no-unbounded-concurrency.js +117 -0
- package/dist/checks/resilience/no-unbounded-concurrency.js.map +1 -0
- package/dist/checks/security/__tests__/sql-injection.test.d.ts +17 -0
- package/dist/checks/security/__tests__/sql-injection.test.d.ts.map +1 -0
- package/dist/checks/security/__tests__/sql-injection.test.js +97 -0
- package/dist/checks/security/__tests__/sql-injection.test.js.map +1 -0
- package/dist/checks/security/index.d.ts +4 -0
- package/dist/checks/security/index.d.ts.map +1 -0
- package/dist/checks/security/index.js +4 -0
- package/dist/checks/security/index.js.map +1 -0
- package/dist/checks/security/input-sanitization.d.ts +20 -0
- package/dist/checks/security/input-sanitization.d.ts.map +1 -0
- package/dist/checks/security/input-sanitization.js +255 -0
- package/dist/checks/security/input-sanitization.js.map +1 -0
- package/dist/checks/security/sql-injection.d.ts +24 -0
- package/dist/checks/security/sql-injection.d.ts.map +1 -0
- package/dist/checks/security/sql-injection.js +330 -0
- package/dist/checks/security/sql-injection.js.map +1 -0
- package/dist/checks/security/unsafe-secret-comparison.d.ts +17 -0
- package/dist/checks/security/unsafe-secret-comparison.d.ts.map +1 -0
- package/dist/checks/security/unsafe-secret-comparison.js +227 -0
- package/dist/checks/security/unsafe-secret-comparison.js.map +1 -0
- package/dist/checks/testing/index.d.ts +2 -0
- package/dist/checks/testing/index.d.ts.map +1 -0
- package/dist/checks/testing/index.js +2 -0
- package/dist/checks/testing/index.js.map +1 -0
- package/dist/checks/testing/mock-implementations-in-production.d.ts +12 -0
- package/dist/checks/testing/mock-implementations-in-production.d.ts.map +1 -0
- package/dist/checks/testing/mock-implementations-in-production.js +211 -0
- package/dist/checks/testing/mock-implementations-in-production.js.map +1 -0
- package/dist/display/architecture.d.ts +9 -0
- package/dist/display/architecture.d.ts.map +1 -0
- package/dist/display/architecture.js +18 -0
- package/dist/display/architecture.js.map +1 -0
- package/dist/display/index.d.ts +20 -0
- package/dist/display/index.d.ts.map +1 -0
- package/dist/display/index.js +30 -0
- package/dist/display/index.js.map +1 -0
- package/dist/display/quality.d.ts +7 -0
- package/dist/display/quality.d.ts.map +1 -0
- package/dist/display/quality.js +39 -0
- package/dist/display/quality.js.map +1 -0
- package/dist/display/resilience.d.ts +7 -0
- package/dist/display/resilience.d.ts.map +1 -0
- package/dist/display/resilience.js +13 -0
- package/dist/display/resilience.js.map +1 -0
- package/dist/display/security-testing.d.ts +9 -0
- package/dist/display/security-testing.d.ts.map +1 -0
- package/dist/display/security-testing.js +14 -0
- package/dist/display/security-testing.js.map +1 -0
- package/dist/display/types.d.ts +6 -0
- package/dist/display/types.d.ts.map +1 -0
- package/dist/display/types.js +6 -0
- package/dist/display/types.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,1700 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Targeted behavior fixture suite for the lowest-coverage checks.
|
|
3
|
+
*
|
|
4
|
+
* These tests exercise specific analyze()/analyzeAll() branches that the
|
|
5
|
+
* broad fixture-driven suites don't reach:
|
|
6
|
+
* - database-index-coverage: where-clause + raw-query violation arms
|
|
7
|
+
* - package-json-exports-field: relative-path filtering + violation push
|
|
8
|
+
* - missing-type-exports: undeclared deep-import violation + barrel fallback
|
|
9
|
+
* - typescript-frontend: real tsc run that emits parseable errors
|
|
10
|
+
* - display/index: getCheckIcon/getCheckDisplayName known + fallback paths
|
|
11
|
+
*
|
|
12
|
+
* The analyzeAll checks filter `files.paths` on `packages/` / `services/`
|
|
13
|
+
* prefixes, so the fixtures are written under a temp root that we `chdir`
|
|
14
|
+
* into and the checks are run with RELATIVE `targetFiles` — the engine's
|
|
15
|
+
* FileAccessor then resolves `files.read('packages/...')` against cwd.
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
18
|
+
import { tmpdir } from 'node:os';
|
|
19
|
+
import { dirname, join } from 'node:path';
|
|
20
|
+
import { LanguageRegistry, RunScope, runWithScope, runWithScopeSync } from '@opensip-cli/core';
|
|
21
|
+
import { fileCache } from '@opensip-cli/fitness';
|
|
22
|
+
import { typescriptAdapter } from '@opensip-cli/lang-typescript';
|
|
23
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
24
|
+
import { analyzeNullSafety } from '../checks/quality/data-integrity/null-safety.js';
|
|
25
|
+
import { analyzeFileForToctou } from '../checks/quality/patterns/toctou-race-condition.js';
|
|
26
|
+
import { analyzeCallbackInvocationSafe } from '../checks/resilience/callback-invocation-safe.js';
|
|
27
|
+
import { analyzeContextLeakage } from '../checks/resilience/context-leakage.js';
|
|
28
|
+
import { analyzeSqlInjection } from '../checks/security/sql-injection.js';
|
|
29
|
+
import { getCheckDisplayName, getCheckIcon } from '../display/index.js';
|
|
30
|
+
import { checks } from '../index.js';
|
|
31
|
+
// Production simulation: register the TS adapter (see behavior-fixtures.test.ts).
|
|
32
|
+
const langRegistry = new LanguageRegistry();
|
|
33
|
+
langRegistry.register(typescriptAdapter);
|
|
34
|
+
const testScope = new RunScope({ languages: langRegistry });
|
|
35
|
+
// The vitest process cwd is the checks-typescript package dir; captured at
|
|
36
|
+
// module load so the tsc fixture can locate the monorepo root reliably even
|
|
37
|
+
// after other suites chdir into temp roots.
|
|
38
|
+
const originalCwdAtModuleLoad = process.cwd();
|
|
39
|
+
let root;
|
|
40
|
+
let originalCwd;
|
|
41
|
+
function findCheck(slug) {
|
|
42
|
+
const c = checks.find((x) => x.config.slug === slug);
|
|
43
|
+
if (!c)
|
|
44
|
+
throw new Error(`check not found: ${slug}`);
|
|
45
|
+
return c;
|
|
46
|
+
}
|
|
47
|
+
/** Write a file at a path relative to the temp root. */
|
|
48
|
+
function fx(rel, content) {
|
|
49
|
+
const abs = join(root, rel);
|
|
50
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
51
|
+
writeFileSync(abs, content);
|
|
52
|
+
return abs;
|
|
53
|
+
}
|
|
54
|
+
/** Build N leaf modules under `src/<prefix>/` and return their absolute paths. */
|
|
55
|
+
function buildLeaves(prefix, count) {
|
|
56
|
+
const paths = [];
|
|
57
|
+
for (let i = 0; i < count; i++) {
|
|
58
|
+
paths.push(fx(`src/${prefix}/leaf${i}.ts`, `export const v${i} = ${i}`));
|
|
59
|
+
}
|
|
60
|
+
return paths;
|
|
61
|
+
}
|
|
62
|
+
/** Produce `count` relative import statements targeting the leaf modules. */
|
|
63
|
+
function importLines(prefix, count) {
|
|
64
|
+
const lines = [];
|
|
65
|
+
for (let i = 0; i < count; i++) {
|
|
66
|
+
lines.push(`import { v${i} } from "./${prefix}/leaf${i}.js"`);
|
|
67
|
+
}
|
|
68
|
+
return lines.join('\n');
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Run a check with RELATIVE target paths. We chdir into the temp root and
|
|
72
|
+
* prewarm/read relative paths so the analyzeAll path-prefix filters match.
|
|
73
|
+
*/
|
|
74
|
+
async function runRelative(slug, relPaths) {
|
|
75
|
+
const check = findCheck(slug);
|
|
76
|
+
await fileCache.prewarm(root, ['**/*']);
|
|
77
|
+
return runWithScope(testScope, () => check.run(root, { targetFiles: relPaths }));
|
|
78
|
+
}
|
|
79
|
+
/** Run a check with absolute target paths (analyze-mode checks). */
|
|
80
|
+
async function runAbsolute(slug, absPaths) {
|
|
81
|
+
const check = findCheck(slug);
|
|
82
|
+
await fileCache.prewarm(root, ['**/*']);
|
|
83
|
+
return runWithScope(testScope, () => check.run(root, { targetFiles: absPaths }));
|
|
84
|
+
}
|
|
85
|
+
beforeEach(() => {
|
|
86
|
+
originalCwd = process.cwd();
|
|
87
|
+
root = mkdtempSync(join(tmpdir(), 'opensip-cov-push-'));
|
|
88
|
+
});
|
|
89
|
+
afterEach(() => {
|
|
90
|
+
fileCache.clear();
|
|
91
|
+
process.chdir(originalCwd);
|
|
92
|
+
rmSync(root, { recursive: true, force: true });
|
|
93
|
+
});
|
|
94
|
+
// ===========================================================================
|
|
95
|
+
// database-index-coverage — analyze() arms (analyze-mode, absolute paths)
|
|
96
|
+
// ===========================================================================
|
|
97
|
+
describe('database-index-coverage — analyze branches', () => {
|
|
98
|
+
it('returns nothing for non-repository files', async () => {
|
|
99
|
+
const abs = fx('src/services/foo.ts', `repo.find({ where: { description: 'x' } })`);
|
|
100
|
+
const result = await runAbsolute('database-index-coverage', [abs]);
|
|
101
|
+
expect(result.signals).toHaveLength(0);
|
|
102
|
+
});
|
|
103
|
+
it('flags a find() where-clause referencing an unindexed risky column', async () => {
|
|
104
|
+
const abs = fx('src/database/user-repository.ts', [
|
|
105
|
+
'export class UserRepository {',
|
|
106
|
+
' load(repo: any) {',
|
|
107
|
+
" return repo.find({ where: { description: 'abc' } })",
|
|
108
|
+
' }',
|
|
109
|
+
'}',
|
|
110
|
+
].join('\n'));
|
|
111
|
+
const result = await runAbsolute('database-index-coverage', [abs]);
|
|
112
|
+
expect(result.signals.length).toBeGreaterThan(0);
|
|
113
|
+
expect(result.signals.some((s) => s.message.includes('description'))).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
it('does NOT flag a find() where-clause on an indexed column', async () => {
|
|
116
|
+
const abs = fx('src/repositories/account.repository.ts', `export const f = (repo: any) => repo.findOne({ where: { id: 1 } })`);
|
|
117
|
+
const result = await runAbsolute('database-index-coverage', [abs]);
|
|
118
|
+
expect(result.signals).toHaveLength(0);
|
|
119
|
+
});
|
|
120
|
+
it('skips find() calls whose first argument is not an object literal', async () => {
|
|
121
|
+
const abs = fx('src/repositories/x.repository.ts', `export const f = (repo: any, opts: any) => repo.find(opts)`);
|
|
122
|
+
const result = await runAbsolute('database-index-coverage', [abs]);
|
|
123
|
+
expect(result.signals).toHaveLength(0);
|
|
124
|
+
});
|
|
125
|
+
it('skips find() calls with an object that has no where property', async () => {
|
|
126
|
+
const abs = fx('src/repositories/x.repository.ts', `export const f = (repo: any) => repo.find({ take: 5 })`);
|
|
127
|
+
const result = await runAbsolute('database-index-coverage', [abs]);
|
|
128
|
+
expect(result.signals).toHaveLength(0);
|
|
129
|
+
});
|
|
130
|
+
it('flags a raw query() using LIKE with a leading wildcard', async () => {
|
|
131
|
+
const abs = fx('src/database/search.ts', String.raw `export const f = (db: any) => db.query("SELECT id FROM t WHERE name LIKE '%abc%'")`);
|
|
132
|
+
const result = await runAbsolute('database-index-coverage', [abs]);
|
|
133
|
+
expect(result.signals.some((s) => s.message.includes('LIKE'))).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
it('flags an unbounded SELECT * query() with no LIMIT', async () => {
|
|
136
|
+
const abs = fx('src/database/dump.ts', String.raw `export const f = (db: any) => db.query("SELECT * FROM users")`);
|
|
137
|
+
const result = await runAbsolute('database-index-coverage', [abs]);
|
|
138
|
+
expect(result.signals.some((s) => s.message.includes('SELECT *'))).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
it('does NOT flag SELECT * when bounded by LIMIT', async () => {
|
|
141
|
+
const abs = fx('src/database/dump2.ts', String.raw `export const f = (db: any) => db.query("SELECT * FROM users LIMIT 10")`);
|
|
142
|
+
const result = await runAbsolute('database-index-coverage', [abs]);
|
|
143
|
+
expect(result.signals.some((s) => s.message.includes('SELECT *'))).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
it('skips raw query() whose argument is not a string literal', async () => {
|
|
146
|
+
const abs = fx('src/database/dynamic.ts', `export const f = (db: any, sql: string) => db.query(sql)`);
|
|
147
|
+
const result = await runAbsolute('database-index-coverage', [abs]);
|
|
148
|
+
expect(result.signals).toHaveLength(0);
|
|
149
|
+
});
|
|
150
|
+
it('ignores method calls that are neither find nor raw query methods', async () => {
|
|
151
|
+
const abs = fx('src/repositories/x.repository.ts', `export const f = (repo: any) => repo.save({ description: 'x' })`);
|
|
152
|
+
const result = await runAbsolute('database-index-coverage', [abs]);
|
|
153
|
+
expect(result.signals).toHaveLength(0);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
// ===========================================================================
|
|
157
|
+
// package-json-exports-field — analyzeAll relative-path filtering
|
|
158
|
+
// ===========================================================================
|
|
159
|
+
describe('package-json-exports-field — analyzeAll branches', () => {
|
|
160
|
+
it('flags a packages/* package.json missing an exports field', async () => {
|
|
161
|
+
fx('packages/foo/package.json', JSON.stringify({ name: '@scope/foo', version: '1.0.0' }));
|
|
162
|
+
process.chdir(root);
|
|
163
|
+
const result = await runRelative('package-json-exports-field', ['packages/foo/package.json']);
|
|
164
|
+
expect(result.signals).toHaveLength(1);
|
|
165
|
+
expect(result.signals[0]?.message).toContain('@scope/foo');
|
|
166
|
+
});
|
|
167
|
+
it('does NOT flag a packages/* package with an exports field', async () => {
|
|
168
|
+
fx('packages/bar/package.json', JSON.stringify({ name: '@scope/bar', exports: { '.': './dist/index.js' } }));
|
|
169
|
+
process.chdir(root);
|
|
170
|
+
const result = await runRelative('package-json-exports-field', ['packages/bar/package.json']);
|
|
171
|
+
expect(result.signals).toHaveLength(0);
|
|
172
|
+
});
|
|
173
|
+
it('skips a private services/* package that is not under packages/', async () => {
|
|
174
|
+
fx('services/api/package.json', JSON.stringify({ name: '@scope/api', private: true }));
|
|
175
|
+
process.chdir(root);
|
|
176
|
+
const result = await runRelative('package-json-exports-field', ['services/api/package.json']);
|
|
177
|
+
expect(result.signals).toHaveLength(0);
|
|
178
|
+
});
|
|
179
|
+
it('still flags a services/* package that is not private and lacks exports', async () => {
|
|
180
|
+
fx('services/api/package.json', JSON.stringify({ name: '@scope/api' }));
|
|
181
|
+
process.chdir(root);
|
|
182
|
+
const result = await runRelative('package-json-exports-field', ['services/api/package.json']);
|
|
183
|
+
expect(result.signals).toHaveLength(1);
|
|
184
|
+
expect(result.signals[0]?.message).toContain('@scope/api');
|
|
185
|
+
});
|
|
186
|
+
it('falls back to the file path when the package has no name', async () => {
|
|
187
|
+
fx('packages/anon/package.json', JSON.stringify({ version: '1.0.0' }));
|
|
188
|
+
process.chdir(root);
|
|
189
|
+
const result = await runRelative('package-json-exports-field', ['packages/anon/package.json']);
|
|
190
|
+
expect(result.signals).toHaveLength(1);
|
|
191
|
+
expect(result.signals[0]?.message).toContain('packages/anon/package.json');
|
|
192
|
+
});
|
|
193
|
+
it('ignores root, node_modules, and non-package.json paths', async () => {
|
|
194
|
+
fx('package.json', JSON.stringify({ name: 'root' }));
|
|
195
|
+
fx('packages/x/node_modules/dep/package.json', JSON.stringify({ name: 'dep' }));
|
|
196
|
+
fx('packages/x/src/index.ts', 'export const x = 1');
|
|
197
|
+
process.chdir(root);
|
|
198
|
+
const result = await runRelative('package-json-exports-field', [
|
|
199
|
+
'package.json',
|
|
200
|
+
'packages/x/node_modules/dep/package.json',
|
|
201
|
+
'packages/x/src/index.ts',
|
|
202
|
+
]);
|
|
203
|
+
expect(result.signals).toHaveLength(0);
|
|
204
|
+
});
|
|
205
|
+
it('skips package.json files that are not valid JSON', async () => {
|
|
206
|
+
fx('packages/broken/package.json', '{ not valid json');
|
|
207
|
+
process.chdir(root);
|
|
208
|
+
const result = await runRelative('package-json-exports-field', [
|
|
209
|
+
'packages/broken/package.json',
|
|
210
|
+
]);
|
|
211
|
+
expect(result.signals).toHaveLength(0);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
// ===========================================================================
|
|
215
|
+
// missing-type-exports — analyzeAll undeclared deep-import detection
|
|
216
|
+
// ===========================================================================
|
|
217
|
+
describe('missing-type-exports — analyzeAll branches', () => {
|
|
218
|
+
it('flags a deep import of a subpath not declared in the package exports map', async () => {
|
|
219
|
+
fx('packages/foo/package.json', JSON.stringify({ name: '@scope/foo', exports: { '.': './dist/index.js' } }));
|
|
220
|
+
fx('packages/consumer/src/uses.ts', [
|
|
221
|
+
'import type { Internal } from "@scope/foo/internal"',
|
|
222
|
+
'export const x: Internal = {} as Internal',
|
|
223
|
+
].join('\n'));
|
|
224
|
+
process.chdir(root);
|
|
225
|
+
const result = await runRelative('missing-type-exports', ['packages/consumer/src/uses.ts']);
|
|
226
|
+
expect(result.signals).toHaveLength(1);
|
|
227
|
+
expect(result.signals[0]?.message).toContain('@scope/foo');
|
|
228
|
+
});
|
|
229
|
+
it('does NOT flag a deep import that IS declared as a subpath export', async () => {
|
|
230
|
+
fx('packages/foo/package.json', JSON.stringify({
|
|
231
|
+
name: '@scope/foo',
|
|
232
|
+
exports: { '.': './dist/index.js', './internal': './dist/internal.js' },
|
|
233
|
+
}));
|
|
234
|
+
fx('packages/consumer/src/uses.ts', 'import { thing } from "@scope/foo/internal"\nexport const y = thing');
|
|
235
|
+
process.chdir(root);
|
|
236
|
+
const result = await runRelative('missing-type-exports', ['packages/consumer/src/uses.ts']);
|
|
237
|
+
expect(result.signals).toHaveLength(0);
|
|
238
|
+
});
|
|
239
|
+
it('does NOT flag a deep import matching a wildcard subpath export', async () => {
|
|
240
|
+
fx('packages/foo/package.json', JSON.stringify({
|
|
241
|
+
name: '@scope/foo',
|
|
242
|
+
exports: { './plugins/*': './dist/plugins/*.js' },
|
|
243
|
+
}));
|
|
244
|
+
fx('packages/consumer/src/uses.ts', 'import { p } from "@scope/foo/plugins/alpha"\nexport const z = p');
|
|
245
|
+
process.chdir(root);
|
|
246
|
+
const result = await runRelative('missing-type-exports', ['packages/consumer/src/uses.ts']);
|
|
247
|
+
expect(result.signals).toHaveLength(0);
|
|
248
|
+
});
|
|
249
|
+
it('treats a name re-exported by some barrel as public when the package has no exports map', async () => {
|
|
250
|
+
fx('packages/foo/package.json', JSON.stringify({ name: '@scope/foo' }));
|
|
251
|
+
fx('packages/foo/src/index.ts', 'export { PublicThing } from "./public.js"');
|
|
252
|
+
fx('packages/consumer/src/uses.ts', 'import { PublicThing } from "@scope/foo/deep"\nexport const w = PublicThing');
|
|
253
|
+
process.chdir(root);
|
|
254
|
+
const result = await runRelative('missing-type-exports', [
|
|
255
|
+
'packages/foo/src/index.ts',
|
|
256
|
+
'packages/consumer/src/uses.ts',
|
|
257
|
+
]);
|
|
258
|
+
expect(result.signals).toHaveLength(0);
|
|
259
|
+
});
|
|
260
|
+
it('flags a deep import whose name is not surfaced by any barrel and has no exports map', async () => {
|
|
261
|
+
fx('packages/foo/package.json', JSON.stringify({ name: '@scope/foo' }));
|
|
262
|
+
fx('packages/foo/src/index.ts', 'export const Unrelated = 1');
|
|
263
|
+
fx('packages/consumer/src/uses.ts', 'import { Hidden } from "@scope/foo/deep"\nexport const u = Hidden');
|
|
264
|
+
process.chdir(root);
|
|
265
|
+
const result = await runRelative('missing-type-exports', [
|
|
266
|
+
'packages/foo/src/index.ts',
|
|
267
|
+
'packages/consumer/src/uses.ts',
|
|
268
|
+
]);
|
|
269
|
+
expect(result.signals).toHaveLength(1);
|
|
270
|
+
expect(result.signals[0]?.message).toContain('Hidden');
|
|
271
|
+
});
|
|
272
|
+
it('ignores root imports, relative imports, test files, and dist paths', async () => {
|
|
273
|
+
fx('packages/foo/package.json', JSON.stringify({ name: '@scope/foo', exports: { '.': './dist/index.js' } }));
|
|
274
|
+
fx('packages/consumer/src/root.ts', 'import { a } from "@scope/foo"\nexport const r = a');
|
|
275
|
+
fx('packages/consumer/src/rel.ts', 'import { b } from "./local"\nexport const s = b');
|
|
276
|
+
fx('packages/consumer/src/uses.test.ts', 'import { c } from "@scope/foo/deep"\nexport const t = c');
|
|
277
|
+
fx('packages/consumer/dist/uses.ts', 'import { d } from "@scope/foo/deep"\nexport const v = d');
|
|
278
|
+
process.chdir(root);
|
|
279
|
+
const result = await runRelative('missing-type-exports', [
|
|
280
|
+
'packages/consumer/src/root.ts',
|
|
281
|
+
'packages/consumer/src/rel.ts',
|
|
282
|
+
'packages/consumer/src/uses.test.ts',
|
|
283
|
+
'packages/consumer/dist/uses.ts',
|
|
284
|
+
]);
|
|
285
|
+
expect(result.signals).toHaveLength(0);
|
|
286
|
+
});
|
|
287
|
+
it('handles the conditional-only exports shorthand (root is public)', async () => {
|
|
288
|
+
fx('packages/foo/package.json', JSON.stringify({
|
|
289
|
+
name: '@scope/foo',
|
|
290
|
+
exports: { import: './dist/index.js', default: './dist/index.cjs' },
|
|
291
|
+
}));
|
|
292
|
+
fx('packages/consumer/src/uses.ts', 'import { deep } from "@scope/foo/deep"\nexport const q = deep');
|
|
293
|
+
process.chdir(root);
|
|
294
|
+
const result = await runRelative('missing-type-exports', ['packages/consumer/src/uses.ts']);
|
|
295
|
+
expect(result.signals).toHaveLength(1);
|
|
296
|
+
expect(result.signals[0]?.message).toContain('deep');
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
// ===========================================================================
|
|
300
|
+
// typescript-frontend — real tsc run emitting parseable errors
|
|
301
|
+
// ===========================================================================
|
|
302
|
+
describe('typescript-frontend — analyzeAll over a real app fixture', () => {
|
|
303
|
+
// The check shells out to `npx tsc --noEmit` inside each app directory,
|
|
304
|
+
// and `findRepoRoot` walks up from the first file path until it finds an
|
|
305
|
+
// `apps/` directory. For `npx tsc` to resolve the real compiler we must
|
|
306
|
+
// sit inside this monorepo's node_modules tree, so the fixture lives in a
|
|
307
|
+
// uniquely-named `apps/<id>/` under the repo root and is cleaned up after.
|
|
308
|
+
const repoRoot = join(originalCwdAtModuleLoad, '..', '..', '..');
|
|
309
|
+
let appsDir;
|
|
310
|
+
beforeEach(() => {
|
|
311
|
+
// Remove any cov-tsf-* fixtures a prior *interrupted* run left behind (the
|
|
312
|
+
// afterEach below cleans up on normal completion, but SIGINT bypasses it).
|
|
313
|
+
// We also prune an empty `apps/` parent (which this test may create via
|
|
314
|
+
// recursive mkdir) so that `pnpm test` / `pnpm fit` runs never leave an
|
|
315
|
+
// empty apps/ directory in the project root.
|
|
316
|
+
// This keeps the working tree — and a subsequent `pnpm fit` — free of stale
|
|
317
|
+
// detritus that would otherwise be analyzed as real source.
|
|
318
|
+
const appsRoot = join(repoRoot, 'apps');
|
|
319
|
+
try {
|
|
320
|
+
if (existsSync(appsRoot)) {
|
|
321
|
+
let hasNonCov = false;
|
|
322
|
+
for (const entry of readdirSync(appsRoot)) {
|
|
323
|
+
if (entry.startsWith('cov-tsf-')) {
|
|
324
|
+
rmSync(join(appsRoot, entry), { recursive: true, force: true });
|
|
325
|
+
}
|
|
326
|
+
else if (!entry.startsWith('.')) {
|
|
327
|
+
hasNonCov = true;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (!hasNonCov) {
|
|
331
|
+
// After cleaning only our temp subdirs, the parent is now empty.
|
|
332
|
+
// Remove it so we don't leave an empty apps/ behind from prior runs.
|
|
333
|
+
try {
|
|
334
|
+
rmSync(appsRoot, { recursive: true, force: true });
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
// ignore
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
// apps/ may not exist yet — nothing to clean.
|
|
344
|
+
}
|
|
345
|
+
appsDir = join(repoRoot, 'apps', `cov-tsf-${process.pid}-${Date.now()}`);
|
|
346
|
+
mkdirSync(join(appsDir, 'src'), { recursive: true });
|
|
347
|
+
writeFileSync(join(appsDir, 'tsconfig.json'), JSON.stringify({
|
|
348
|
+
compilerOptions: {
|
|
349
|
+
noEmit: true,
|
|
350
|
+
strict: true,
|
|
351
|
+
skipLibCheck: true,
|
|
352
|
+
types: [],
|
|
353
|
+
moduleResolution: 'node',
|
|
354
|
+
},
|
|
355
|
+
include: ['src'],
|
|
356
|
+
}));
|
|
357
|
+
writeFileSync(join(appsDir, 'src', 'index.ts'), 'export const n: number = "not a number"\n');
|
|
358
|
+
});
|
|
359
|
+
afterEach(() => {
|
|
360
|
+
rmSync(appsDir, { recursive: true, force: true });
|
|
361
|
+
// Remove the temp `apps/` dir if it is now empty (we may have created the
|
|
362
|
+
// parent as a side-effect of the recursive mkdir for the fixture).
|
|
363
|
+
// Only remove if empty to avoid touching a real apps/ layout a user has.
|
|
364
|
+
const appsRoot = join(repoRoot, 'apps');
|
|
365
|
+
try {
|
|
366
|
+
if (existsSync(appsRoot)) {
|
|
367
|
+
const entries = readdirSync(appsRoot).filter((e) => !e.startsWith('.'));
|
|
368
|
+
if (entries.length === 0) {
|
|
369
|
+
rmSync(appsRoot, { recursive: true, force: true });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
// apps/ may be non-empty (a real one) or removal failed — leave it.
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
it('reports TypeScript compilation errors parsed from tsc output', async () => {
|
|
378
|
+
const fileA = join(appsDir, 'src', 'index.ts');
|
|
379
|
+
const check = findCheck('typescript-frontend');
|
|
380
|
+
const result = await runWithScope(testScope, () => check.run(repoRoot, { targetFiles: [fileA] }));
|
|
381
|
+
expect(result.signals.length).toBeGreaterThan(0);
|
|
382
|
+
// Errors are parsed from `tsc` output into per-error signals carrying a
|
|
383
|
+
// typescript.tv suggestion (the parseErrors → errorsToViolations path).
|
|
384
|
+
const tsErr = result.signals.find((s) => s.suggestion?.includes('typescript.tv'));
|
|
385
|
+
expect(tsErr).toBeDefined();
|
|
386
|
+
expect(tsErr?.message).toMatch(/TS\d+/);
|
|
387
|
+
}, 120_000);
|
|
388
|
+
});
|
|
389
|
+
// ===========================================================================
|
|
390
|
+
// null-safety — direct analyzeNullSafety skip arms
|
|
391
|
+
// ===========================================================================
|
|
392
|
+
function runNullSafety(content, path = 'packages/x/src/svc.ts') {
|
|
393
|
+
return runWithScopeSync(testScope, () => analyzeNullSafety(content, path));
|
|
394
|
+
}
|
|
395
|
+
describe('null-safety — analyze branches', () => {
|
|
396
|
+
const run = runNullSafety;
|
|
397
|
+
it('flags an unguarded property access on an element-access result', () => {
|
|
398
|
+
const v = run('export const name = items[0].displayName');
|
|
399
|
+
expect(v.some((x) => x.message.includes('displayName'))).toBe(true);
|
|
400
|
+
});
|
|
401
|
+
it('does NOT flag property access guarded by an enclosing && on the base', () => {
|
|
402
|
+
const v = run('export const name = items[0] && items[0].displayName');
|
|
403
|
+
expect(v.some((x) => x.message.includes('displayName'))).toBe(false);
|
|
404
|
+
});
|
|
405
|
+
it('does NOT flag property access guarded by a ternary condition', () => {
|
|
406
|
+
const v = run('export const name = items[0] ? items[0].displayName : fallback');
|
|
407
|
+
expect(v.some((x) => x.message.includes('displayName'))).toBe(false);
|
|
408
|
+
});
|
|
409
|
+
it('does NOT flag property access originating from this', () => {
|
|
410
|
+
const src = ['export class C {', ' read() { return this.pick().displayName }', '}'].join('\n');
|
|
411
|
+
expect(run(src)).toHaveLength(0);
|
|
412
|
+
});
|
|
413
|
+
it('does NOT flag access to a safe member like length on an element-access base', () => {
|
|
414
|
+
const v = run('export const n = items[0].length');
|
|
415
|
+
expect(v.some((x) => x.message.includes('length'))).toBe(false);
|
|
416
|
+
});
|
|
417
|
+
it('does NOT flag a line already using optional chaining elsewhere', () => {
|
|
418
|
+
const v = run('export const name = items[0]?.displayName ?? items[0].displayName');
|
|
419
|
+
expect(v).toHaveLength(0);
|
|
420
|
+
});
|
|
421
|
+
it('skips files on a safe-by-construction null path (*-schema.ts)', () => {
|
|
422
|
+
const v = run('export const name = items[0].displayName', 'packages/x/src/user-schema.ts');
|
|
423
|
+
expect(v).toHaveLength(0);
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
// ===========================================================================
|
|
427
|
+
// error-handling-quality — analyze() catch/Result violation arms
|
|
428
|
+
// ===========================================================================
|
|
429
|
+
async function runErrorHandling(src, rel = 'packages/x/src/svc.ts') {
|
|
430
|
+
const abs = fx(rel, src);
|
|
431
|
+
return runAbsolute('error-handling-quality', [abs]);
|
|
432
|
+
}
|
|
433
|
+
describe('error-handling-quality — analyze branches', () => {
|
|
434
|
+
const run = runErrorHandling;
|
|
435
|
+
it('skips test files', async () => {
|
|
436
|
+
const src = 'export function f() { try { work() } catch (e) {} }';
|
|
437
|
+
const result = await run(src, 'packages/x/src/svc.test.ts');
|
|
438
|
+
expect(result.signals).toHaveLength(0);
|
|
439
|
+
});
|
|
440
|
+
it('skips files without catch/isErr/.match( (quick filter)', async () => {
|
|
441
|
+
const result = await run('export const x = 1');
|
|
442
|
+
expect(result.signals).toHaveLength(0);
|
|
443
|
+
});
|
|
444
|
+
it('flags an empty catch block', async () => {
|
|
445
|
+
const src = 'export function f() { try { work() } catch (e) {} }';
|
|
446
|
+
const result = await run(src);
|
|
447
|
+
expect(result.signals.some((s) => s.message.toLowerCase().includes('catch'))).toBe(true);
|
|
448
|
+
});
|
|
449
|
+
it('flags a catch block that silently returns a sentinel value', async () => {
|
|
450
|
+
const src = 'export function f() { try { return work() } catch (e) { return null } }';
|
|
451
|
+
const result = await run(src);
|
|
452
|
+
expect(result.signals.some((s) => s.message.includes('Catch returns null'))).toBe(true);
|
|
453
|
+
});
|
|
454
|
+
it('does NOT flag a catch block that logs the error', async () => {
|
|
455
|
+
const src = 'export function f() { try { work() } catch (e) { logger.warn(e) } }';
|
|
456
|
+
const result = await run(src);
|
|
457
|
+
expect(result.signals).toHaveLength(0);
|
|
458
|
+
});
|
|
459
|
+
it('does NOT flag a catch block that re-throws', async () => {
|
|
460
|
+
const src = 'export function f() { try { work() } catch (e) { throw e } }';
|
|
461
|
+
const result = await run(src);
|
|
462
|
+
expect(result.signals).toHaveLength(0);
|
|
463
|
+
});
|
|
464
|
+
it('flags a Result.isErr() branch that silently returns a sentinel', async () => {
|
|
465
|
+
const src = [
|
|
466
|
+
'export function f(r: any) {',
|
|
467
|
+
' if (r.isErr()) { return false }',
|
|
468
|
+
' return r.value',
|
|
469
|
+
'}',
|
|
470
|
+
].join('\n');
|
|
471
|
+
const result = await run(src);
|
|
472
|
+
expect(result.signals.some((s) => s.message.includes('silently discarded'))).toBe(true);
|
|
473
|
+
});
|
|
474
|
+
it('flags a mapErr() callback that does not log', async () => {
|
|
475
|
+
// The quick filter requires a catch/isErr/.match( token; the logging
|
|
476
|
+
// catch satisfies it without contributing a violation of its own.
|
|
477
|
+
const src = [
|
|
478
|
+
'export function f(result: any) {',
|
|
479
|
+
' try { warmup() } catch (e) { logger.warn(e) }',
|
|
480
|
+
' return result.mapErr((e: any) => defaultValue)',
|
|
481
|
+
'}',
|
|
482
|
+
].join('\n');
|
|
483
|
+
const out = await run(src);
|
|
484
|
+
expect(out.signals.some((s) => s.message.includes('mapErr'))).toBe(true);
|
|
485
|
+
});
|
|
486
|
+
it('flags a match() error handler that does not log', async () => {
|
|
487
|
+
const src = 'export const out = result.match((v: any) => v, (e: any) => fallback)';
|
|
488
|
+
const out = await run(src);
|
|
489
|
+
expect(out.signals.some((s) => s.message.includes('match'))).toBe(true);
|
|
490
|
+
});
|
|
491
|
+
it('flags an unsafe `as Error` cast in a catch without an instanceof guard', async () => {
|
|
492
|
+
const src = 'export function f() { try { work() } catch (e) { report((e as Error).message) } }';
|
|
493
|
+
const result = await run(src);
|
|
494
|
+
expect(result.signals.some((s) => s.message.includes('as Error'))).toBe(true);
|
|
495
|
+
});
|
|
496
|
+
it('does NOT flag an `as Error` cast guarded by instanceof Error', async () => {
|
|
497
|
+
const src = 'export function f() { try { work() } catch (e) { if (e instanceof Error) report((e as Error).message) } }';
|
|
498
|
+
const result = await run(src);
|
|
499
|
+
expect(result.signals.some((s) => s.message.includes('as Error'))).toBe(false);
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
// ===========================================================================
|
|
503
|
+
// sql-injection — direct analyzeSqlInjection tagged-template arms
|
|
504
|
+
// ===========================================================================
|
|
505
|
+
function analyzeSql(src) {
|
|
506
|
+
return analyzeSqlInjection(src, 'packages/x/src/repo.ts');
|
|
507
|
+
}
|
|
508
|
+
describe('sql-injection — analyze branches', () => {
|
|
509
|
+
const analyze = analyzeSql;
|
|
510
|
+
it('flags a raw query with template interpolation', () => {
|
|
511
|
+
const src = 'export const r = (db: any, id: string) => db.query(`SELECT * FROM users WHERE id = ${id}`)';
|
|
512
|
+
expect(analyze(src).some((v) => v.message.includes('SQL injection'))).toBe(true);
|
|
513
|
+
});
|
|
514
|
+
it('does NOT flag a safe `sql` tagged template', () => {
|
|
515
|
+
const src = 'export const r = (id: string) => sql`SELECT * FROM users WHERE id = ${id}`';
|
|
516
|
+
expect(analyze(src)).toHaveLength(0);
|
|
517
|
+
});
|
|
518
|
+
it('does NOT flag a safe property-access tagged template (db.sql`...`)', () => {
|
|
519
|
+
const src = 'export const r = (db: any, id: string) => db.sql`SELECT * FROM users WHERE id = ${id}`';
|
|
520
|
+
expect(analyze(src)).toHaveLength(0);
|
|
521
|
+
});
|
|
522
|
+
it('truncates the match text for a very long interpolated query', () => {
|
|
523
|
+
const filler = 'x'.repeat(260);
|
|
524
|
+
const src = `export const r = (db: any, id: string) => db.query(\`SELECT ${filler} FROM users WHERE id = \${id}\`)`;
|
|
525
|
+
const out = analyze(src);
|
|
526
|
+
expect(out.some((v) => v.message.includes('SQL injection'))).toBe(true);
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
// ===========================================================================
|
|
530
|
+
// database-schema-validation — analyze() TypeORM entity arms
|
|
531
|
+
// ===========================================================================
|
|
532
|
+
describe('database-schema-validation — analyze branches', () => {
|
|
533
|
+
const P = 'packages/x/src/user.entity.ts';
|
|
534
|
+
async function run(src, rel = P) {
|
|
535
|
+
const abs = fx(rel, src);
|
|
536
|
+
return runAbsolute('database-schema-validation', [abs]);
|
|
537
|
+
}
|
|
538
|
+
it('skips non-entity files', async () => {
|
|
539
|
+
const src = '@Entity()\nexport class User { @Column() name!: string }';
|
|
540
|
+
const result = await run(src, 'packages/x/src/user.ts');
|
|
541
|
+
expect(result.signals).toHaveLength(0);
|
|
542
|
+
});
|
|
543
|
+
it('skips entity-path files that contain no @Entity/@Table decorator', async () => {
|
|
544
|
+
const result = await run('export class User { name!: string }');
|
|
545
|
+
expect(result.signals).toHaveLength(0);
|
|
546
|
+
});
|
|
547
|
+
it('flags an entity missing a primary key and audit columns', async () => {
|
|
548
|
+
const src = ['@Entity()', 'export class User {', ' @Column() name!: string', '}'].join('\n');
|
|
549
|
+
const result = await run(src);
|
|
550
|
+
expect(result.signals.some((s) => s.message.toLowerCase().includes('primary'))).toBe(true);
|
|
551
|
+
expect(result.signals.some((s) => /createdAt|updatedAt/i.test(s.message))).toBe(true);
|
|
552
|
+
});
|
|
553
|
+
it('reports only the missing updatedAt audit column when createdAt exists', async () => {
|
|
554
|
+
const src = [
|
|
555
|
+
'@Entity()',
|
|
556
|
+
'export class User {',
|
|
557
|
+
' @PrimaryGeneratedColumn() id!: number',
|
|
558
|
+
' @CreateDateColumn() createdAt!: Date',
|
|
559
|
+
' @Column() name!: string',
|
|
560
|
+
'}',
|
|
561
|
+
].join('\n');
|
|
562
|
+
const result = await run(src);
|
|
563
|
+
expect(result.signals.some((s) => s.message.includes('updatedAt'))).toBe(true);
|
|
564
|
+
});
|
|
565
|
+
it('does NOT flag a complete entity (PK + both audit columns)', async () => {
|
|
566
|
+
const src = [
|
|
567
|
+
'@Entity()',
|
|
568
|
+
'export class User {',
|
|
569
|
+
' @PrimaryGeneratedColumn() id!: number',
|
|
570
|
+
' @CreateDateColumn() createdAt!: Date',
|
|
571
|
+
' @UpdateDateColumn() updatedAt!: Date',
|
|
572
|
+
' @Column() name!: string',
|
|
573
|
+
'}',
|
|
574
|
+
].join('\n');
|
|
575
|
+
const result = await run(src);
|
|
576
|
+
expect(result.signals).toHaveLength(0);
|
|
577
|
+
});
|
|
578
|
+
it('flags a nullable column declared without a default', async () => {
|
|
579
|
+
const src = [
|
|
580
|
+
'@Entity()',
|
|
581
|
+
'export class User {',
|
|
582
|
+
' @PrimaryGeneratedColumn() id!: number',
|
|
583
|
+
' @CreateDateColumn() createdAt!: Date',
|
|
584
|
+
' @UpdateDateColumn() updatedAt!: Date',
|
|
585
|
+
' @Column({ nullable: true }) nickname?: string',
|
|
586
|
+
'}',
|
|
587
|
+
].join('\n');
|
|
588
|
+
const result = await run(src);
|
|
589
|
+
expect(result.signals.some((s) => s.message.toLowerCase().includes('nullable'))).toBe(true);
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
// ===========================================================================
|
|
593
|
+
// context-leakage — direct analyzeContextLeakage branches
|
|
594
|
+
// ===========================================================================
|
|
595
|
+
describe('context-leakage — analyze branches', () => {
|
|
596
|
+
const P = 'packages/x/src/state.ts';
|
|
597
|
+
function run(content, path = P) {
|
|
598
|
+
return analyzeContextLeakage(content, path);
|
|
599
|
+
}
|
|
600
|
+
it('skips test files', () => {
|
|
601
|
+
expect(run('let activeContext: RequestContext | null = null', 'packages/x/src/state.test.ts')).toHaveLength(0);
|
|
602
|
+
});
|
|
603
|
+
it('skips files under dbos/steps/', () => {
|
|
604
|
+
expect(run('let activeContext: RequestContext | null = null', 'packages/x/src/dbos/steps/s.ts')).toHaveLength(0);
|
|
605
|
+
});
|
|
606
|
+
it('flags a module-level let with a *Context type', () => {
|
|
607
|
+
const v = run('let activeContext: RequestContext | null = null');
|
|
608
|
+
expect(v).toHaveLength(1);
|
|
609
|
+
expect(v[0]?.match).toContain('activeContext');
|
|
610
|
+
});
|
|
611
|
+
it('does NOT flag a const binding', () => {
|
|
612
|
+
expect(run('const activeContext: RequestContext = build()')).toHaveLength(0);
|
|
613
|
+
});
|
|
614
|
+
it('does NOT flag an AsyncLocalStorage-typed binding', () => {
|
|
615
|
+
expect(run('let store: AsyncLocalStorage<RequestContext> = new AsyncLocalStorage()')).toHaveLength(0);
|
|
616
|
+
});
|
|
617
|
+
it('does NOT flag a lazy-init metric instrument', () => {
|
|
618
|
+
expect(run('let requestCounter: Counter | null = null')).toHaveLength(0);
|
|
619
|
+
});
|
|
620
|
+
it('does NOT flag an OTel-imported Context type', () => {
|
|
621
|
+
const src = [
|
|
622
|
+
'import { Context } from "@opentelemetry/api"',
|
|
623
|
+
'let parentContext: Context | null = null',
|
|
624
|
+
].join('\n');
|
|
625
|
+
expect(run(src)).toHaveLength(0);
|
|
626
|
+
});
|
|
627
|
+
it('does NOT flag a non-contextual, non-typed let', () => {
|
|
628
|
+
expect(run('let total: number = 0')).toHaveLength(0);
|
|
629
|
+
});
|
|
630
|
+
it('does NOT flag a name-only contextual signal without a contextual type', () => {
|
|
631
|
+
expect(run('let requestCount: number = 0')).toHaveLength(0);
|
|
632
|
+
});
|
|
633
|
+
it('flags a non-readonly context field in a request-scoped class', () => {
|
|
634
|
+
const src = [
|
|
635
|
+
'export class Handler {',
|
|
636
|
+
' current!: RequestContext',
|
|
637
|
+
' handle(tenantId: string) { return tenantId }',
|
|
638
|
+
'}',
|
|
639
|
+
].join('\n');
|
|
640
|
+
const v = run(src);
|
|
641
|
+
expect(v.some((f) => f.match?.includes('current'))).toBe(true);
|
|
642
|
+
});
|
|
643
|
+
it('does NOT flag a readonly context field', () => {
|
|
644
|
+
const src = [
|
|
645
|
+
'export class Handler {',
|
|
646
|
+
' readonly current!: RequestContext',
|
|
647
|
+
' handle(tenantId: string) { return tenantId }',
|
|
648
|
+
'}',
|
|
649
|
+
].join('\n');
|
|
650
|
+
expect(run(src)).toHaveLength(0);
|
|
651
|
+
});
|
|
652
|
+
it('does NOT flag context fields on a class that is not request-scoped', () => {
|
|
653
|
+
const src = [
|
|
654
|
+
'export class Plain {',
|
|
655
|
+
' current!: RequestContext',
|
|
656
|
+
' compute(x: number) { return x }',
|
|
657
|
+
'}',
|
|
658
|
+
].join('\n');
|
|
659
|
+
expect(run(src)).toHaveLength(0);
|
|
660
|
+
});
|
|
661
|
+
});
|
|
662
|
+
// ===========================================================================
|
|
663
|
+
// throws-documentation — analyze() detection + skip arms
|
|
664
|
+
// ===========================================================================
|
|
665
|
+
describe('throws-documentation — analyze branches', () => {
|
|
666
|
+
const P = 'packages/x/src/svc.ts';
|
|
667
|
+
async function run(src, rel = P) {
|
|
668
|
+
const abs = fx(rel, src);
|
|
669
|
+
return runAbsolute('throws-documentation', [abs]);
|
|
670
|
+
}
|
|
671
|
+
it('skips test files', async () => {
|
|
672
|
+
const src = 'export function f() { throw new Error("x") }';
|
|
673
|
+
const result = await run(src, 'packages/x/src/svc.test.ts');
|
|
674
|
+
expect(result.signals).toHaveLength(0);
|
|
675
|
+
});
|
|
676
|
+
it('skips files without a throw statement (quick filter)', async () => {
|
|
677
|
+
const result = await run('export function f() { return 1 }');
|
|
678
|
+
expect(result.signals).toHaveLength(0);
|
|
679
|
+
});
|
|
680
|
+
it('flags a function that throws a plain Error without @throws JSDoc', async () => {
|
|
681
|
+
const src = 'export function risky() { throw new Error("boom") }';
|
|
682
|
+
const result = await run(src);
|
|
683
|
+
expect(result.signals.some((s) => s.message.includes('@throws'))).toBe(true);
|
|
684
|
+
});
|
|
685
|
+
it('does NOT flag a function that already has @throws JSDoc', async () => {
|
|
686
|
+
const src = [
|
|
687
|
+
'/** @throws {Error} when it fails */',
|
|
688
|
+
'export function risky() { throw new Error("boom") }',
|
|
689
|
+
].join('\n');
|
|
690
|
+
const result = await run(src);
|
|
691
|
+
expect(result.signals.some((s) => s.message.includes("'risky'"))).toBe(false);
|
|
692
|
+
});
|
|
693
|
+
it('does NOT flag a throw of a self-documenting typed error', async () => {
|
|
694
|
+
const src = 'export function risky() { throw new ValidationError("bad") }';
|
|
695
|
+
const result = await run(src);
|
|
696
|
+
expect(result.signals.some((s) => s.message.includes("'risky'"))).toBe(false);
|
|
697
|
+
});
|
|
698
|
+
it('does NOT flag a bare re-throw of a caught error variable', async () => {
|
|
699
|
+
const src = [
|
|
700
|
+
'export function risky() {',
|
|
701
|
+
' try { work() } catch (err) { throw err }',
|
|
702
|
+
'}',
|
|
703
|
+
].join('\n');
|
|
704
|
+
const result = await run(src);
|
|
705
|
+
expect(result.signals.some((s) => s.message.includes("'risky'"))).toBe(false);
|
|
706
|
+
});
|
|
707
|
+
it('does NOT flag an anonymous arrow callback that throws', async () => {
|
|
708
|
+
const src = 'export const out = list.map(() => { throw new Error("x") })';
|
|
709
|
+
const result = await run(src);
|
|
710
|
+
expect(result.signals).toHaveLength(0);
|
|
711
|
+
});
|
|
712
|
+
it('flags a class method that throws a plain Error', async () => {
|
|
713
|
+
const src = ['export class C {', ' doWork() { throw new Error("fail") }', '}'].join('\n');
|
|
714
|
+
const result = await run(src);
|
|
715
|
+
expect(result.signals.some((s) => s.message.includes("'doWork'"))).toBe(true);
|
|
716
|
+
});
|
|
717
|
+
});
|
|
718
|
+
// ===========================================================================
|
|
719
|
+
// result-pattern-consistency — analyze() throw + skip arms
|
|
720
|
+
// ===========================================================================
|
|
721
|
+
describe('result-pattern-consistency — analyze branches', () => {
|
|
722
|
+
const P = 'packages/x/src/logic.ts';
|
|
723
|
+
async function run(src, rel = P) {
|
|
724
|
+
const abs = fx(rel, src);
|
|
725
|
+
return runAbsolute('result-pattern-consistency', [abs]);
|
|
726
|
+
}
|
|
727
|
+
it('skips files without throw or Result (quick filter)', async () => {
|
|
728
|
+
const result = await run('export const x = 1');
|
|
729
|
+
expect(result.signals).toHaveLength(0);
|
|
730
|
+
});
|
|
731
|
+
it('flags a bare throw of an expected ValidationError', async () => {
|
|
732
|
+
const src = 'export function doIt() { throw new ValidationError("bad") }';
|
|
733
|
+
const result = await run(src);
|
|
734
|
+
expect(result.signals.some((s) => s.message.includes('Throwing ValidationError'))).toBe(true);
|
|
735
|
+
});
|
|
736
|
+
it('skips throws in throw-allowed paths like /routes/', async () => {
|
|
737
|
+
const src = 'export function doIt() { throw new ValidationError("bad") }';
|
|
738
|
+
const result = await run(src, 'packages/x/src/routes/logic.ts');
|
|
739
|
+
expect(result.signals).toHaveLength(0);
|
|
740
|
+
});
|
|
741
|
+
it('skips throws in infrastructure files (e.g. *-registry.ts)', async () => {
|
|
742
|
+
const src = 'export function doIt() { throw new NotFoundError("missing") }';
|
|
743
|
+
const result = await run(src, 'packages/x/src/plugin-registry.ts');
|
|
744
|
+
expect(result.signals).toHaveLength(0);
|
|
745
|
+
});
|
|
746
|
+
it('skips re-throws inside a catch block', async () => {
|
|
747
|
+
const src = [
|
|
748
|
+
'export function doIt() {',
|
|
749
|
+
' try { work() } catch (e) { throw new ValidationError("wrap") }',
|
|
750
|
+
'}',
|
|
751
|
+
].join('\n');
|
|
752
|
+
const result = await run(src);
|
|
753
|
+
expect(result.signals).toHaveLength(0);
|
|
754
|
+
});
|
|
755
|
+
it('skips throws inside a constructor', async () => {
|
|
756
|
+
const src = [
|
|
757
|
+
'export class C {',
|
|
758
|
+
' constructor(n: number) { if (n < 0) throw new ValidationError("neg") }',
|
|
759
|
+
'}',
|
|
760
|
+
].join('\n');
|
|
761
|
+
const result = await run(src);
|
|
762
|
+
expect(result.signals).toHaveLength(0);
|
|
763
|
+
});
|
|
764
|
+
it('skips throws inside a private method', async () => {
|
|
765
|
+
const src = [
|
|
766
|
+
'export class C {',
|
|
767
|
+
' private guard() { throw new ValidationError("x") }',
|
|
768
|
+
'}',
|
|
769
|
+
].join('\n');
|
|
770
|
+
const result = await run(src);
|
|
771
|
+
expect(result.signals).toHaveLength(0);
|
|
772
|
+
});
|
|
773
|
+
it('skips throws inside a validation-helper function (validateXxx)', async () => {
|
|
774
|
+
const src = 'export function validateInput() { throw new ValidationError("x") }';
|
|
775
|
+
const result = await run(src);
|
|
776
|
+
expect(result.signals).toHaveLength(0);
|
|
777
|
+
});
|
|
778
|
+
it('does NOT flag a throw of a non-expected error type', async () => {
|
|
779
|
+
const src = 'export function doIt() { throw new TypeError("boom") }';
|
|
780
|
+
const result = await run(src);
|
|
781
|
+
expect(result.signals).toHaveLength(0);
|
|
782
|
+
});
|
|
783
|
+
it('flags a Result-returning function that throws an expected error', async () => {
|
|
784
|
+
const src = [
|
|
785
|
+
'export function load(id: string): Result<Row, ValidationError> {',
|
|
786
|
+
' if (!id) throw new ValidationError("missing id")',
|
|
787
|
+
' return ok(fetch(id))',
|
|
788
|
+
'}',
|
|
789
|
+
].join('\n');
|
|
790
|
+
const result = await run(src);
|
|
791
|
+
expect(result.signals.some((s) => s.message.includes('returns Result but throws'))).toBe(true);
|
|
792
|
+
});
|
|
793
|
+
it('flags a throw inside a non-private class method (containing-function name via method arm)', async () => {
|
|
794
|
+
const src = ['export class C {', ' doWork() { throw new NotFoundError("missing") }', '}'].join('\n');
|
|
795
|
+
const result = await run(src);
|
|
796
|
+
expect(result.signals.some((s) => s.message.includes('Throwing NotFoundError'))).toBe(true);
|
|
797
|
+
});
|
|
798
|
+
it('flags a throw inside an arrow function assigned to a const', async () => {
|
|
799
|
+
const src = 'export const handle = () => { throw new ConflictError("dup") }';
|
|
800
|
+
const result = await run(src);
|
|
801
|
+
expect(result.signals.some((s) => s.message.includes('Throwing ConflictError'))).toBe(true);
|
|
802
|
+
});
|
|
803
|
+
it('skips a private method that returns Result and throws (private-method body arm)', async () => {
|
|
804
|
+
const src = [
|
|
805
|
+
'export class C {',
|
|
806
|
+
' private compute(): Result<number, ValidationError> {',
|
|
807
|
+
' throw new ValidationError("x")',
|
|
808
|
+
' }',
|
|
809
|
+
'}',
|
|
810
|
+
].join('\n');
|
|
811
|
+
const result = await run(src);
|
|
812
|
+
expect(result.signals.some((s) => s.message.includes('returns Result but throws'))).toBe(false);
|
|
813
|
+
});
|
|
814
|
+
it('skips a Result-returning validation helper that throws (funcName arm)', async () => {
|
|
815
|
+
const src = [
|
|
816
|
+
'export function validateRow(): Result<Row, ValidationError> {',
|
|
817
|
+
' throw new ValidationError("x")',
|
|
818
|
+
'}',
|
|
819
|
+
].join('\n');
|
|
820
|
+
const result = await run(src);
|
|
821
|
+
expect(result.signals.some((s) => s.message.includes('returns Result but throws'))).toBe(false);
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
// ===========================================================================
|
|
825
|
+
// stubbed-implementation-detection — analyze() pattern + skip arms
|
|
826
|
+
// ===========================================================================
|
|
827
|
+
describe('stubbed-implementation-detection — analyze branches', () => {
|
|
828
|
+
const P = 'packages/x/src/svc.ts';
|
|
829
|
+
async function run(src, rel = P) {
|
|
830
|
+
const abs = fx(rel, src);
|
|
831
|
+
return runAbsolute('stubbed-implementation-detection', [abs]);
|
|
832
|
+
}
|
|
833
|
+
it('flags an empty object stub cast to a non-primitive type', async () => {
|
|
834
|
+
const src = 'export function make(): Widget { return {} as Widget }';
|
|
835
|
+
const result = await run(src);
|
|
836
|
+
expect(result.signals.some((s) => s.message.includes('Empty object stub'))).toBe(true);
|
|
837
|
+
});
|
|
838
|
+
it('does NOT flag an empty object cast to a primitive', async () => {
|
|
839
|
+
const src = 'export function make() { return {} as unknown }';
|
|
840
|
+
const result = await run(src);
|
|
841
|
+
expect(result.signals.some((s) => s.message.includes('Empty object stub'))).toBe(false);
|
|
842
|
+
});
|
|
843
|
+
it('does NOT flag an empty object cast to a Record type', async () => {
|
|
844
|
+
const src = 'export function make() { return {} as Record<string, number> }';
|
|
845
|
+
const result = await run(src);
|
|
846
|
+
expect(result.signals.some((s) => s.message.includes('Empty object stub'))).toBe(false);
|
|
847
|
+
});
|
|
848
|
+
it('does NOT flag an empty object cast to a generic type parameter', async () => {
|
|
849
|
+
const src = 'export function make<T>(): T { return {} as T }';
|
|
850
|
+
const result = await run(src);
|
|
851
|
+
expect(result.signals.some((s) => s.message.includes('Empty object stub'))).toBe(false);
|
|
852
|
+
});
|
|
853
|
+
it('does NOT flag an empty object cast used as a Proxy target', async () => {
|
|
854
|
+
const src = 'export function make(): Widget { return new Proxy({} as Widget, {}) }';
|
|
855
|
+
const result = await run(src);
|
|
856
|
+
expect(result.signals.some((s) => s.message.includes('Empty object stub'))).toBe(false);
|
|
857
|
+
});
|
|
858
|
+
it('flags a Promise.resolve() placeholder return', async () => {
|
|
859
|
+
const src = 'export async function fetchData() { return Promise.resolve(null) }';
|
|
860
|
+
const result = await run(src);
|
|
861
|
+
expect(result.signals.some((s) => s.message.includes('Promise.resolve'))).toBe(true);
|
|
862
|
+
});
|
|
863
|
+
it('does NOT flag Promise.resolve() in a lifecycle method', async () => {
|
|
864
|
+
const src = [
|
|
865
|
+
'export class Svc {',
|
|
866
|
+
' async dispose() { return Promise.resolve(undefined) }',
|
|
867
|
+
'}',
|
|
868
|
+
].join('\n');
|
|
869
|
+
const result = await run(src);
|
|
870
|
+
expect(result.signals.some((s) => s.message.includes('Promise.resolve'))).toBe(false);
|
|
871
|
+
});
|
|
872
|
+
it('does NOT flag Promise.resolve() inside a conditional guard', async () => {
|
|
873
|
+
const src = [
|
|
874
|
+
'export async function fetchData(skip: boolean) {',
|
|
875
|
+
' if (skip) { return Promise.resolve(null) }',
|
|
876
|
+
' return realWork()',
|
|
877
|
+
'}',
|
|
878
|
+
].join('\n');
|
|
879
|
+
const result = await run(src);
|
|
880
|
+
expect(result.signals.some((s) => s.message.includes('Promise.resolve'))).toBe(false);
|
|
881
|
+
});
|
|
882
|
+
it('does NOT flag Promise.resolve() when the body has substantive statements', async () => {
|
|
883
|
+
const src = [
|
|
884
|
+
'export async function fetchData() {',
|
|
885
|
+
' const x = computeStuff()',
|
|
886
|
+
' return Promise.resolve(null)',
|
|
887
|
+
'}',
|
|
888
|
+
].join('\n');
|
|
889
|
+
const result = await run(src);
|
|
890
|
+
expect(result.signals.some((s) => s.message.includes('Promise.resolve'))).toBe(false);
|
|
891
|
+
});
|
|
892
|
+
it('flags a hardcoded { success: true, data: [] } stub return', async () => {
|
|
893
|
+
const src = 'export function list() { return { success: true, data: [] } }';
|
|
894
|
+
const result = await run(src);
|
|
895
|
+
expect(result.signals.some((s) => s.message.includes('Hardcoded stub return'))).toBe(true);
|
|
896
|
+
});
|
|
897
|
+
it('does NOT flag a hardcoded stub return inside a conditional branch', async () => {
|
|
898
|
+
const src = [
|
|
899
|
+
'export function list(empty: boolean) {',
|
|
900
|
+
' if (empty) { return { success: true, data: [] } }',
|
|
901
|
+
' return { success: true, data: fetchRows() }',
|
|
902
|
+
'}',
|
|
903
|
+
].join('\n');
|
|
904
|
+
const result = await run(src);
|
|
905
|
+
expect(result.signals.some((s) => s.message.includes('Hardcoded stub return'))).toBe(false);
|
|
906
|
+
});
|
|
907
|
+
it('does NOT flag a hardcoded stub return when calls precede it', async () => {
|
|
908
|
+
const src = [
|
|
909
|
+
'export function list() {',
|
|
910
|
+
' doSetup()',
|
|
911
|
+
' return { success: true, data: [] }',
|
|
912
|
+
'}',
|
|
913
|
+
].join('\n');
|
|
914
|
+
const result = await run(src);
|
|
915
|
+
expect(result.signals.some((s) => s.message.includes('Hardcoded stub return'))).toBe(false);
|
|
916
|
+
});
|
|
917
|
+
it('flags a placeholder comment', async () => {
|
|
918
|
+
const src = 'export function todo() {\n // STUB: implement this\n return 0\n}';
|
|
919
|
+
const result = await run(src);
|
|
920
|
+
expect(result.signals.some((s) => s.message.includes('Placeholder comment'))).toBe(true);
|
|
921
|
+
});
|
|
922
|
+
it('skips all AST stub detection in test files', async () => {
|
|
923
|
+
const src = 'export function make(): Widget { return {} as Widget }';
|
|
924
|
+
const result = await run(src, 'packages/x/src/svc.test.ts');
|
|
925
|
+
expect(result.signals).toHaveLength(0);
|
|
926
|
+
});
|
|
927
|
+
});
|
|
928
|
+
// ===========================================================================
|
|
929
|
+
// numeric-validation — analyze() parse-call + parameter arms
|
|
930
|
+
// ===========================================================================
|
|
931
|
+
describe('numeric-validation — analyze branches', () => {
|
|
932
|
+
const P = 'packages/x/src/calc.ts';
|
|
933
|
+
async function run(src, rel = P) {
|
|
934
|
+
const abs = fx(rel, src);
|
|
935
|
+
return runAbsolute('numeric-validation', [abs]);
|
|
936
|
+
}
|
|
937
|
+
it('skips test files', async () => {
|
|
938
|
+
const result = await run('export const n = parseInt(input, 10)', 'packages/x/src/calc.test.ts');
|
|
939
|
+
expect(result.signals).toHaveLength(0);
|
|
940
|
+
});
|
|
941
|
+
it('skips route handler files', async () => {
|
|
942
|
+
const result = await run('export const n = parseInt(input, 10)', 'packages/x/src/routes/calc.ts');
|
|
943
|
+
expect(result.signals).toHaveLength(0);
|
|
944
|
+
});
|
|
945
|
+
it('skips files without any numeric keyword (quick filter)', async () => {
|
|
946
|
+
const result = await run('export const s = "hello"');
|
|
947
|
+
expect(result.signals).toHaveLength(0);
|
|
948
|
+
});
|
|
949
|
+
it('skips files that import zod', async () => {
|
|
950
|
+
const src = 'import { z } from "zod"\nexport const n = parseInt(input, 10)';
|
|
951
|
+
const result = await run(src);
|
|
952
|
+
expect(result.signals).toHaveLength(0);
|
|
953
|
+
});
|
|
954
|
+
it('flags an unvalidated parseInt call', async () => {
|
|
955
|
+
const src = 'export function f(input: string) { const n = parseInt(input, 10); return n }';
|
|
956
|
+
const result = await run(src);
|
|
957
|
+
expect(result.signals.some((s) => s.message.includes('parseInt'))).toBe(true);
|
|
958
|
+
});
|
|
959
|
+
it('does NOT flag parseInt with a "|| 0" fallback on the same line', async () => {
|
|
960
|
+
const src = 'export function f(input: string) { const n = parseInt(input, 10) || 0; return n }';
|
|
961
|
+
const result = await run(src);
|
|
962
|
+
expect(result.signals.some((s) => s.message.includes('parseInt'))).toBe(false);
|
|
963
|
+
});
|
|
964
|
+
it('does NOT flag parseInt when a Number.isFinite check follows nearby', async () => {
|
|
965
|
+
const src = [
|
|
966
|
+
'export function f(input: string) {',
|
|
967
|
+
' const n = parseInt(input, 10)',
|
|
968
|
+
' if (!Number.isFinite(n)) throw new Error("bad")',
|
|
969
|
+
' return n',
|
|
970
|
+
'}',
|
|
971
|
+
].join('\n');
|
|
972
|
+
const result = await run(src);
|
|
973
|
+
expect(result.signals.some((s) => s.message.includes('parseInt'))).toBe(false);
|
|
974
|
+
});
|
|
975
|
+
it('does NOT flag parseInt of a DynamoDB .N attribute', async () => {
|
|
976
|
+
const src = 'export function f(item: any) { const n = parseInt(item.count.N, 10); return n }';
|
|
977
|
+
const result = await run(src);
|
|
978
|
+
expect(result.signals.some((s) => s.message.includes('parseInt'))).toBe(false);
|
|
979
|
+
});
|
|
980
|
+
it('does NOT flag parseInt with a safe numeric-string fallback in the argument', async () => {
|
|
981
|
+
const src = "export function f(input?: string) { const n = parseInt(input ?? '0', 10); return n }";
|
|
982
|
+
const result = await run(src);
|
|
983
|
+
expect(result.signals.some((s) => s.message.includes('parseInt'))).toBe(false);
|
|
984
|
+
});
|
|
985
|
+
it('does NOT flag parseInt when a regex digit guard precedes it', async () => {
|
|
986
|
+
const src = [
|
|
987
|
+
'export function f(input: string) {',
|
|
988
|
+
String.raw ` if (/^\d+$/.test(input)) {`,
|
|
989
|
+
' const n = parseInt(input, 10)',
|
|
990
|
+
' return n',
|
|
991
|
+
' }',
|
|
992
|
+
' return 0',
|
|
993
|
+
'}',
|
|
994
|
+
].join('\n');
|
|
995
|
+
const result = await run(src);
|
|
996
|
+
expect(result.signals.some((s) => s.message.includes('parseInt'))).toBe(false);
|
|
997
|
+
});
|
|
998
|
+
it('does NOT flag parseInt of a regex capture subscript with a nearby digit regex', async () => {
|
|
999
|
+
const src = [
|
|
1000
|
+
'export function f(text: string) {',
|
|
1001
|
+
String.raw ` const re = /(\d+)/`,
|
|
1002
|
+
' const m = re.exec(text)',
|
|
1003
|
+
' const n = parseInt(m[1], 10)',
|
|
1004
|
+
' return n',
|
|
1005
|
+
'}',
|
|
1006
|
+
].join('\n');
|
|
1007
|
+
const result = await run(src);
|
|
1008
|
+
expect(result.signals.some((s) => s.message.includes('parseInt'))).toBe(false);
|
|
1009
|
+
});
|
|
1010
|
+
it('does NOT flag a primitive `number` keyword parameter (only type references named number)', async () => {
|
|
1011
|
+
// The TS AST models `: number` as a NumberKeyword node, not a
|
|
1012
|
+
// TypeReferenceNode, so isNumberTypeParam returns false — exercising the
|
|
1013
|
+
// parameter-filter no-match arm without producing a violation.
|
|
1014
|
+
const src = 'export function area(width: number) { return width * 2 }';
|
|
1015
|
+
const result = await run(src);
|
|
1016
|
+
expect(result.signals.some((s) => s.message.includes("'width'"))).toBe(false);
|
|
1017
|
+
});
|
|
1018
|
+
it('does NOT flag a number param that has a default value', async () => {
|
|
1019
|
+
const src = 'export function area(width: number = 1) { return width * 2 }';
|
|
1020
|
+
const result = await run(src);
|
|
1021
|
+
expect(result.signals.some((s) => s.message.includes("'width'"))).toBe(false);
|
|
1022
|
+
});
|
|
1023
|
+
it('does NOT flag a number param named like a safe loop counter', async () => {
|
|
1024
|
+
const src = 'export function loop(index: number) { return index }';
|
|
1025
|
+
const result = await run(src);
|
|
1026
|
+
expect(result.signals.some((s) => s.message.includes("'index'"))).toBe(false);
|
|
1027
|
+
});
|
|
1028
|
+
it('does NOT flag a number param when the body validates it', async () => {
|
|
1029
|
+
const src = 'export function area(width: number) { if (!Number.isFinite(width)) throw new Error("x"); return width }';
|
|
1030
|
+
const result = await run(src);
|
|
1031
|
+
expect(result.signals.some((s) => s.message.includes("'width'"))).toBe(false);
|
|
1032
|
+
});
|
|
1033
|
+
it('does NOT flag a private (_-prefixed) function with a number param', async () => {
|
|
1034
|
+
const src = 'export function _internal(width: number) { return width * 2 }';
|
|
1035
|
+
const result = await run(src);
|
|
1036
|
+
expect(result.signals.some((s) => s.message.includes("'width'"))).toBe(false);
|
|
1037
|
+
});
|
|
1038
|
+
it('does NOT flag a private method with a number param', async () => {
|
|
1039
|
+
const src = [
|
|
1040
|
+
'export class C {',
|
|
1041
|
+
' private compute(width: number) { return width * 2 }',
|
|
1042
|
+
'}',
|
|
1043
|
+
].join('\n');
|
|
1044
|
+
const result = await run(src);
|
|
1045
|
+
expect(result.signals.some((s) => s.message.includes("'width'"))).toBe(false);
|
|
1046
|
+
});
|
|
1047
|
+
});
|
|
1048
|
+
// ===========================================================================
|
|
1049
|
+
// silent-early-returns — analyze() exemption arms
|
|
1050
|
+
// ===========================================================================
|
|
1051
|
+
describe('silent-early-returns — analyze branches', () => {
|
|
1052
|
+
const P = 'packages/x/src/biz.ts';
|
|
1053
|
+
async function run(src, rel = P) {
|
|
1054
|
+
const abs = fx(rel, src);
|
|
1055
|
+
return runAbsolute('silent-early-returns', [abs]);
|
|
1056
|
+
}
|
|
1057
|
+
it('skips files without any return null/false (quick filter)', async () => {
|
|
1058
|
+
const result = await run('export function f() { return 1 }');
|
|
1059
|
+
expect(result.signals).toHaveLength(0);
|
|
1060
|
+
});
|
|
1061
|
+
it('flags a non-guard function with a silent return null', async () => {
|
|
1062
|
+
const src = [
|
|
1063
|
+
'export function loadThing(cfg: any) {',
|
|
1064
|
+
' doStuff()',
|
|
1065
|
+
' doMore()',
|
|
1066
|
+
' doEvenMore()',
|
|
1067
|
+
' if (!cfg.ready) return null',
|
|
1068
|
+
' return cfg',
|
|
1069
|
+
'}',
|
|
1070
|
+
].join('\n');
|
|
1071
|
+
const result = await run(src);
|
|
1072
|
+
expect(result.signals.some((s) => s.message.includes('Silent early return'))).toBe(true);
|
|
1073
|
+
});
|
|
1074
|
+
it('flags a silent return false in a then-block', async () => {
|
|
1075
|
+
const src = [
|
|
1076
|
+
'export function flow(cfg: any) {',
|
|
1077
|
+
' doStuff()',
|
|
1078
|
+
' doMore()',
|
|
1079
|
+
' doEvenMore()',
|
|
1080
|
+
' if (!cfg.ok) { return false }',
|
|
1081
|
+
' return true',
|
|
1082
|
+
'}',
|
|
1083
|
+
].join('\n');
|
|
1084
|
+
const result = await run(src);
|
|
1085
|
+
expect(result.signals.some((s) => s.message.includes('Silent early return (false)'))).toBe(true);
|
|
1086
|
+
});
|
|
1087
|
+
it('does NOT flag a type-guard function (x is T return type)', async () => {
|
|
1088
|
+
const src = 'export function check(x: unknown): x is string { if (typeof x !== "string") return false; return true }';
|
|
1089
|
+
const result = await run(src);
|
|
1090
|
+
expect(result.signals).toHaveLength(0);
|
|
1091
|
+
});
|
|
1092
|
+
it('does NOT flag a predicate-prefixed function (isXxx)', async () => {
|
|
1093
|
+
const src = 'export function isReady(x: any) { if (!x) return false; return true }';
|
|
1094
|
+
const result = await run(src);
|
|
1095
|
+
expect(result.signals).toHaveLength(0);
|
|
1096
|
+
});
|
|
1097
|
+
it('does NOT flag a function whose return type is T | null', async () => {
|
|
1098
|
+
const src = 'export function lookupRow(x: any): Row | null { doA(); doB(); if (!x) return null; return x }';
|
|
1099
|
+
const result = await run(src);
|
|
1100
|
+
expect(result.signals).toHaveLength(0);
|
|
1101
|
+
});
|
|
1102
|
+
it('does NOT flag a predicate callback in arr.filter', async () => {
|
|
1103
|
+
const src = 'export const out = arr.filter((x: any) => { if (!x) return false; return true })';
|
|
1104
|
+
const result = await run(src);
|
|
1105
|
+
expect(result.signals).toHaveLength(0);
|
|
1106
|
+
});
|
|
1107
|
+
it('does NOT flag an early guard clause in the first 3 statements', async () => {
|
|
1108
|
+
const src = 'export function compute(cfg: any) { if (!cfg) return null; return work(cfg) }';
|
|
1109
|
+
const result = await run(src);
|
|
1110
|
+
expect(result.signals).toHaveLength(0);
|
|
1111
|
+
});
|
|
1112
|
+
it('does NOT flag when logging is present near the return', async () => {
|
|
1113
|
+
const src = [
|
|
1114
|
+
'export function flow(cfg: any) {',
|
|
1115
|
+
' doStuff()',
|
|
1116
|
+
' doMore()',
|
|
1117
|
+
' doEvenMore()',
|
|
1118
|
+
' if (!cfg.ok) { logger.warn("not ok"); return null }',
|
|
1119
|
+
' return cfg',
|
|
1120
|
+
'}',
|
|
1121
|
+
].join('\n');
|
|
1122
|
+
const result = await run(src);
|
|
1123
|
+
expect(result.signals).toHaveLength(0);
|
|
1124
|
+
});
|
|
1125
|
+
});
|
|
1126
|
+
// ===========================================================================
|
|
1127
|
+
// missing-input-validation — analyze() handler-detection arms
|
|
1128
|
+
// ===========================================================================
|
|
1129
|
+
describe('missing-input-validation — analyze branches', () => {
|
|
1130
|
+
const P = 'packages/api/src/routes/handler.ts';
|
|
1131
|
+
async function run(src, rel = P) {
|
|
1132
|
+
const abs = fx(rel, src);
|
|
1133
|
+
return runAbsolute('missing-input-validation', [abs]);
|
|
1134
|
+
}
|
|
1135
|
+
it('skips excluded internal paths like /services/', async () => {
|
|
1136
|
+
const src = 'export function h(req: any, res: any) { return res.send(req.body) }';
|
|
1137
|
+
const result = await run(src, 'packages/api/src/services/handler.ts');
|
|
1138
|
+
expect(result.signals).toHaveLength(0);
|
|
1139
|
+
});
|
|
1140
|
+
it('skips files without any handler-shaped keyword (quick filter)', async () => {
|
|
1141
|
+
const result = await run('export const x = 1');
|
|
1142
|
+
expect(result.signals).toHaveLength(0);
|
|
1143
|
+
});
|
|
1144
|
+
it('flags an Express (req, res) handler with no validation', async () => {
|
|
1145
|
+
const src = 'export function createUser(req: any, res: any) { return res.send(req.body) }';
|
|
1146
|
+
const result = await run(src);
|
|
1147
|
+
expect(result.signals).toHaveLength(1);
|
|
1148
|
+
expect(result.signals[0]?.message).toContain("'createUser'");
|
|
1149
|
+
});
|
|
1150
|
+
it('flags a Fastify (request, reply) arrow handler assigned to a const', async () => {
|
|
1151
|
+
const src = 'export const createUser = (request: any, reply: any) => { return reply.send(request.body) }';
|
|
1152
|
+
const result = await run(src);
|
|
1153
|
+
expect(result.signals).toHaveLength(1);
|
|
1154
|
+
expect(result.signals[0]?.message).toContain("'createUser'");
|
|
1155
|
+
});
|
|
1156
|
+
it('does NOT flag a handler that validates with .parse()', async () => {
|
|
1157
|
+
const src = 'export function h(req: any, res: any) { const d = schema.parse(req.body); return res.send(d) }';
|
|
1158
|
+
const result = await run(src);
|
|
1159
|
+
expect(result.signals).toHaveLength(0);
|
|
1160
|
+
});
|
|
1161
|
+
it('does NOT flag a method whose first two params are not request/response', async () => {
|
|
1162
|
+
const src = [
|
|
1163
|
+
'export class C {',
|
|
1164
|
+
' handler(first: number, second: number) { return first + second }',
|
|
1165
|
+
'}',
|
|
1166
|
+
].join('\n');
|
|
1167
|
+
const result = await run(src);
|
|
1168
|
+
expect(result.signals).toHaveLength(0);
|
|
1169
|
+
});
|
|
1170
|
+
it('does NOT flag a handler whose parameters are destructured (non-identifier)', async () => {
|
|
1171
|
+
const src = 'export function h({ req }: any, { res }: any) { return req }';
|
|
1172
|
+
const result = await run(src);
|
|
1173
|
+
expect(result.signals).toHaveLength(0);
|
|
1174
|
+
});
|
|
1175
|
+
it('does NOT flag a single-parameter function', async () => {
|
|
1176
|
+
const src = 'export function h(req: any) { return req.body }';
|
|
1177
|
+
const result = await run(src);
|
|
1178
|
+
expect(result.signals).toHaveLength(0);
|
|
1179
|
+
});
|
|
1180
|
+
});
|
|
1181
|
+
// ===========================================================================
|
|
1182
|
+
// fastify-route-validation — analyze() validation-detection arms
|
|
1183
|
+
// ===========================================================================
|
|
1184
|
+
describe('fastify-route-validation — analyze branches', () => {
|
|
1185
|
+
const P = 'packages/api/src/routes/users.ts';
|
|
1186
|
+
async function run(src, rel = P) {
|
|
1187
|
+
const abs = fx(rel, src);
|
|
1188
|
+
return runAbsolute('fastify-route-validation', [abs]);
|
|
1189
|
+
}
|
|
1190
|
+
it('skips files not under /routes/', async () => {
|
|
1191
|
+
const src = 'fastify.post("/u", async (req, reply) => { req.body })';
|
|
1192
|
+
const result = await run(src, 'packages/api/src/handlers/users.ts');
|
|
1193
|
+
expect(result.signals).toHaveLength(0);
|
|
1194
|
+
});
|
|
1195
|
+
it('skips files without any fastify route pattern (quick filter)', async () => {
|
|
1196
|
+
const result = await run('export const x = 1');
|
|
1197
|
+
expect(result.signals).toHaveLength(0);
|
|
1198
|
+
});
|
|
1199
|
+
it('flags a POST handler that reads request.body without validation', async () => {
|
|
1200
|
+
const src = [
|
|
1201
|
+
'export function reg(fastify: any) {',
|
|
1202
|
+
' fastify.post("/users", async (request, reply) => {',
|
|
1203
|
+
' const data = request.body',
|
|
1204
|
+
' return reply.send(data)',
|
|
1205
|
+
' })',
|
|
1206
|
+
'}',
|
|
1207
|
+
].join('\n');
|
|
1208
|
+
const result = await run(src);
|
|
1209
|
+
expect(result.signals).toHaveLength(1);
|
|
1210
|
+
expect(result.signals[0]?.message).toContain('POST /users');
|
|
1211
|
+
});
|
|
1212
|
+
it('does NOT flag a handler that validates with Zod .parse()', async () => {
|
|
1213
|
+
const src = [
|
|
1214
|
+
'export function reg(fastify: any) {',
|
|
1215
|
+
' fastify.put("/users", async (request, reply) => {',
|
|
1216
|
+
' const data = schema.parse(request.body)',
|
|
1217
|
+
' return reply.send(data)',
|
|
1218
|
+
' })',
|
|
1219
|
+
'}',
|
|
1220
|
+
].join('\n');
|
|
1221
|
+
const result = await run(src);
|
|
1222
|
+
expect(result.signals).toHaveLength(0);
|
|
1223
|
+
});
|
|
1224
|
+
it('does NOT flag a handler using an alternative validateBody() validator', async () => {
|
|
1225
|
+
const src = [
|
|
1226
|
+
'export function reg(fastify: any) {',
|
|
1227
|
+
' fastify.patch("/users", async (request, reply) => {',
|
|
1228
|
+
' const data = validateBody(request.body)',
|
|
1229
|
+
' return reply.send(data)',
|
|
1230
|
+
' })',
|
|
1231
|
+
'}',
|
|
1232
|
+
].join('\n');
|
|
1233
|
+
const result = await run(src);
|
|
1234
|
+
expect(result.signals).toHaveLength(0);
|
|
1235
|
+
});
|
|
1236
|
+
it('does NOT flag a handler with manual if-! body validation', async () => {
|
|
1237
|
+
const src = [
|
|
1238
|
+
'export function reg(fastify: any) {',
|
|
1239
|
+
' fastify.post("/users", async (request, reply) => {',
|
|
1240
|
+
' const body = request.body',
|
|
1241
|
+
' if (!body) return reply.code(400).send()',
|
|
1242
|
+
' return reply.send(body)',
|
|
1243
|
+
' })',
|
|
1244
|
+
'}',
|
|
1245
|
+
].join('\n');
|
|
1246
|
+
const result = await run(src);
|
|
1247
|
+
expect(result.signals).toHaveLength(0);
|
|
1248
|
+
});
|
|
1249
|
+
it('does NOT flag a handler that returns a 400 with an Invalid message', async () => {
|
|
1250
|
+
const src = [
|
|
1251
|
+
'export function reg(fastify: any) {',
|
|
1252
|
+
' fastify.post("/users", async (request, reply) => {',
|
|
1253
|
+
' if (bad(request.body)) return reply.code(400).send({ error: "Invalid input" })',
|
|
1254
|
+
' return reply.send(ok)',
|
|
1255
|
+
' })',
|
|
1256
|
+
'}',
|
|
1257
|
+
].join('\n');
|
|
1258
|
+
const result = await run(src);
|
|
1259
|
+
expect(result.signals).toHaveLength(0);
|
|
1260
|
+
});
|
|
1261
|
+
it('does NOT flag a route passing a zod schema in the options object', async () => {
|
|
1262
|
+
const src = [
|
|
1263
|
+
'export function reg(fastify: any) {',
|
|
1264
|
+
' fastify.post("/users", { schema: { body: userSchema } }, async (request, reply) => {',
|
|
1265
|
+
' return reply.send(request.body)',
|
|
1266
|
+
' })',
|
|
1267
|
+
'}',
|
|
1268
|
+
].join('\n');
|
|
1269
|
+
const result = await run(src);
|
|
1270
|
+
expect(result.signals).toHaveLength(0);
|
|
1271
|
+
});
|
|
1272
|
+
it('uses content-level fallback when no handler function is present', async () => {
|
|
1273
|
+
// No inline handler arrow — the route options reference a named handler.
|
|
1274
|
+
// checkForValidation falls back to hasValidationInContent: zod + .parse(.
|
|
1275
|
+
const src = [
|
|
1276
|
+
'import { z } from "zod"',
|
|
1277
|
+
'const userSchema = z.object({})',
|
|
1278
|
+
'export function reg(fastify: any) {',
|
|
1279
|
+
' fastify.post("/users", { handler: namedHandler })',
|
|
1280
|
+
'}',
|
|
1281
|
+
'function namedHandler(request: any) { return userSchema.parse(request.body) }',
|
|
1282
|
+
].join('\n');
|
|
1283
|
+
const result = await run(src);
|
|
1284
|
+
expect(result.signals).toHaveLength(0);
|
|
1285
|
+
});
|
|
1286
|
+
it('skips a route call with fewer than two arguments', async () => {
|
|
1287
|
+
const src = 'export function reg(fastify: any) { fastify.post("/users") }';
|
|
1288
|
+
const result = await run(src);
|
|
1289
|
+
expect(result.signals).toHaveLength(0);
|
|
1290
|
+
});
|
|
1291
|
+
});
|
|
1292
|
+
// ===========================================================================
|
|
1293
|
+
// toctou-race-condition — direct analyzeFileForToctou branches
|
|
1294
|
+
// ===========================================================================
|
|
1295
|
+
describe('toctou-race-condition — analyze branches', () => {
|
|
1296
|
+
const P = 'packages/x/src/account-service.ts';
|
|
1297
|
+
function run(content, path = P) {
|
|
1298
|
+
// analyzeFileForToctou reads recipe config via currentScope().
|
|
1299
|
+
return runWithScopeSync(testScope, () => analyzeFileForToctou(path, content));
|
|
1300
|
+
}
|
|
1301
|
+
it('flags a shared read-then-update on the same receiver', () => {
|
|
1302
|
+
const src = [
|
|
1303
|
+
'export async function applyDelta(store: any) {',
|
|
1304
|
+
' const current = await store.get(key)',
|
|
1305
|
+
' await store.update({ ...current, n: current.n + 1 })',
|
|
1306
|
+
'}',
|
|
1307
|
+
].join('\n');
|
|
1308
|
+
const v = run(src);
|
|
1309
|
+
expect(v).toHaveLength(1);
|
|
1310
|
+
expect(v[0]?.message).toContain('read-then-update');
|
|
1311
|
+
expect(v[0]?.match).toBe('applyDelta');
|
|
1312
|
+
});
|
|
1313
|
+
it('skips files in a safe TOCTOU path (e.g. /cache/)', () => {
|
|
1314
|
+
const src = [
|
|
1315
|
+
'export async function applyDelta(store: any) {',
|
|
1316
|
+
' const current = await store.get(key)',
|
|
1317
|
+
' await store.update(current)',
|
|
1318
|
+
'}',
|
|
1319
|
+
].join('\n');
|
|
1320
|
+
expect(run(src, 'packages/x/src/cache/store.ts')).toHaveLength(0);
|
|
1321
|
+
});
|
|
1322
|
+
it('skips a function documenting atomic / transaction semantics', () => {
|
|
1323
|
+
const src = [
|
|
1324
|
+
'export async function applyDelta(store: any) {',
|
|
1325
|
+
' // uses withTransaction for atomicity',
|
|
1326
|
+
' const current = await store.get(key)',
|
|
1327
|
+
' await store.update(current)',
|
|
1328
|
+
'}',
|
|
1329
|
+
].join('\n');
|
|
1330
|
+
expect(run(src)).toHaveLength(0);
|
|
1331
|
+
});
|
|
1332
|
+
it('does NOT flag read-then-update on a local Map parameter', () => {
|
|
1333
|
+
const src = [
|
|
1334
|
+
'export function bump(counts: Map<string, number>) {',
|
|
1335
|
+
' const current = counts.get(key)',
|
|
1336
|
+
' counts.set(key, (current ?? 0) + 1)',
|
|
1337
|
+
'}',
|
|
1338
|
+
].join('\n');
|
|
1339
|
+
expect(run(src)).toHaveLength(0);
|
|
1340
|
+
});
|
|
1341
|
+
it('does NOT flag read-then-update on a local `new Map()` variable', () => {
|
|
1342
|
+
const src = [
|
|
1343
|
+
'export function bump() {',
|
|
1344
|
+
' const counts = new Map()',
|
|
1345
|
+
' const current = counts.get(key)',
|
|
1346
|
+
' counts.set(key, current)',
|
|
1347
|
+
'}',
|
|
1348
|
+
].join('\n');
|
|
1349
|
+
expect(run(src)).toHaveLength(0);
|
|
1350
|
+
});
|
|
1351
|
+
it('does NOT flag access to a this.<name>Cache class field', () => {
|
|
1352
|
+
const src = [
|
|
1353
|
+
'export class Svc {',
|
|
1354
|
+
' private headerCache = new Map()',
|
|
1355
|
+
' async load(key: string) {',
|
|
1356
|
+
' const hit = this.headerCache.get(key)',
|
|
1357
|
+
' this.headerCache.set(key, hit)',
|
|
1358
|
+
' }',
|
|
1359
|
+
'}',
|
|
1360
|
+
].join('\n');
|
|
1361
|
+
expect(run(src)).toHaveLength(0);
|
|
1362
|
+
});
|
|
1363
|
+
it('does NOT flag a drizzle-style atomic write (db.update(table))', () => {
|
|
1364
|
+
const src = [
|
|
1365
|
+
'export async function touch(db: any) {',
|
|
1366
|
+
' const row = await db.find(key)',
|
|
1367
|
+
' await db.update(usersTable)',
|
|
1368
|
+
'}',
|
|
1369
|
+
].join('\n');
|
|
1370
|
+
// db.update is classified atomic-sql-write, so no read-then-update pair.
|
|
1371
|
+
expect(run(src)).toHaveLength(0);
|
|
1372
|
+
});
|
|
1373
|
+
it('does NOT flag a function with no read/update pair', () => {
|
|
1374
|
+
const src = 'export function noop(store: any) { return store.size }';
|
|
1375
|
+
expect(run(src)).toHaveLength(0);
|
|
1376
|
+
});
|
|
1377
|
+
it('does NOT flag interface-typed state-bag Map fields (state.lowlink)', () => {
|
|
1378
|
+
const src = [
|
|
1379
|
+
'interface State { lowlink: Map<string, number> }',
|
|
1380
|
+
'export function step(state: State) {',
|
|
1381
|
+
' const v = state.lowlink.get(key)',
|
|
1382
|
+
' state.lowlink.set(key, v ?? 0)',
|
|
1383
|
+
'}',
|
|
1384
|
+
].join('\n');
|
|
1385
|
+
expect(run(src)).toHaveLength(0);
|
|
1386
|
+
});
|
|
1387
|
+
});
|
|
1388
|
+
// ===========================================================================
|
|
1389
|
+
// callback-invocation-safe — direct analyze branches
|
|
1390
|
+
// ===========================================================================
|
|
1391
|
+
describe('callback-invocation-safe — analyze branches', () => {
|
|
1392
|
+
const P = 'packages/x/src/notifier.ts';
|
|
1393
|
+
function run(content, path = P) {
|
|
1394
|
+
return analyzeCallbackInvocationSafe(content, path);
|
|
1395
|
+
}
|
|
1396
|
+
it('skips non-.ts, .d.ts, test, and out-of-scope files', () => {
|
|
1397
|
+
const c = 'subscribers.forEach((cb) => cb())';
|
|
1398
|
+
expect(run(c, 'packages/x/src/n.js')).toHaveLength(0);
|
|
1399
|
+
expect(run(c, 'packages/x/src/n.d.ts')).toHaveLength(0);
|
|
1400
|
+
expect(run(c, 'packages/x/src/n.test.ts')).toHaveLength(0);
|
|
1401
|
+
expect(run(c, 'apps/web/src/n.ts')).toHaveLength(0);
|
|
1402
|
+
});
|
|
1403
|
+
it('fast-paths files that never mention a collection name', () => {
|
|
1404
|
+
expect(run('export const x = 1')).toHaveLength(0);
|
|
1405
|
+
});
|
|
1406
|
+
it('flags an unguarded subscribers.forEach((cb) => cb())', () => {
|
|
1407
|
+
const c = 'export function fire() {\n subscribers.forEach((cb) => cb(payload))\n}';
|
|
1408
|
+
const v = run(c);
|
|
1409
|
+
expect(v).toHaveLength(1);
|
|
1410
|
+
expect(v[0]?.severity).toBe('error');
|
|
1411
|
+
expect(v[0]?.message).toContain('subscribers');
|
|
1412
|
+
});
|
|
1413
|
+
it('flags an unguarded for-of over listeners', () => {
|
|
1414
|
+
const c = 'export function fire() {\n for (const cb of this.listeners) {\n cb(payload)\n }\n}';
|
|
1415
|
+
const v = run(c);
|
|
1416
|
+
expect(v).toHaveLength(1);
|
|
1417
|
+
expect(v[0]?.message).toContain('listeners');
|
|
1418
|
+
});
|
|
1419
|
+
it('does NOT flag when the forEach body wraps the call in a safe<Name>() helper', () => {
|
|
1420
|
+
const c = 'export function fire() {\n observers.forEach((cb) => this.safeObserver(cb, payload))\n}';
|
|
1421
|
+
expect(run(c)).toHaveLength(0);
|
|
1422
|
+
});
|
|
1423
|
+
it('does NOT flag a forEach inside a try block', () => {
|
|
1424
|
+
const c = [
|
|
1425
|
+
'export function fire() {',
|
|
1426
|
+
' try {',
|
|
1427
|
+
' callbacks.forEach((cb) => cb(payload))',
|
|
1428
|
+
' } catch (e) {',
|
|
1429
|
+
' log(e)',
|
|
1430
|
+
' }',
|
|
1431
|
+
'}',
|
|
1432
|
+
].join('\n');
|
|
1433
|
+
expect(run(c)).toHaveLength(0);
|
|
1434
|
+
});
|
|
1435
|
+
it('does NOT flag when the arrow parameter is never invoked in the body', () => {
|
|
1436
|
+
const c = 'export function fire() {\n handlers.forEach((cb) => log("noop"))\n}';
|
|
1437
|
+
expect(run(c)).toHaveLength(0);
|
|
1438
|
+
});
|
|
1439
|
+
it('does NOT flag iteration over a collection whose name is not recognised', () => {
|
|
1440
|
+
const c = 'export function fire() {\n widgets.forEach((cb) => cb(payload))\n}';
|
|
1441
|
+
expect(run(c)).toHaveLength(0);
|
|
1442
|
+
});
|
|
1443
|
+
it('honors a pragma with a rationale on the same line', () => {
|
|
1444
|
+
const c = 'export function fire() {\n subscribers.forEach((cb) => cb(payload)) // @callback-invocation-safe-by-caller -- caller wraps\n}';
|
|
1445
|
+
expect(run(c)).toHaveLength(0);
|
|
1446
|
+
});
|
|
1447
|
+
it('honors a pragma with a rationale on the line above', () => {
|
|
1448
|
+
const c = [
|
|
1449
|
+
'export function fire() {',
|
|
1450
|
+
' // @callback-invocation-safe-by-caller -- producer is already inside try',
|
|
1451
|
+
' subscribers.forEach((cb) => cb(payload))',
|
|
1452
|
+
'}',
|
|
1453
|
+
].join('\n');
|
|
1454
|
+
expect(run(c)).toHaveLength(0);
|
|
1455
|
+
});
|
|
1456
|
+
it('rejects a BARE pragma with no rationale', () => {
|
|
1457
|
+
const c = [
|
|
1458
|
+
'export function fire() {',
|
|
1459
|
+
' // @callback-invocation-safe-by-caller',
|
|
1460
|
+
' subscribers.forEach((cb) => cb(payload))',
|
|
1461
|
+
'}',
|
|
1462
|
+
].join('\n');
|
|
1463
|
+
const v = run(c);
|
|
1464
|
+
expect(v).toHaveLength(1);
|
|
1465
|
+
expect(v[0]?.message).toContain('rationale');
|
|
1466
|
+
});
|
|
1467
|
+
it('matches scope when packages/ appears mid-path (nested workspace)', () => {
|
|
1468
|
+
const c = 'export function fire() {\n subscribers.forEach((cb) => cb(payload))\n}';
|
|
1469
|
+
const v = run(c, 'repo/packages/x/src/notifier.ts');
|
|
1470
|
+
expect(v).toHaveLength(1);
|
|
1471
|
+
});
|
|
1472
|
+
it('does NOT flag a for-of over an unrecognised collection name', () => {
|
|
1473
|
+
const c = 'export function fire() {\n for (const cb of this.widgets) {\n cb(payload)\n }\n}';
|
|
1474
|
+
expect(run(c)).toHaveLength(0);
|
|
1475
|
+
});
|
|
1476
|
+
it('does NOT flag a for-of whose loop variable is never invoked', () => {
|
|
1477
|
+
const c = 'export function fire() {\n for (const cb of listeners) {\n log(cb)\n }\n}';
|
|
1478
|
+
expect(run(c)).toHaveLength(0);
|
|
1479
|
+
});
|
|
1480
|
+
it('does NOT flag a for-of whose body uses a safe<Name>() wrapper', () => {
|
|
1481
|
+
const c = 'export function fire() {\n for (const cb of observers) {\n this.safeObserver(cb, payload)\n }\n}';
|
|
1482
|
+
expect(run(c)).toHaveLength(0);
|
|
1483
|
+
});
|
|
1484
|
+
it('does NOT flag a for-of nested inside a try block', () => {
|
|
1485
|
+
const c = [
|
|
1486
|
+
'export function fire() {',
|
|
1487
|
+
' try {',
|
|
1488
|
+
' for (const cb of callbacks) {',
|
|
1489
|
+
' cb(payload)',
|
|
1490
|
+
' }',
|
|
1491
|
+
' } catch (e) {',
|
|
1492
|
+
' log(e)',
|
|
1493
|
+
' }',
|
|
1494
|
+
'}',
|
|
1495
|
+
].join('\n');
|
|
1496
|
+
expect(run(c)).toHaveLength(0);
|
|
1497
|
+
});
|
|
1498
|
+
it('honors a for-of opt-out pragma with a rationale', () => {
|
|
1499
|
+
const c = [
|
|
1500
|
+
'export function fire() {',
|
|
1501
|
+
' // @callback-invocation-safe-by-caller -- drained inside producer try',
|
|
1502
|
+
' for (const cb of handlers) {',
|
|
1503
|
+
' cb(payload)',
|
|
1504
|
+
' }',
|
|
1505
|
+
'}',
|
|
1506
|
+
].join('\n');
|
|
1507
|
+
expect(run(c)).toHaveLength(0);
|
|
1508
|
+
});
|
|
1509
|
+
});
|
|
1510
|
+
// ===========================================================================
|
|
1511
|
+
// async-waterfall-detection — analyze() skip arms + positive case
|
|
1512
|
+
// ===========================================================================
|
|
1513
|
+
async function runWaterfall(src, rel = 'src/wf.ts') {
|
|
1514
|
+
const abs = fx(rel, src);
|
|
1515
|
+
return runAbsolute('async-waterfall-detection', [abs]);
|
|
1516
|
+
}
|
|
1517
|
+
describe('async-waterfall-detection — analyze branches', () => {
|
|
1518
|
+
const run = runWaterfall;
|
|
1519
|
+
it('skips files in test paths', async () => {
|
|
1520
|
+
const src = 'export async function f() { await a(); await b() }';
|
|
1521
|
+
const result = await run(src, 'src/wf.test.ts');
|
|
1522
|
+
expect(result.signals).toHaveLength(0);
|
|
1523
|
+
});
|
|
1524
|
+
it('skips files without any await', async () => {
|
|
1525
|
+
const result = await run('export function f() { return 1 }');
|
|
1526
|
+
expect(result.signals).toHaveLength(0);
|
|
1527
|
+
});
|
|
1528
|
+
it('flags two independent consecutive awaits of function calls', async () => {
|
|
1529
|
+
// Variable names are chosen NOT to appear as substrings of the next
|
|
1530
|
+
// await's text (e.g. avoid single letters that occur in "await").
|
|
1531
|
+
const src = [
|
|
1532
|
+
'export async function f() {',
|
|
1533
|
+
' const userRow = await loadUser()',
|
|
1534
|
+
' const orgRow = await loadOrg()',
|
|
1535
|
+
' return [userRow, orgRow]',
|
|
1536
|
+
'}',
|
|
1537
|
+
].join('\n');
|
|
1538
|
+
const result = await run(src);
|
|
1539
|
+
expect(result.signals.some((s) => s.message.includes('parallelizable'))).toBe(true);
|
|
1540
|
+
});
|
|
1541
|
+
it('does NOT flag when the second await references the first result', async () => {
|
|
1542
|
+
const src = [
|
|
1543
|
+
'export async function f() {',
|
|
1544
|
+
' const userRow = await loadUser()',
|
|
1545
|
+
' const orgRow = await loadOrg(userRow)',
|
|
1546
|
+
' return orgRow',
|
|
1547
|
+
'}',
|
|
1548
|
+
].join('\n');
|
|
1549
|
+
const result = await run(src);
|
|
1550
|
+
expect(result.signals).toHaveLength(0);
|
|
1551
|
+
});
|
|
1552
|
+
it('does NOT flag awaits in different if/else branches', async () => {
|
|
1553
|
+
const src = [
|
|
1554
|
+
'export async function f(cond: boolean) {',
|
|
1555
|
+
' if (cond) {',
|
|
1556
|
+
' await loadA()',
|
|
1557
|
+
' } else {',
|
|
1558
|
+
' await loadB()',
|
|
1559
|
+
' }',
|
|
1560
|
+
'}',
|
|
1561
|
+
].join('\n');
|
|
1562
|
+
const result = await run(src);
|
|
1563
|
+
expect(result.signals).toHaveLength(0);
|
|
1564
|
+
});
|
|
1565
|
+
it('does NOT flag a sleep/delay call paired with another await', async () => {
|
|
1566
|
+
const src = ['export async function f() {', ' await sleep(100)', ' await loadB()', '}'].join('\n');
|
|
1567
|
+
const result = await run(src);
|
|
1568
|
+
expect(result.signals).toHaveLength(0);
|
|
1569
|
+
});
|
|
1570
|
+
it('does NOT flag a lock acquire followed by work', async () => {
|
|
1571
|
+
const src = [
|
|
1572
|
+
'export async function f(mutex: any) {',
|
|
1573
|
+
' await mutex.acquire()',
|
|
1574
|
+
' await doWork()',
|
|
1575
|
+
'}',
|
|
1576
|
+
].join('\n');
|
|
1577
|
+
const result = await run(src);
|
|
1578
|
+
expect(result.signals).toHaveLength(0);
|
|
1579
|
+
});
|
|
1580
|
+
it('does NOT flag when the second await uses a destructured binding from the first', async () => {
|
|
1581
|
+
const src = [
|
|
1582
|
+
'export async function f() {',
|
|
1583
|
+
' const { handler } = await import("./mod.js")',
|
|
1584
|
+
' await handler()',
|
|
1585
|
+
'}',
|
|
1586
|
+
].join('\n');
|
|
1587
|
+
const result = await run(src);
|
|
1588
|
+
expect(result.signals).toHaveLength(0);
|
|
1589
|
+
});
|
|
1590
|
+
it('does NOT flag when one await is not a function call (bare variable)', async () => {
|
|
1591
|
+
const src = [
|
|
1592
|
+
'export async function f(pending: Promise<number>) {',
|
|
1593
|
+
' const value = await pending',
|
|
1594
|
+
' const orgRow = await loadOrg()',
|
|
1595
|
+
' return [value, orgRow]',
|
|
1596
|
+
'}',
|
|
1597
|
+
].join('\n');
|
|
1598
|
+
const result = await run(src);
|
|
1599
|
+
expect(result.signals).toHaveLength(0);
|
|
1600
|
+
});
|
|
1601
|
+
it('does NOT flag awaits in different switch case branches', async () => {
|
|
1602
|
+
const src = [
|
|
1603
|
+
'export async function f(kind: string) {',
|
|
1604
|
+
' switch (kind) {',
|
|
1605
|
+
' case "a":',
|
|
1606
|
+
' await loadA()',
|
|
1607
|
+
' break',
|
|
1608
|
+
' default:',
|
|
1609
|
+
' await loadB()',
|
|
1610
|
+
' }',
|
|
1611
|
+
'}',
|
|
1612
|
+
].join('\n');
|
|
1613
|
+
const result = await run(src);
|
|
1614
|
+
expect(result.signals).toHaveLength(0);
|
|
1615
|
+
});
|
|
1616
|
+
it('does NOT flag a single await with no following await', async () => {
|
|
1617
|
+
const src = 'export async function f() { const a = await loadA(); return a }';
|
|
1618
|
+
const result = await run(src);
|
|
1619
|
+
expect(result.signals).toHaveLength(0);
|
|
1620
|
+
});
|
|
1621
|
+
});
|
|
1622
|
+
// ===========================================================================
|
|
1623
|
+
// module-coupling-fan-out — analyzeAll thresholds + barrel/d.ts exemptions
|
|
1624
|
+
// ===========================================================================
|
|
1625
|
+
describe('module-coupling-fan-out — analyzeAll branches', () => {
|
|
1626
|
+
it('emits an error for >30 imports and a warning for >15, sorted by fan-out', async () => {
|
|
1627
|
+
const godLeaves = buildLeaves('god', 31);
|
|
1628
|
+
const medLeaves = buildLeaves('med', 16);
|
|
1629
|
+
const godUses = godLeaves.map((_, i) => `v${i}`).join(',');
|
|
1630
|
+
const medUses = medLeaves.map((_, i) => `v${i}`).join(',');
|
|
1631
|
+
const god = fx('src/god-file.ts', `${importLines('god', 31)}\nexport const usesAll = [${godUses}]`);
|
|
1632
|
+
const med = fx('src/medium-file.ts', `${importLines('med', 16)}\nexport const usesAll = [${medUses}]`);
|
|
1633
|
+
const result = await runAbsolute('module-coupling-fan-out', [
|
|
1634
|
+
god,
|
|
1635
|
+
med,
|
|
1636
|
+
...godLeaves,
|
|
1637
|
+
...medLeaves,
|
|
1638
|
+
]);
|
|
1639
|
+
// Two violations: god-file (error, fan-out 31) sorts before medium (warning, 16).
|
|
1640
|
+
const fanViolations = result.signals.filter((s) => s.message.includes('High fan-out'));
|
|
1641
|
+
expect(fanViolations).toHaveLength(2);
|
|
1642
|
+
expect(fanViolations[0]?.message).toContain('31');
|
|
1643
|
+
expect(fanViolations[1]?.message).toContain('16');
|
|
1644
|
+
});
|
|
1645
|
+
it('auto-exempts pure barrel files even with high re-export fan-out', async () => {
|
|
1646
|
+
const leaves = buildLeaves('barrel', 20);
|
|
1647
|
+
const lines = leaves.map((_, i) => `export { v${i} } from "./barrel/leaf${i}.js"`).join('\n');
|
|
1648
|
+
const barrel = fx('src/index.ts', lines);
|
|
1649
|
+
const result = await runAbsolute('module-coupling-fan-out', [barrel, ...leaves]);
|
|
1650
|
+
expect(result.signals.filter((s) => s.message.includes('High fan-out'))).toHaveLength(0);
|
|
1651
|
+
});
|
|
1652
|
+
it('auto-exempts .d.ts type-declaration files', async () => {
|
|
1653
|
+
const leaves = buildLeaves('types', 18);
|
|
1654
|
+
// A .d.ts file with import + non-re-export content so isBarrelFile is false,
|
|
1655
|
+
// proving the .d.ts extension branch (not the barrel branch) does the exempting.
|
|
1656
|
+
const decl = fx('src/types.d.ts', `${importLines('types', 18)}\nexport declare const total: number`);
|
|
1657
|
+
const result = await runAbsolute('module-coupling-fan-out', [decl, ...leaves]);
|
|
1658
|
+
expect(result.signals.filter((s) => s.message.includes('High fan-out'))).toHaveLength(0);
|
|
1659
|
+
});
|
|
1660
|
+
it('treats a file with non-re-export top-level statements as a god-file, not a barrel', async () => {
|
|
1661
|
+
const leaves = buildLeaves('mix', 16);
|
|
1662
|
+
// Mix re-exports with a real const declaration — disqualifies the barrel heuristic.
|
|
1663
|
+
const lines = [
|
|
1664
|
+
'/* a block comment */',
|
|
1665
|
+
'// a line comment',
|
|
1666
|
+
...leaves.map((_, i) => `export { v${i} } from "./mix/leaf${i}.js"`),
|
|
1667
|
+
'export const computed = 1',
|
|
1668
|
+
].join('\n');
|
|
1669
|
+
const file = fx('src/mixed.ts', lines);
|
|
1670
|
+
const result = await runAbsolute('module-coupling-fan-out', [file, ...leaves]);
|
|
1671
|
+
expect(result.signals.filter((s) => s.message.includes('High fan-out'))).toHaveLength(1);
|
|
1672
|
+
});
|
|
1673
|
+
it('does not flag files under the warning threshold', async () => {
|
|
1674
|
+
const leaves = buildLeaves('small', 5);
|
|
1675
|
+
const file = fx('src/small.ts', `${importLines('small', 5)}\nexport const x = 1`);
|
|
1676
|
+
const result = await runAbsolute('module-coupling-fan-out', [file, ...leaves]);
|
|
1677
|
+
expect(result.signals.filter((s) => s.message.includes('High fan-out'))).toHaveLength(0);
|
|
1678
|
+
});
|
|
1679
|
+
});
|
|
1680
|
+
// ===========================================================================
|
|
1681
|
+
// display/index — icon + display-name lookup and fallback
|
|
1682
|
+
// ===========================================================================
|
|
1683
|
+
describe('display/index — getCheckIcon / getCheckDisplayName', () => {
|
|
1684
|
+
it('returns the mapped icon and name for a known check slug', () => {
|
|
1685
|
+
// circular-import-detection is a real entry in ARCHITECTURE_DISPLAY.
|
|
1686
|
+
const icon = getCheckIcon('circular-import-detection');
|
|
1687
|
+
const name = getCheckDisplayName('circular-import-detection');
|
|
1688
|
+
expect(icon.length).toBeGreaterThan(0);
|
|
1689
|
+
expect(name.length).toBeGreaterThan(0);
|
|
1690
|
+
// The mapped display name differs from the raw slug.
|
|
1691
|
+
expect(name).not.toBe('circular-import-detection');
|
|
1692
|
+
});
|
|
1693
|
+
it('falls back to a default icon for an unknown check and produces a non-empty name', () => {
|
|
1694
|
+
const icon = getCheckIcon('totally-unknown-check-slug');
|
|
1695
|
+
const name = getCheckDisplayName('totally-unknown-check-slug');
|
|
1696
|
+
expect(icon.length).toBeGreaterThan(0);
|
|
1697
|
+
expect(name.length).toBeGreaterThan(0);
|
|
1698
|
+
});
|
|
1699
|
+
});
|
|
1700
|
+
//# sourceMappingURL=behavior-fixtures-6.test.js.map
|