From 89e80f5387b01fc4d4fe1fa76e86e4b0656eec9d Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Mon, 8 Jun 2026 15:57:15 -0400 Subject: [PATCH 1/2] Move adapter shell test sources into fixtures --- ocaml/fixtures/base/dune-project | 1 + ocaml/fixtures/base/harnesses/dune | 5 + .../fixtures/base/harnesses/fuzz_decision.ml | 8 + ocaml/fixtures/base/lib/closure_core.ml | 6 + ocaml/fixtures/base/lib/crowbar.ml | 6 + ocaml/fixtures/base/lib/decision.ml | 5 + ocaml/fixtures/base/lib/dune | 5 + ocaml/fixtures/base/lib/handler.ml | 6 + ocaml/fixtures/base/lib/js_of_ocaml.ml | 6 + ocaml/fixtures/base/lib/qCheck2.ml | 17 + ocaml/fixtures/base/pp/dune | 8 + ocaml/fixtures/base/pp/preprocessed_core.ml | 4 + ocaml/fixtures/base/test/dune | 5 + ocaml/fixtures/base/test/test_closure.ml | 21 + ocaml/fixtures/base/test/test_decision.ml | 9 + .../base/test/test_preprocessed_core.ml | 9 + ocaml/fixtures/base/wasm/dune | 5 + ocaml/fixtures/base/wasm/export.ml | 8 + .../constant-only/lib/constant_only.ml | 5 + .../constant-only/test/test_constant_only.ml | 9 + ocaml/fixtures/dune | 5 + ocaml/fixtures/linkonly/lib/linkonly_core.ml | 4 + ocaml/fixtures/linkonly/test/test_linkonly.ml | 9 + .../fixtures/state-open-plain/lib/counter.ml | 9 + .../state-open-plain/lib/open_core.ml | 10 + .../state-open-plain/lib/open_shell.ml | 6 + ocaml/fixtures/state-open-plain/plain/dune | 5 + .../state-open-plain/plain/plain_exit_test.ml | 14 + .../state-open-plain/test/test_state.ml | 9 + ocaml/test.sh | 299 +---- swift/fixtures/_shared/apps/ios/project.yml | 11 + .../Backend/HTTPMailBackendClient.swift | 7 + .../Backend/HTTPMailBackendDecider.swift | 9 + .../HTTPMailBackendDeciderTests.swift | 11 + .../ios/MailApp/Backend/ClosureClient.swift | 11 + .../ios/MailApp/Backend/ClosureDecider.swift | 15 + .../MailAppTests/ClosureDeciderTests.swift | 20 + .../Backend/HTTPMailBackendClient.swift | 9 + .../Backend/HTTPMailBackendDecider.swift | 11 + .../HTTPMailBackendDeciderTests.swift | 11 + .../Backend/HTTPMailBackendClient.swift | 15 + .../Backend/HTTPMailBackendDecider.swift | 7 + .../HTTPMailBackendDeciderTests.swift | 11 + .../Backend/FakeMailBackendClient.swift | 7 + .../apps/ios/MailApp/Backend/Random.swift | 4 + .../MailApp/Backend/MailPrototypeData.swift | 6 + .../apps/ios/MailApp/Backend/Random.swift | 4 + .../Backend/SystemKeychainClient.swift | 7 + .../MailApp/Backend/MailBackendClient.swift | 5 + .../Backend/SQLiteMailSyncStateDecider.swift | 9 + .../Backend/SQLiteMailSyncStateDecider.swift | 3 + .../apps/ios/MailApp/App/MailApp.swift | 12 + .../MailApp/Backend/MailPrototypeData.swift | 6 + .../MailApp/Backend/AddAccountRequest.swift | 6 + .../Backend/BroadDomainDecision1.swift | 5 + .../Backend/BroadDomainDecision10.swift | 5 + .../Backend/BroadDomainDecision11.swift | 5 + .../Backend/BroadDomainDecision12.swift | 5 + .../Backend/BroadDomainDecision13.swift | 5 + .../Backend/BroadDomainDecision14.swift | 5 + .../Backend/BroadDomainDecision15.swift | 5 + .../Backend/BroadDomainDecision16.swift | 5 + .../Backend/BroadDomainDecision17.swift | 5 + .../Backend/BroadDomainDecision2.swift | 5 + .../Backend/BroadDomainDecision3.swift | 5 + .../Backend/BroadDomainDecision4.swift | 5 + .../Backend/BroadDomainDecision5.swift | 5 + .../Backend/BroadDomainDecision6.swift | 5 + .../Backend/BroadDomainDecision7.swift | 5 + .../Backend/BroadDomainDecision8.swift | 5 + .../Backend/BroadDomainDecision9.swift | 5 + .../ios/MailApp/Backend/BroadDomain1.swift | 5 + .../ios/MailApp/Backend/BroadDomain10.swift | 5 + .../ios/MailApp/Backend/BroadDomain11.swift | 5 + .../ios/MailApp/Backend/BroadDomain12.swift | 5 + .../ios/MailApp/Backend/BroadDomain13.swift | 5 + .../ios/MailApp/Backend/BroadDomain14.swift | 5 + .../ios/MailApp/Backend/BroadDomain15.swift | 5 + .../ios/MailApp/Backend/BroadDomain16.swift | 5 + .../ios/MailApp/Backend/BroadDomain17.swift | 5 + .../ios/MailApp/Backend/BroadDomain2.swift | 5 + .../ios/MailApp/Backend/BroadDomain3.swift | 5 + .../ios/MailApp/Backend/BroadDomain4.swift | 5 + .../ios/MailApp/Backend/BroadDomain5.swift | 5 + .../ios/MailApp/Backend/BroadDomain6.swift | 5 + .../ios/MailApp/Backend/BroadDomain7.swift | 5 + .../ios/MailApp/Backend/BroadDomain8.swift | 5 + .../ios/MailApp/Backend/BroadDomain9.swift | 5 + .../MailApp/Backend/AddAccountRequest.swift | 9 + .../Backend/HTTPMailBackendClient.swift | 8 + .../Backend/HTTPMailBackendDecider.swift | 7 + .../HTTPMailBackendDeciderTests.swift | 11 + .../ios/MailApp/Backend/RandomDecider.swift | 7 + .../apps/ios/MailApp/Backend/Random.swift | 3 + .../Backend/HTTPMailBackendClient.swift | 9 + .../Backend/HTTPMailBackendDecider.swift | 7 + .../HTTPMailBackendDeciderTests.swift | 7 + .../Backend/InstallationCredential.swift | 4 + .../apps/ios/MailApp/Backend/Random.swift | 1 + .../Backend/HTTPMailBackendDecider.swift | 7 + .../HTTPMailBackendDeciderTests.swift | 9 + .../MailApp/Backend/HTTPMailBackendCore.swift | 7 + .../Backend/HTTPMailBackendDecider.swift | 7 + .../HTTPMailBackendDeciderPropertySuite.swift | 13 + .../MailApp/Backend/AddAccountRequest.swift | 6 + .../Backend/HTTPMailBackendClient.swift | 11 + .../Backend/HTTPMailBackendDecider.swift | 7 + .../HTTPMailBackendDeciderTests.swift | 18 + .../apps/ios/MailAppTests/RandomDecider.swift | 7 + .../Backend/HTTPMailBackendDecider.swift | 7 + .../HTTPMailBackendDeciderPropertySuite.swift | 10 + .../Backend/HTTPMailBackendDecider.swift | 7 + .../HTTPMailBackendDeciderPropertySuite.swift | 14 + .../Backend/HTTPMailBackendDecider.swift | 7 + .../HTTPMailBackendDeciderTests.swift | 8 + .../Backend/HTTPMailBackendDecider.swift | 7 + .../HTTPMailBackendDeciderPropertySuite.swift | 17 + .../Backend/HTTPMailBackendDecider.swift | 7 + .../HTTPMailBackendDeciderPropertySuite.swift | 17 + .../Backend/FakeMailBackendClient.swift | 7 + .../Features/Mailboxes/MailboxRoute.swift | 7 + .../Backend/FakeMailBackendClient.swift | 9 + .../Backend/FakeMailBackendClient.swift | 11 + .../Backend/FakeMailBackendClient.swift | 11 + ...iteMailSyncStateDeciderPropertySuite.swift | 19 + .../Backend/SQLiteMailSyncStateDecider.swift | 7 + ...iteMailSyncStateDeciderPropertySuite.swift | 26 + ...iteMailSyncStateDeciderPropertySuite.swift | 15 + ...iteMailSyncStateDeciderPropertySuite.swift | 13 + .../Backend/SQLiteMailSyncStateDecider.swift | 7 + ...iteMailSyncStateDeciderPropertySuite.swift | 15 + .../Backend/FakeMailBackendClient.swift | 11 + .../HTTPMailBackendStatePropertySuite.swift | 15 + .../Backend/SQLiteMailSyncStateDecider.swift | 7 + .../Backend/SQLiteMailSyncStateStore.swift | 7 + ...iteMailSyncStateDeciderPropertySuite.swift | 15 + .../ios/MailApp/Domain/MailStaticData.swift | 5 + .../Backend/SQLiteMailSyncStateDecider.swift | 9 + .../Backend/SQLiteMailSyncStateSchema.swift | 8 + .../SQLiteMailSyncStateDeciderTests.swift | 11 + .../ios/MailApp/Backend/RandomTests.swift | 3 + .../FailingMailSyncStateStore.swift | 5 + .../Backend/SQLiteMailSyncStateDecision.swift | 3 + .../Backend/InstallationCredential.swift | 5 + .../Backend/HTTPMailBackendDecider.swift | 7 + .../HTTPMailBackendDeciderPropertySuite.swift | 13 + .../Backend/FakeMailBackendClient.swift | 9 + .../MailApp/Backend/AddAccountRequest.swift | 12 + .../MailApp/Backend/AddAccountRequest.swift | 9 + .../MailApp/Backend/AddAccountRequest.swift | 9 + .../Backend/HTTPMailBackendDecider.swift | 7 + .../HTTPMailBackendDeciderTests.swift | 7 + .../Backend/HTTPMailBackendDecider.swift | 7 + .../HTTPMailBackendDeciderTests.swift | 7 + swift/test.sh | 1026 +---------------- test/lib.sh | 15 + .../node_modules/@types/local-env/index.d.ts | 1 + .../@types/local-env/package.json | 5 + typescript/fixtures/base/src/closure.ts | 14 + typescript/fixtures/base/src/consumer.ts | 13 + typescript/fixtures/base/src/decision.ts | 15 + typescript/fixtures/base/src/handler.ts | 14 + .../fixtures/base/tests/closure.test.mts | 28 + .../fixtures/base/tests/decision.test.mts | 19 + typescript/fixtures/base/tsconfig.json | 11 + .../constant-only/src/constant-only.ts | 6 + .../tests/constant-only.test.mts | 15 + typescript/test.sh | 180 +-- 168 files changed, 1384 insertions(+), 1487 deletions(-) create mode 100644 ocaml/fixtures/base/dune-project create mode 100644 ocaml/fixtures/base/harnesses/dune create mode 100644 ocaml/fixtures/base/harnesses/fuzz_decision.ml create mode 100644 ocaml/fixtures/base/lib/closure_core.ml create mode 100644 ocaml/fixtures/base/lib/crowbar.ml create mode 100644 ocaml/fixtures/base/lib/decision.ml create mode 100644 ocaml/fixtures/base/lib/dune create mode 100644 ocaml/fixtures/base/lib/handler.ml create mode 100644 ocaml/fixtures/base/lib/js_of_ocaml.ml create mode 100644 ocaml/fixtures/base/lib/qCheck2.ml create mode 100644 ocaml/fixtures/base/pp/dune create mode 100644 ocaml/fixtures/base/pp/preprocessed_core.ml create mode 100644 ocaml/fixtures/base/test/dune create mode 100644 ocaml/fixtures/base/test/test_closure.ml create mode 100644 ocaml/fixtures/base/test/test_decision.ml create mode 100644 ocaml/fixtures/base/test/test_preprocessed_core.ml create mode 100644 ocaml/fixtures/base/wasm/dune create mode 100644 ocaml/fixtures/base/wasm/export.ml create mode 100644 ocaml/fixtures/constant-only/lib/constant_only.ml create mode 100644 ocaml/fixtures/constant-only/test/test_constant_only.ml create mode 100644 ocaml/fixtures/dune create mode 100644 ocaml/fixtures/linkonly/lib/linkonly_core.ml create mode 100644 ocaml/fixtures/linkonly/test/test_linkonly.ml create mode 100644 ocaml/fixtures/state-open-plain/lib/counter.ml create mode 100644 ocaml/fixtures/state-open-plain/lib/open_core.ml create mode 100644 ocaml/fixtures/state-open-plain/lib/open_shell.ml create mode 100644 ocaml/fixtures/state-open-plain/plain/dune create mode 100644 ocaml/fixtures/state-open-plain/plain/plain_exit_test.ml create mode 100644 ocaml/fixtures/state-open-plain/test/test_state.ml create mode 100644 swift/fixtures/_shared/apps/ios/project.yml create mode 100644 swift/fixtures/arbitrary-core-type-reference/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift create mode 100644 swift/fixtures/arbitrary-core-type-reference/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift create mode 100644 swift/fixtures/arbitrary-core-type-reference/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift create mode 100644 swift/fixtures/closure/apps/ios/MailApp/Backend/ClosureClient.swift create mode 100644 swift/fixtures/closure/apps/ios/MailApp/Backend/ClosureDecider.swift create mode 100644 swift/fixtures/closure/apps/ios/MailAppTests/ClosureDeciderTests.swift create mode 100644 swift/fixtures/core-enum-case-matching-shell-method/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift create mode 100644 swift/fixtures/core-enum-case-matching-shell-method/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift create mode 100644 swift/fixtures/core-enum-case-matching-shell-method/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift create mode 100644 swift/fixtures/cross-file-bare-reference/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift create mode 100644 swift/fixtures/cross-file-bare-reference/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift create mode 100644 swift/fixtures/cross-file-bare-reference/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift create mode 100644 swift/fixtures/database-queue-state-outside-state/apps/ios/MailApp/Backend/FakeMailBackendClient.swift create mode 100644 swift/fixtures/duplicate-domain-metadata/apps/ios/MailApp/Backend/Random.swift create mode 100644 swift/fixtures/duplicate-exempt-reason-metadata/apps/ios/MailApp/Backend/MailPrototypeData.swift create mode 100644 swift/fixtures/duplicate-module-metadata/apps/ios/MailApp/Backend/Random.swift create mode 100644 swift/fixtures/effect-boundary-exemption/apps/ios/MailApp/Backend/SystemKeychainClient.swift create mode 100644 swift/fixtures/effect-facade-exemption/apps/ios/MailApp/Backend/MailBackendClient.swift create mode 100644 swift/fixtures/effectful-decider/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift create mode 100644 swift/fixtures/empty-decider/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift create mode 100644 swift/fixtures/entrypoint-exemption/apps/ios/MailApp/App/MailApp.swift create mode 100644 swift/fixtures/exempt-domain/apps/ios/MailApp/Backend/MailPrototypeData.swift create mode 100644 swift/fixtures/exempt-reason-outside-exempt/apps/ios/MailApp/Backend/AddAccountRequest.swift create mode 100644 swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision1.swift create mode 100644 swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision10.swift create mode 100644 swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision11.swift create mode 100644 swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision12.swift create mode 100644 swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision13.swift create mode 100644 swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision14.swift create mode 100644 swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision15.swift create mode 100644 swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision16.swift create mode 100644 swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision17.swift create mode 100644 swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision2.swift create mode 100644 swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision3.swift create mode 100644 swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision4.swift create mode 100644 swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision5.swift create mode 100644 swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision6.swift create mode 100644 swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision7.swift create mode 100644 swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision8.swift create mode 100644 swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision9.swift create mode 100644 swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain1.swift create mode 100644 swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain10.swift create mode 100644 swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain11.swift create mode 100644 swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain12.swift create mode 100644 swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain13.swift create mode 100644 swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain14.swift create mode 100644 swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain15.swift create mode 100644 swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain16.swift create mode 100644 swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain17.swift create mode 100644 swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain2.swift create mode 100644 swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain3.swift create mode 100644 swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain4.swift create mode 100644 swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain5.swift create mode 100644 swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain6.swift create mode 100644 swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain7.swift create mode 100644 swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain8.swift create mode 100644 swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain9.swift create mode 100644 swift/fixtures/interface-with-logic/apps/ios/MailApp/Backend/AddAccountRequest.swift create mode 100644 swift/fixtures/local-core-name-only/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift create mode 100644 swift/fixtures/local-core-name-only/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift create mode 100644 swift/fixtures/local-core-name-only/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift create mode 100644 swift/fixtures/malformed-domain/apps/ios/MailApp/Backend/RandomDecider.swift create mode 100644 swift/fixtures/metadata-after-source/apps/ios/MailApp/Backend/Random.swift create mode 100644 swift/fixtures/missing-core-reference/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift create mode 100644 swift/fixtures/missing-core-reference/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift create mode 100644 swift/fixtures/missing-core-reference/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift create mode 100644 swift/fixtures/missing-exempt-reason/apps/ios/MailApp/Backend/InstallationCredential.swift create mode 100644 swift/fixtures/missing-metadata/apps/ios/MailApp/Backend/Random.swift create mode 100644 swift/fixtures/missing-property/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift create mode 100644 swift/fixtures/missing-property/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift create mode 100644 swift/fixtures/non-decider-core/apps/ios/MailApp/Backend/HTTPMailBackendCore.swift create mode 100644 swift/fixtures/ordinary-array-property/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift create mode 100644 swift/fixtures/ordinary-array-property/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift create mode 100644 swift/fixtures/passing/apps/ios/MailApp/Backend/AddAccountRequest.swift create mode 100644 swift/fixtures/passing/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift create mode 100644 swift/fixtures/passing/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift create mode 100644 swift/fixtures/passing/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift create mode 100644 swift/fixtures/production-module-in-test/apps/ios/MailAppTests/RandomDecider.swift create mode 100644 swift/fixtures/property-import-only/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift create mode 100644 swift/fixtures/property-import-only/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift create mode 100644 swift/fixtures/property-local-core-name-only/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift create mode 100644 swift/fixtures/property-local-core-name-only/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift create mode 100644 swift/fixtures/property-name-only/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift create mode 100644 swift/fixtures/property-name-only/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift create mode 100644 swift/fixtures/property-reachable-helper/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift create mode 100644 swift/fixtures/property-reachable-helper/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift create mode 100644 swift/fixtures/property-unreachable-helper/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift create mode 100644 swift/fixtures/property-unreachable-helper/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift create mode 100644 swift/fixtures/published-state-outside-state/apps/ios/MailApp/Backend/FakeMailBackendClient.swift create mode 100644 swift/fixtures/pure-glue-exemption/apps/ios/MailApp/Features/Mailboxes/MailboxRoute.swift create mode 100644 swift/fixtures/shared-state-comment-only/apps/ios/MailApp/Backend/FakeMailBackendClient.swift create mode 100644 swift/fixtures/shared-state-outside-state/apps/ios/MailApp/Backend/FakeMailBackendClient.swift create mode 100644 swift/fixtures/shared-state-without-state-test/apps/ios/MailApp/Backend/FakeMailBackendClient.swift create mode 100644 swift/fixtures/state-test-operation-sequence-refs/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift create mode 100644 swift/fixtures/state-test-operation-sequences-without-core-refs/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift create mode 100644 swift/fixtures/state-test-operation-sequences-without-core-refs/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift create mode 100644 swift/fixtures/state-test-operation-sequences-without-refs/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift create mode 100644 swift/fixtures/state-test-without-operation-sequences/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift create mode 100644 swift/fixtures/state-test-without-state-module/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift create mode 100644 swift/fixtures/state-test-without-state-module/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift create mode 100644 swift/fixtures/state-unrelated-to-operation-sequences/apps/ios/MailApp/Backend/FakeMailBackendClient.swift create mode 100644 swift/fixtures/state-unrelated-to-operation-sequences/apps/ios/MailAppTests/HTTPMailBackendStatePropertySuite.swift create mode 100644 swift/fixtures/state-without-stateful-apis/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift create mode 100644 swift/fixtures/state-without-stateful-apis/apps/ios/MailApp/Backend/SQLiteMailSyncStateStore.swift create mode 100644 swift/fixtures/state-without-stateful-apis/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift create mode 100644 swift/fixtures/static-data-exemption/apps/ios/MailApp/Domain/MailStaticData.swift create mode 100644 swift/fixtures/static-property-decision-surface/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift create mode 100644 swift/fixtures/static-property-decision-surface/apps/ios/MailApp/Backend/SQLiteMailSyncStateSchema.swift create mode 100644 swift/fixtures/static-property-decision-surface/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderTests.swift create mode 100644 swift/fixtures/test-module-in-production/apps/ios/MailApp/Backend/RandomTests.swift create mode 100644 swift/fixtures/test-support-exemption/apps/ios/MailAppTests/FailingMailSyncStateStore.swift create mode 100644 swift/fixtures/type-only-decider/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecision.swift create mode 100644 swift/fixtures/unknown-exempt-reason/apps/ios/MailApp/Backend/InstallationCredential.swift create mode 100644 swift/fixtures/unrelated-property-test/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift create mode 100644 swift/fixtures/unrelated-property-test/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift create mode 100644 swift/fixtures/user-defaults-state-outside-state/apps/ios/MailApp/Backend/FakeMailBackendClient.swift create mode 100644 swift/fixtures/value-with-branching-computed-property/apps/ios/MailApp/Backend/AddAccountRequest.swift create mode 100644 swift/fixtures/value-with-callable-decision/apps/ios/MailApp/Backend/AddAccountRequest.swift create mode 100644 swift/fixtures/value-with-computed-property/apps/ios/MailApp/Backend/AddAccountRequest.swift create mode 100644 swift/fixtures/wrong-domain-test/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift create mode 100644 swift/fixtures/wrong-domain-test/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift create mode 100644 swift/fixtures/wrong-test-module-type/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift create mode 100644 swift/fixtures/wrong-test-module-type/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift create mode 100644 typescript/fixtures/base/node_modules/@types/local-env/index.d.ts create mode 100644 typescript/fixtures/base/node_modules/@types/local-env/package.json create mode 100644 typescript/fixtures/base/src/closure.ts create mode 100644 typescript/fixtures/base/src/consumer.ts create mode 100644 typescript/fixtures/base/src/decision.ts create mode 100644 typescript/fixtures/base/src/handler.ts create mode 100644 typescript/fixtures/base/tests/closure.test.mts create mode 100644 typescript/fixtures/base/tests/decision.test.mts create mode 100644 typescript/fixtures/base/tsconfig.json create mode 100644 typescript/fixtures/constant-only/src/constant-only.ts create mode 100644 typescript/fixtures/constant-only/tests/constant-only.test.mts diff --git a/ocaml/fixtures/base/dune-project b/ocaml/fixtures/base/dune-project new file mode 100644 index 0000000..e1b8e59 --- /dev/null +++ b/ocaml/fixtures/base/dune-project @@ -0,0 +1 @@ +(lang dune 3.21) diff --git a/ocaml/fixtures/base/harnesses/dune b/ocaml/fixtures/base/harnesses/dune new file mode 100644 index 0000000..6d5621f --- /dev/null +++ b/ocaml/fixtures/base/harnesses/dune @@ -0,0 +1,5 @@ +(library + (name fixture_harnesses) + (wrapped false) + (modules :standard) + (libraries fixture_lib)) diff --git a/ocaml/fixtures/base/harnesses/fuzz_decision.ml b/ocaml/fixtures/base/harnesses/fuzz_decision.ml new file mode 100644 index 0000000..bed41df --- /dev/null +++ b/ocaml/fixtures/base/harnesses/fuzz_decision.ml @@ -0,0 +1,8 @@ +(* @archlint.module test + @archlint.domain demo.decision *) + +let register_fuzz_property () = + Crowbar.add_test ~name:"dependency-defined test scope" [ Crowbar.int ] (fun x -> + ignore (Decision.decide x)) + +let () = register_fuzz_property () diff --git a/ocaml/fixtures/base/lib/closure_core.ml b/ocaml/fixtures/base/lib/closure_core.ml new file mode 100644 index 0000000..30b121f --- /dev/null +++ b/ocaml/fixtures/base/lib/closure_core.ml @@ -0,0 +1,6 @@ +(* @archlint.module core + @archlint.domain demo.closure *) + +let decide_a x = x > 0 +let decide_b x = x < 0 +let decide_c x = x = 0 diff --git a/ocaml/fixtures/base/lib/crowbar.ml b/ocaml/fixtures/base/lib/crowbar.ml new file mode 100644 index 0000000..230cbfe --- /dev/null +++ b/ocaml/fixtures/base/lib/crowbar.ml @@ -0,0 +1,6 @@ +(* @archlint.module exempt + @archlint.exempt-reason test-support *) + +let int = 0 + +let add_test ~name:_ _args f = f 0 diff --git a/ocaml/fixtures/base/lib/decision.ml b/ocaml/fixtures/base/lib/decision.ml new file mode 100644 index 0000000..7057f3b --- /dev/null +++ b/ocaml/fixtures/base/lib/decision.ml @@ -0,0 +1,5 @@ +(* @archlint.module core + @archlint.domain demo.decision *) + +let decide x = + if x > 0 then `Positive else `Non_positive diff --git a/ocaml/fixtures/base/lib/dune b/ocaml/fixtures/base/lib/dune new file mode 100644 index 0000000..2214473 --- /dev/null +++ b/ocaml/fixtures/base/lib/dune @@ -0,0 +1,5 @@ +(library + (name fixture_lib) + (wrapped false) + (modules :standard) + (libraries unix)) diff --git a/ocaml/fixtures/base/lib/handler.ml b/ocaml/fixtures/base/lib/handler.ml new file mode 100644 index 0000000..a714b91 --- /dev/null +++ b/ocaml/fixtures/base/lib/handler.ml @@ -0,0 +1,6 @@ +(* @archlint.module shell + @archlint.domain demo.decision *) + +let run path = + let _exists = Unix.access path [ Unix.F_OK ] in + Decision.decide 1 diff --git a/ocaml/fixtures/base/lib/js_of_ocaml.ml b/ocaml/fixtures/base/lib/js_of_ocaml.ml new file mode 100644 index 0000000..2b9b10c --- /dev/null +++ b/ocaml/fixtures/base/lib/js_of_ocaml.ml @@ -0,0 +1,6 @@ +(* @archlint.module exempt + @archlint.exempt-reason test-support *) + +module Js = struct + let export _name _value = () +end diff --git a/ocaml/fixtures/base/lib/qCheck2.ml b/ocaml/fixtures/base/lib/qCheck2.ml new file mode 100644 index 0000000..ed6d4f0 --- /dev/null +++ b/ocaml/fixtures/base/lib/qCheck2.ml @@ -0,0 +1,17 @@ +(* @archlint.module exempt + @archlint.exempt-reason test-support *) + +module Gen = struct + type 'a t = unit + + let list (_ : 'a t) : 'a list t = () + let map (_ : 'a -> 'b) (_ : 'a t) : 'b t = () + let int : int t = () + let small_int : int t = () + let unit : unit t = () +end + +module Test = struct + let make ~name:_ (_ : 'a Gen.t) (_ : 'a -> bool) = () + let check_exn _ = () +end diff --git a/ocaml/fixtures/base/pp/dune b/ocaml/fixtures/base/pp/dune new file mode 100644 index 0000000..181be53 --- /dev/null +++ b/ocaml/fixtures/base/pp/dune @@ -0,0 +1,8 @@ +(library + (name fixture_pp) + (wrapped false) + (modules :standard) + (libraries fixture_lib) + (preprocess + (action + (run cat %{input-file})))) diff --git a/ocaml/fixtures/base/pp/preprocessed_core.ml b/ocaml/fixtures/base/pp/preprocessed_core.ml new file mode 100644 index 0000000..e139ca1 --- /dev/null +++ b/ocaml/fixtures/base/pp/preprocessed_core.ml @@ -0,0 +1,4 @@ +(* @archlint.module core + @archlint.domain demo.preprocessed *) + +let decide_preprocessed x = x >= 0 diff --git a/ocaml/fixtures/base/test/dune b/ocaml/fixtures/base/test/dune new file mode 100644 index 0000000..9379f07 --- /dev/null +++ b/ocaml/fixtures/base/test/dune @@ -0,0 +1,5 @@ +(library + (name fixture_tests) + (wrapped false) + (modules :standard) + (libraries fixture_lib fixture_pp)) diff --git a/ocaml/fixtures/base/test/test_closure.ml b/ocaml/fixtures/base/test/test_closure.ml new file mode 100644 index 0000000..5e27c18 --- /dev/null +++ b/ocaml/fixtures/base/test/test_closure.ml @@ -0,0 +1,21 @@ +(* @archlint.module test + @archlint.domain demo.closure *) + +let gen_b = + ignore (Closure_core.decide_b 0); + QCheck2.Gen.small_int + +let totality name f = + QCheck2.Test.make ~name QCheck2.Gen.small_int (fun x -> ignore (f x); true) + +let prop_a = + QCheck2.Test.make ~name:"a" QCheck2.Gen.small_int (fun x -> + Closure_core.decide_a x || x >= 0) + +let prop_b = QCheck2.Test.make ~name:"b" gen_b (fun (x : int) -> x = x) +let prop_c = totality "c" (fun x -> Closure_core.decide_c x) + +let () = + ignore prop_a; + ignore prop_b; + ignore prop_c diff --git a/ocaml/fixtures/base/test/test_decision.ml b/ocaml/fixtures/base/test/test_decision.ml new file mode 100644 index 0000000..72116a9 --- /dev/null +++ b/ocaml/fixtures/base/test/test_decision.ml @@ -0,0 +1,9 @@ +(* @archlint.module test + @archlint.domain demo.decision *) + +let suite = + [ + QCheck2.Test.make ~name:"decide total" + QCheck2.Gen.(list small_int) + (fun xs -> List.for_all (fun x -> match Decision.decide x with _ -> true) xs); + ] diff --git a/ocaml/fixtures/base/test/test_preprocessed_core.ml b/ocaml/fixtures/base/test/test_preprocessed_core.ml new file mode 100644 index 0000000..1429dc1 --- /dev/null +++ b/ocaml/fixtures/base/test/test_preprocessed_core.ml @@ -0,0 +1,9 @@ +(* @archlint.module test + @archlint.domain demo.preprocessed *) + +let prop = + QCheck2.Test.make ~name:"preprocessed core" + QCheck2.Gen.small_int + (fun x -> Preprocessed_core.decide_preprocessed x || x < 0) + +let () = ignore prop diff --git a/ocaml/fixtures/base/wasm/dune b/ocaml/fixtures/base/wasm/dune new file mode 100644 index 0000000..efe8887 --- /dev/null +++ b/ocaml/fixtures/base/wasm/dune @@ -0,0 +1,5 @@ +(library + (name fixture_wasm) + (wrapped false) + (modules :standard) + (libraries fixture_lib)) diff --git a/ocaml/fixtures/base/wasm/export.ml b/ocaml/fixtures/base/wasm/export.ml new file mode 100644 index 0000000..e540b99 --- /dev/null +++ b/ocaml/fixtures/base/wasm/export.ml @@ -0,0 +1,8 @@ +(* @archlint.module shell + @archlint.domain demo.decision *) + +open Js_of_ocaml + +let () = + Js.export "demo" + Decision.decide diff --git a/ocaml/fixtures/constant-only/lib/constant_only.ml b/ocaml/fixtures/constant-only/lib/constant_only.ml new file mode 100644 index 0000000..b4e0d29 --- /dev/null +++ b/ocaml/fixtures/constant-only/lib/constant_only.ml @@ -0,0 +1,5 @@ +(* @archlint.module core + @archlint.domain demo.constant *) + +let decide x = + if x > 0 then `Positive else `Non_positive diff --git a/ocaml/fixtures/constant-only/test/test_constant_only.ml b/ocaml/fixtures/constant-only/test/test_constant_only.ml new file mode 100644 index 0000000..7b491cc --- /dev/null +++ b/ocaml/fixtures/constant-only/test/test_constant_only.ml @@ -0,0 +1,9 @@ +(* @archlint.module test + @archlint.domain demo.constant *) + +let suite = + [ + QCheck2.Test.make ~name:"constant assertion" + QCheck2.Gen.unit + (fun () -> match Constant_only.decide 1 with _ -> true); + ] diff --git a/ocaml/fixtures/dune b/ocaml/fixtures/dune new file mode 100644 index 0000000..b92e34c --- /dev/null +++ b/ocaml/fixtures/dune @@ -0,0 +1,5 @@ +(data_only_dirs + base + constant-only + linkonly + state-open-plain) diff --git a/ocaml/fixtures/linkonly/lib/linkonly_core.ml b/ocaml/fixtures/linkonly/lib/linkonly_core.ml new file mode 100644 index 0000000..e77303f --- /dev/null +++ b/ocaml/fixtures/linkonly/lib/linkonly_core.ml @@ -0,0 +1,4 @@ +(* @archlint.module core + @archlint.domain demo.linkonly *) + +let decide_link x = x > 0 diff --git a/ocaml/fixtures/linkonly/test/test_linkonly.ml b/ocaml/fixtures/linkonly/test/test_linkonly.ml new file mode 100644 index 0000000..0ed3dce --- /dev/null +++ b/ocaml/fixtures/linkonly/test/test_linkonly.ml @@ -0,0 +1,9 @@ +(* @archlint.module test + @archlint.domain demo.linkonly *) + +let prop = + QCheck2.Test.make ~name:"link" QCheck2.Gen.unit (fun () -> + ignore Linkonly_core.decide_link; + true) + +let () = ignore prop diff --git a/ocaml/fixtures/state-open-plain/lib/counter.ml b/ocaml/fixtures/state-open-plain/lib/counter.ml new file mode 100644 index 0000000..33474e4 --- /dev/null +++ b/ocaml/fixtures/state-open-plain/lib/counter.ml @@ -0,0 +1,9 @@ +(* @archlint.module state + @archlint.domain demo.decision *) + +module Counter = struct + let cell = Atomic.make 0 +end + +let read () = + Decision.decide (Atomic.get Counter.cell) diff --git a/ocaml/fixtures/state-open-plain/lib/open_core.ml b/ocaml/fixtures/state-open-plain/lib/open_core.ml new file mode 100644 index 0000000..19e415f --- /dev/null +++ b/ocaml/fixtures/state-open-plain/lib/open_core.ml @@ -0,0 +1,10 @@ +(* @archlint.module core + @archlint.domain demo.opened *) + +open Open_shell + +let run () = decide () + +let incidental () = + let incidental = "local" in + incidental diff --git a/ocaml/fixtures/state-open-plain/lib/open_shell.ml b/ocaml/fixtures/state-open-plain/lib/open_shell.ml new file mode 100644 index 0000000..3ac4356 --- /dev/null +++ b/ocaml/fixtures/state-open-plain/lib/open_shell.ml @@ -0,0 +1,6 @@ +(* @archlint.module shell + @archlint.domain demo.opened *) + +let decide () = `Opened + +let incidental = "implementation" diff --git a/ocaml/fixtures/state-open-plain/plain/dune b/ocaml/fixtures/state-open-plain/plain/dune new file mode 100644 index 0000000..819cdd4 --- /dev/null +++ b/ocaml/fixtures/state-open-plain/plain/dune @@ -0,0 +1,5 @@ +; covers the (test ...) stanza form +(test + (name plain_exit_test) + (modules plain_exit_test) + (libraries fixture_lib)) diff --git a/ocaml/fixtures/state-open-plain/plain/plain_exit_test.ml b/ocaml/fixtures/state-open-plain/plain/plain_exit_test.ml new file mode 100644 index 0000000..9898214 --- /dev/null +++ b/ocaml/fixtures/state-open-plain/plain/plain_exit_test.ml @@ -0,0 +1,14 @@ +(* @archlint.module test + @archlint.domain demo.decision *) + +let failures = ref 0 + +let check name cond = + if cond then Stdlib.Printf.printf "ok: %s\n" name + else ( + Stdlib.incr failures; + Stdlib.Printf.printf "FAIL: %s\n" name) + +let () = + check "decide is positive" (match Decision.decide 1 with `Positive -> true | _ -> false); + if !failures > 0 then Stdlib.exit 1 diff --git a/ocaml/fixtures/state-open-plain/test/test_state.ml b/ocaml/fixtures/state-open-plain/test/test_state.ml new file mode 100644 index 0000000..7c228fa --- /dev/null +++ b/ocaml/fixtures/state-open-plain/test/test_state.ml @@ -0,0 +1,9 @@ +(* @archlint.module stateTest + @archlint.domain demo.decision *) + +let suite = + [ + QCheck2.Test.make ~name:"operation sequence" + QCheck2.Gen.(list small_int) + (fun xs -> List.for_all (fun x -> match Decision.decide x with _ -> true) xs); + ] diff --git a/ocaml/test.sh b/ocaml/test.sh index a069b1e..21f66bb 100644 --- a/ocaml/test.sh +++ b/ocaml/test.sh @@ -6,284 +6,29 @@ ARCHLINT_ROOT="$ROOT" . "$ROOT/test/lib.sh" TMPDIR="${TMPDIR:-/tmp}/archlint-ocaml-fixture-$$" trap 'rm -rf "$TMPDIR"' EXIT +export UV_CACHE_DIR="${UV_CACHE_DIR:-$TMPDIR/uv-cache}" -mkdir -p "$TMPDIR/lib" "$TMPDIR/pp" "$TMPDIR/test" "$TMPDIR/harnesses" "$TMPDIR/wasm" - -cat > "$TMPDIR/dune-project" <<'DUNE' -(lang dune 3.21) -DUNE - -cat > "$TMPDIR/lib/dune" <<'DUNE' -(library - (name fixture_lib) - (wrapped false) - (modules :standard) - (libraries unix)) -DUNE - -cat > "$TMPDIR/pp/dune" <<'DUNE' -(library - (name fixture_pp) - (wrapped false) - (modules :standard) - (libraries fixture_lib) - (preprocess - (action - (run cat %{input-file})))) -DUNE - -cat > "$TMPDIR/test/dune" <<'DUNE' -(library - (name fixture_tests) - (wrapped false) - (modules :standard) - (libraries fixture_lib fixture_pp)) -DUNE - -cat > "$TMPDIR/harnesses/dune" <<'DUNE' -(library - (name fixture_harnesses) - (wrapped false) - (modules :standard) - (libraries fixture_lib)) -DUNE - -cat > "$TMPDIR/wasm/dune" <<'DUNE' -(library - (name fixture_wasm) - (wrapped false) - (modules :standard) - (libraries fixture_lib)) -DUNE - -cat > "$TMPDIR/lib/qCheck2.ml" <<'ML' -(* @archlint.module exempt - @archlint.exempt-reason test-support *) - -module Gen = struct - type 'a t = unit - - let list (_ : 'a t) : 'a list t = () - let map (_ : 'a -> 'b) (_ : 'a t) : 'b t = () - let int : int t = () - let small_int : int t = () - let unit : unit t = () -end - -module Test = struct - let make ~name:_ (_ : 'a Gen.t) (_ : 'a -> bool) = () - let check_exn _ = () -end -ML - -cat > "$TMPDIR/lib/crowbar.ml" <<'ML' -(* @archlint.module exempt - @archlint.exempt-reason test-support *) - -let int = 0 - -let add_test ~name:_ _args f = f 0 -ML - -cat > "$TMPDIR/lib/js_of_ocaml.ml" <<'ML' -(* @archlint.module exempt - @archlint.exempt-reason test-support *) - -module Js = struct - let export _name _value = () -end -ML - -cat > "$TMPDIR/lib/decision.ml" <<'ML' -(* @archlint.module core - @archlint.domain demo.decision *) - -let decide x = - if x > 0 then `Positive else `Non_positive -ML - -cat > "$TMPDIR/lib/handler.ml" <<'ML' -(* @archlint.module shell - @archlint.domain demo.decision *) - -let run path = - let _exists = Unix.access path [ Unix.F_OK ] in - Decision.decide 1 -ML - -cat > "$TMPDIR/test/test_decision.ml" <<'ML' -(* @archlint.module test - @archlint.domain demo.decision *) - -let suite = - [ - QCheck2.Test.make ~name:"decide total" - QCheck2.Gen.(list small_int) - (fun xs -> List.for_all (fun x -> match Decision.decide x with _ -> true) xs); - ] -ML - -cat > "$TMPDIR/harnesses/fuzz_decision.ml" <<'ML' -(* @archlint.module test - @archlint.domain demo.decision *) - -let register_fuzz_property () = - Crowbar.add_test ~name:"dependency-defined test scope" [ Crowbar.int ] (fun x -> - ignore (Decision.decide x)) - -let () = register_fuzz_property () -ML - -cat > "$TMPDIR/wasm/export.ml" <<'ML' -(* @archlint.module shell - @archlint.domain demo.decision *) - -open Js_of_ocaml - -let () = - Js.export "demo" - Decision.decide -ML +copy_fixture "$ROOT/ocaml/fixtures/base" "$TMPDIR" # Dune action preprocessing records the typedtree source as [*.pp.ml]. # The adapter must map that generated source path back to the real source file. -cat > "$TMPDIR/pp/preprocessed_core.ml" <<'ML' -(* @archlint.module core - @archlint.domain demo.preprocessed *) - -let decide_preprocessed x = x >= 0 -ML - -cat > "$TMPDIR/test/test_preprocessed_core.ml" <<'ML' -(* @archlint.module test - @archlint.domain demo.preprocessed *) - -let prop = - QCheck2.Test.make ~name:"preprocessed core" - QCheck2.Gen.small_int - (fun x -> Preprocessed_core.decide_preprocessed x || x < 0) - -let () = ignore prop -ML # Call-graph closure for property coverage: a core whose three decision APIs # are each reached differently — directly in a callback (decide_a), only inside # a generator (decide_b), and only through a higher-order test helper (decide_c). # All must count, so coverage follows the call graph rather than the syntactic # callback alone. -cat > "$TMPDIR/lib/closure_core.ml" <<'ML' -(* @archlint.module core - @archlint.domain demo.closure *) - -let decide_a x = x > 0 -let decide_b x = x < 0 -let decide_c x = x = 0 -ML - -cat > "$TMPDIR/test/test_closure.ml" <<'ML' -(* @archlint.module test - @archlint.domain demo.closure *) - -let gen_b = - ignore (Closure_core.decide_b 0); - QCheck2.Gen.small_int - -let totality name f = - QCheck2.Test.make ~name QCheck2.Gen.small_int (fun x -> ignore (f x); true) - -let prop_a = - QCheck2.Test.make ~name:"a" QCheck2.Gen.small_int (fun x -> - Closure_core.decide_a x || x >= 0) - -let prop_b = QCheck2.Test.make ~name:"b" gen_b (fun (x : int) -> x = x) -let prop_c = totality "c" (fun x -> Closure_core.decide_c x) - -let () = - ignore prop_a; - ignore prop_b; - ignore prop_c -ML eval "$(opam env --switch "${ARCHLINT_OPAM_SWITCH:-$ROOT/ocaml}" --set-switch --shell=sh)" dune build --root "$ROOT/ocaml" >/dev/null dune exec --root "$ROOT/ocaml" ./main.exe -- --repo-root "$TMPDIR" --ocaml-root . > "$TMPDIR/facts.json" uv run --project "$ROOT" python "$ROOT/evaluate.py" "$TMPDIR/facts.json" -cat > "$TMPDIR/lib/counter.ml" <<'ML' -(* @archlint.module state - @archlint.domain demo.decision *) - -module Counter = struct - let cell = Atomic.make 0 -end - -let read () = - Decision.decide (Atomic.get Counter.cell) -ML - -cat > "$TMPDIR/test/test_state.ml" <<'ML' -(* @archlint.module stateTest - @archlint.domain demo.decision *) - -let suite = - [ - QCheck2.Test.make ~name:"operation sequence" - QCheck2.Gen.(list small_int) - (fun xs -> List.for_all (fun x -> match Decision.decide x with _ -> true) xs); - ] -ML - -cat > "$TMPDIR/lib/open_shell.ml" <<'ML' -(* @archlint.module shell - @archlint.domain demo.opened *) - -let decide () = `Opened - -let incidental = "implementation" -ML - -cat > "$TMPDIR/lib/open_core.ml" <<'ML' -(* @archlint.module core - @archlint.domain demo.opened *) - -open Open_shell - -let run () = decide () - -let incidental () = - let incidental = "local" in - incidental -ML +copy_fixture "$ROOT/ocaml/fixtures/state-open-plain" "$TMPDIR" # A hand-rolled harness in a dune (test ...) stanza: no Alcotest/QCheck/etc., # pass/fail signalled purely by the process exit code. The dune stanza is the # authoritative test-scope signal, so this must still be detected as a test. -mkdir -p "$TMPDIR/plain" -cat > "$TMPDIR/plain/dune" <<'DUNE' -; covers the (test ...) stanza form -(test - (name plain_exit_test) - (modules plain_exit_test) - (libraries fixture_lib)) -DUNE - -cat > "$TMPDIR/plain/plain_exit_test.ml" <<'ML' -(* @archlint.module test - @archlint.domain demo.decision *) - -let failures = ref 0 - -let check name cond = - if cond then Stdlib.Printf.printf "ok: %s\n" name - else ( - Stdlib.incr failures; - Stdlib.Printf.printf "FAIL: %s\n" name) - -let () = - check "decide is positive" (match Decision.decide 1 with `Positive -> true | _ -> false); - if !failures > 0 then Stdlib.exit 1 -ML - dune exec --root "$ROOT/ocaml" ./main.exe -- --repo-root "$TMPDIR" --ocaml-root . > "$TMPDIR/facts-state.json" assert_facts "$TMPDIR/facts-state.json" <<'PY' import json @@ -330,25 +75,7 @@ assert "Open_shell.decide" in open_core["qualifiedReferences"], open_core assert "Open_shell.incidental" not in open_core["qualifiedReferences"], open_core PY -cat > "$TMPDIR/lib/constant_only.ml" <<'ML' -(* @archlint.module core - @archlint.domain demo.constant *) - -let decide x = - if x > 0 then `Positive else `Non_positive -ML - -cat > "$TMPDIR/test/test_constant_only.ml" <<'ML' -(* @archlint.module test - @archlint.domain demo.constant *) - -let suite = - [ - QCheck2.Test.make ~name:"constant assertion" - QCheck2.Gen.unit - (fun () -> match Constant_only.decide 1 with _ -> true); - ] -ML +copy_fixture "$ROOT/ocaml/fixtures/constant-only" "$TMPDIR" constant_lint() { dune exec --root "$ROOT/ocaml" ./main.exe -- --repo-root "$TMPDIR" --ocaml-root . \ @@ -358,22 +85,6 @@ expect_violation "core module property tests must reference every decision API: # Regression: a linking-only property over [unit] that merely [ignore]s the API # is NOT meaningful, so closure must not let it satisfy coverage. -cat > "$TMPDIR/lib/linkonly_core.ml" <<'ML' -(* @archlint.module core - @archlint.domain demo.linkonly *) - -let decide_link x = x > 0 -ML - -cat > "$TMPDIR/test/test_linkonly.ml" <<'ML' -(* @archlint.module test - @archlint.domain demo.linkonly *) - -let prop = - QCheck2.Test.make ~name:"link" QCheck2.Gen.unit (fun () -> - ignore Linkonly_core.decide_link; - true) +copy_fixture "$ROOT/ocaml/fixtures/linkonly" "$TMPDIR" -let () = ignore prop -ML expect_violation "core module property tests must reference every decision API: decide_link" constant_lint diff --git a/swift/fixtures/_shared/apps/ios/project.yml b/swift/fixtures/_shared/apps/ios/project.yml new file mode 100644 index 0000000..9cea781 --- /dev/null +++ b/swift/fixtures/_shared/apps/ios/project.yml @@ -0,0 +1,11 @@ +targets: + MailApp: + type: application + sources: + - MailApp + MailAppTests: + type: bundle.unit-test + sources: + - MailAppTests + dependencies: + - target: MailApp diff --git a/swift/fixtures/arbitrary-core-type-reference/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift b/swift/fixtures/arbitrary-core-type-reference/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift new file mode 100644 index 0000000..8409fff --- /dev/null +++ b/swift/fixtures/arbitrary-core-type-reference/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift @@ -0,0 +1,7 @@ +// @archlint.module shell +// @archlint.domain backend.http +struct HTTPMailBackendClient { + func handle(_ value: CoreVocabulary) { + _ = value + } +} diff --git a/swift/fixtures/arbitrary-core-type-reference/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift b/swift/fixtures/arbitrary-core-type-reference/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift new file mode 100644 index 0000000..3998e35 --- /dev/null +++ b/swift/fixtures/arbitrary-core-type-reference/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift @@ -0,0 +1,9 @@ +// @archlint.module core +// @archlint.domain backend.http +struct CoreVocabulary {} + +enum HTTPMailBackendDecider { + static func decidePath() -> String { + "/v1/accounts" + } +} diff --git a/swift/fixtures/arbitrary-core-type-reference/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift b/swift/fixtures/arbitrary-core-type-reference/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift new file mode 100644 index 0000000..666ca93 --- /dev/null +++ b/swift/fixtures/arbitrary-core-type-reference/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift @@ -0,0 +1,11 @@ +// @archlint.module test +// @archlint.domain backend.http +import PropertyBased +import Testing + +@Test +func pathProperty() async { + await propertyCheck(input: Gen.int(in: 0...10)) { _ in + #expect(HTTPMailBackendDecider.decidePath() == "/v1/accounts") + } +} diff --git a/swift/fixtures/closure/apps/ios/MailApp/Backend/ClosureClient.swift b/swift/fixtures/closure/apps/ios/MailApp/Backend/ClosureClient.swift new file mode 100644 index 0000000..99ae11a --- /dev/null +++ b/swift/fixtures/closure/apps/ios/MailApp/Backend/ClosureClient.swift @@ -0,0 +1,11 @@ +// @archlint.module shell +// @archlint.domain backend.closure +import Foundation + +struct ClosureClient { + func handle() -> URLRequest { + let path = ClosureDecider.decideA(1) ? "/v1/accounts" : "/v1/accounts" + let url = URL(string: "http://localhost" + path)! + return URLRequest(url: url) + } +} diff --git a/swift/fixtures/closure/apps/ios/MailApp/Backend/ClosureDecider.swift b/swift/fixtures/closure/apps/ios/MailApp/Backend/ClosureDecider.swift new file mode 100644 index 0000000..3af620a --- /dev/null +++ b/swift/fixtures/closure/apps/ios/MailApp/Backend/ClosureDecider.swift @@ -0,0 +1,15 @@ +// @archlint.module core +// @archlint.domain backend.closure +enum ClosureDecider { + static func decideA(_ shard: Int) -> Bool { + shard >= 0 + } + + static func decideB(_ shard: Int) -> Bool { + shard < 0 + } + + static func decideC(_ shard: Int) -> Bool { + shard == 0 + } +} diff --git a/swift/fixtures/closure/apps/ios/MailAppTests/ClosureDeciderTests.swift b/swift/fixtures/closure/apps/ios/MailAppTests/ClosureDeciderTests.swift new file mode 100644 index 0000000..e6c43a9 --- /dev/null +++ b/swift/fixtures/closure/apps/ios/MailAppTests/ClosureDeciderTests.swift @@ -0,0 +1,20 @@ +// @archlint.module test +// @archlint.domain backend.closure +import PropertyBased +import Testing + +func helper(_ shard: Int) -> Bool { + ClosureDecider.decideC(shard) +} + +@Test +func closureProperty() async { + await propertyCheck( + input: Gen.int(in: 0...10).map { shard in + _ = ClosureDecider.decideB(shard) + return shard + } + ) { shard in + #expect(ClosureDecider.decideA(shard) || helper(shard) || shard >= 0) + } +} diff --git a/swift/fixtures/core-enum-case-matching-shell-method/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift b/swift/fixtures/core-enum-case-matching-shell-method/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift new file mode 100644 index 0000000..5c42300 --- /dev/null +++ b/swift/fixtures/core-enum-case-matching-shell-method/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift @@ -0,0 +1,9 @@ +// @archlint.module shell +// @archlint.domain backend.http +import Foundation + +struct HTTPMailBackendClient { + func add() -> URLRequest { + return URLRequest(url: URL(string: "http://localhost")!) + } +} diff --git a/swift/fixtures/core-enum-case-matching-shell-method/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift b/swift/fixtures/core-enum-case-matching-shell-method/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift new file mode 100644 index 0000000..fe9fdff --- /dev/null +++ b/swift/fixtures/core-enum-case-matching-shell-method/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift @@ -0,0 +1,11 @@ +// @archlint.module core +// @archlint.domain backend.http +enum HTTPMailBackendDecider { + enum Decision { + case add + } + + static func decide(_ shard: Int) -> Decision { + shard >= 0 ? .add : .add + } +} diff --git a/swift/fixtures/core-enum-case-matching-shell-method/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift b/swift/fixtures/core-enum-case-matching-shell-method/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift new file mode 100644 index 0000000..c18f8bd --- /dev/null +++ b/swift/fixtures/core-enum-case-matching-shell-method/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift @@ -0,0 +1,11 @@ +// @archlint.module test +// @archlint.domain backend.http +import PropertyBased +import Testing + +@Test +func decisionProperty() async { + await propertyCheck(input: Gen.int(in: 0...10)) { shard in + #expect(HTTPMailBackendDecider.decide(shard) == .add) + } +} diff --git a/swift/fixtures/cross-file-bare-reference/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift b/swift/fixtures/cross-file-bare-reference/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift new file mode 100644 index 0000000..786a5ed --- /dev/null +++ b/swift/fixtures/cross-file-bare-reference/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift @@ -0,0 +1,15 @@ +// @archlint.module shell +// @archlint.domain backend.http +import Foundation + +func decidePathForRequest(_ shard: Int) -> String { + HTTPMailBackendDecider.decidePath(shard) +} + +final class HTTPMailBackendClient { + func makeRequest() -> URLRequest { + let path = decidePathForRequest(0) + let url = URL(string: "http://localhost" + path)! + return URLRequest(url: url) + } +} diff --git a/swift/fixtures/cross-file-bare-reference/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift b/swift/fixtures/cross-file-bare-reference/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift new file mode 100644 index 0000000..99133d0 --- /dev/null +++ b/swift/fixtures/cross-file-bare-reference/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift @@ -0,0 +1,7 @@ +// @archlint.module core +// @archlint.domain backend.http +enum HTTPMailBackendDecider { + static func decidePath(_ shard: Int) -> String { + shard >= 0 ? "/v1/accounts" : "/v1/accounts" + } +} diff --git a/swift/fixtures/cross-file-bare-reference/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift b/swift/fixtures/cross-file-bare-reference/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift new file mode 100644 index 0000000..40d8c29 --- /dev/null +++ b/swift/fixtures/cross-file-bare-reference/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift @@ -0,0 +1,11 @@ +// @archlint.module test +// @archlint.domain backend.http +import PropertyBased +import Testing + +@Test +func pathProperty() async { + await propertyCheck(input: Gen.int(in: 0...10)) { shard in + #expect(HTTPMailBackendDecider.decidePath(shard) == "/v1/accounts") + } +} diff --git a/swift/fixtures/database-queue-state-outside-state/apps/ios/MailApp/Backend/FakeMailBackendClient.swift b/swift/fixtures/database-queue-state-outside-state/apps/ios/MailApp/Backend/FakeMailBackendClient.swift new file mode 100644 index 0000000..d37c607 --- /dev/null +++ b/swift/fixtures/database-queue-state-outside-state/apps/ios/MailApp/Backend/FakeMailBackendClient.swift @@ -0,0 +1,7 @@ +// @archlint.module shell +// @archlint.domain backend.http +import GRDB + +struct FakeMailBackendClient { + let database: DatabaseQueue +} diff --git a/swift/fixtures/duplicate-domain-metadata/apps/ios/MailApp/Backend/Random.swift b/swift/fixtures/duplicate-domain-metadata/apps/ios/MailApp/Backend/Random.swift new file mode 100644 index 0000000..b78f817 --- /dev/null +++ b/swift/fixtures/duplicate-domain-metadata/apps/ios/MailApp/Backend/Random.swift @@ -0,0 +1,4 @@ +// @archlint.module value +// @archlint.domain random.value +// @archlint.domain random.other +struct RandomValue {} diff --git a/swift/fixtures/duplicate-exempt-reason-metadata/apps/ios/MailApp/Backend/MailPrototypeData.swift b/swift/fixtures/duplicate-exempt-reason-metadata/apps/ios/MailApp/Backend/MailPrototypeData.swift new file mode 100644 index 0000000..8b19a77 --- /dev/null +++ b/swift/fixtures/duplicate-exempt-reason-metadata/apps/ios/MailApp/Backend/MailPrototypeData.swift @@ -0,0 +1,6 @@ +// @archlint.module exempt +// @archlint.exempt-reason static-data +// @archlint.exempt-reason test-support +enum MailPrototypeData { + static let account = "demo" +} diff --git a/swift/fixtures/duplicate-module-metadata/apps/ios/MailApp/Backend/Random.swift b/swift/fixtures/duplicate-module-metadata/apps/ios/MailApp/Backend/Random.swift new file mode 100644 index 0000000..4936532 --- /dev/null +++ b/swift/fixtures/duplicate-module-metadata/apps/ios/MailApp/Backend/Random.swift @@ -0,0 +1,4 @@ +// @archlint.module core +// @archlint.module value +// @archlint.domain random.value +struct RandomValue {} diff --git a/swift/fixtures/effect-boundary-exemption/apps/ios/MailApp/Backend/SystemKeychainClient.swift b/swift/fixtures/effect-boundary-exemption/apps/ios/MailApp/Backend/SystemKeychainClient.swift new file mode 100644 index 0000000..3921fc8 --- /dev/null +++ b/swift/fixtures/effect-boundary-exemption/apps/ios/MailApp/Backend/SystemKeychainClient.swift @@ -0,0 +1,7 @@ +// @archlint.module exempt +// @archlint.exempt-reason effect-boundary +import Security + +struct SystemKeychainClient { + let keychainClass: CFString +} diff --git a/swift/fixtures/effect-facade-exemption/apps/ios/MailApp/Backend/MailBackendClient.swift b/swift/fixtures/effect-facade-exemption/apps/ios/MailApp/Backend/MailBackendClient.swift new file mode 100644 index 0000000..5bfd91b --- /dev/null +++ b/swift/fixtures/effect-facade-exemption/apps/ios/MailApp/Backend/MailBackendClient.swift @@ -0,0 +1,5 @@ +// @archlint.module exempt +// @archlint.exempt-reason effect-facade +protocol MailBackendClient { + func sync() async throws +} diff --git a/swift/fixtures/effectful-decider/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift b/swift/fixtures/effectful-decider/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift new file mode 100644 index 0000000..493c604 --- /dev/null +++ b/swift/fixtures/effectful-decider/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift @@ -0,0 +1,9 @@ +// @archlint.module core +// @archlint.domain backend.http +import Foundation + +enum SQLiteMailSyncStateDecider { + static func decideRequest() -> URLRequest { + URLRequest(url: URL(string: "http://localhost")!) + } +} diff --git a/swift/fixtures/empty-decider/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift b/swift/fixtures/empty-decider/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift new file mode 100644 index 0000000..d7187f3 --- /dev/null +++ b/swift/fixtures/empty-decider/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift @@ -0,0 +1,3 @@ +// @archlint.module core +// @archlint.domain backend.http +enum SQLiteMailSyncStateDecider {} diff --git a/swift/fixtures/entrypoint-exemption/apps/ios/MailApp/App/MailApp.swift b/swift/fixtures/entrypoint-exemption/apps/ios/MailApp/App/MailApp.swift new file mode 100644 index 0000000..10b3c40 --- /dev/null +++ b/swift/fixtures/entrypoint-exemption/apps/ios/MailApp/App/MailApp.swift @@ -0,0 +1,12 @@ +// @archlint.module exempt +// @archlint.exempt-reason entrypoint +import SwiftUI + +@main +struct MailApp: App { + var body: some Scene { + WindowGroup { + Text("Mail") + } + } +} diff --git a/swift/fixtures/exempt-domain/apps/ios/MailApp/Backend/MailPrototypeData.swift b/swift/fixtures/exempt-domain/apps/ios/MailApp/Backend/MailPrototypeData.swift new file mode 100644 index 0000000..a948262 --- /dev/null +++ b/swift/fixtures/exempt-domain/apps/ios/MailApp/Backend/MailPrototypeData.swift @@ -0,0 +1,6 @@ +// @archlint.module exempt +// @archlint.domain mail.sync +// @archlint.exempt-reason static-data +enum MailPrototypeData { + static let account = "demo" +} diff --git a/swift/fixtures/exempt-reason-outside-exempt/apps/ios/MailApp/Backend/AddAccountRequest.swift b/swift/fixtures/exempt-reason-outside-exempt/apps/ios/MailApp/Backend/AddAccountRequest.swift new file mode 100644 index 0000000..271af60 --- /dev/null +++ b/swift/fixtures/exempt-reason-outside-exempt/apps/ios/MailApp/Backend/AddAccountRequest.swift @@ -0,0 +1,6 @@ +// @archlint.module interface +// @archlint.domain backend.http +// @archlint.exempt-reason static-data +struct AddAccountRequest { + let displayName: String +} diff --git a/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision1.swift b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision1.swift new file mode 100644 index 0000000..e9b2586 --- /dev/null +++ b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision1.swift @@ -0,0 +1,5 @@ +// @archlint.module core +// @archlint.domain overbroad.example +func decideBroadDomain1() -> Bool { + true +} diff --git a/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision10.swift b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision10.swift new file mode 100644 index 0000000..4cc6797 --- /dev/null +++ b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision10.swift @@ -0,0 +1,5 @@ +// @archlint.module core +// @archlint.domain overbroad.example +func decideBroadDomain10() -> Bool { + true +} diff --git a/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision11.swift b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision11.swift new file mode 100644 index 0000000..0ceb5c6 --- /dev/null +++ b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision11.swift @@ -0,0 +1,5 @@ +// @archlint.module core +// @archlint.domain overbroad.example +func decideBroadDomain11() -> Bool { + true +} diff --git a/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision12.swift b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision12.swift new file mode 100644 index 0000000..ef51a9d --- /dev/null +++ b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision12.swift @@ -0,0 +1,5 @@ +// @archlint.module core +// @archlint.domain overbroad.example +func decideBroadDomain12() -> Bool { + true +} diff --git a/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision13.swift b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision13.swift new file mode 100644 index 0000000..3fd058c --- /dev/null +++ b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision13.swift @@ -0,0 +1,5 @@ +// @archlint.module core +// @archlint.domain overbroad.example +func decideBroadDomain13() -> Bool { + true +} diff --git a/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision14.swift b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision14.swift new file mode 100644 index 0000000..30f5233 --- /dev/null +++ b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision14.swift @@ -0,0 +1,5 @@ +// @archlint.module core +// @archlint.domain overbroad.example +func decideBroadDomain14() -> Bool { + true +} diff --git a/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision15.swift b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision15.swift new file mode 100644 index 0000000..9a6b62f --- /dev/null +++ b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision15.swift @@ -0,0 +1,5 @@ +// @archlint.module core +// @archlint.domain overbroad.example +func decideBroadDomain15() -> Bool { + true +} diff --git a/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision16.swift b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision16.swift new file mode 100644 index 0000000..1371609 --- /dev/null +++ b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision16.swift @@ -0,0 +1,5 @@ +// @archlint.module core +// @archlint.domain overbroad.example +func decideBroadDomain16() -> Bool { + true +} diff --git a/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision17.swift b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision17.swift new file mode 100644 index 0000000..81bed6f --- /dev/null +++ b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision17.swift @@ -0,0 +1,5 @@ +// @archlint.module core +// @archlint.domain overbroad.example +func decideBroadDomain17() -> Bool { + true +} diff --git a/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision2.swift b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision2.swift new file mode 100644 index 0000000..588d321 --- /dev/null +++ b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision2.swift @@ -0,0 +1,5 @@ +// @archlint.module core +// @archlint.domain overbroad.example +func decideBroadDomain2() -> Bool { + true +} diff --git a/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision3.swift b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision3.swift new file mode 100644 index 0000000..b407014 --- /dev/null +++ b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision3.swift @@ -0,0 +1,5 @@ +// @archlint.module core +// @archlint.domain overbroad.example +func decideBroadDomain3() -> Bool { + true +} diff --git a/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision4.swift b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision4.swift new file mode 100644 index 0000000..e7c7d22 --- /dev/null +++ b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision4.swift @@ -0,0 +1,5 @@ +// @archlint.module core +// @archlint.domain overbroad.example +func decideBroadDomain4() -> Bool { + true +} diff --git a/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision5.swift b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision5.swift new file mode 100644 index 0000000..d1d4b01 --- /dev/null +++ b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision5.swift @@ -0,0 +1,5 @@ +// @archlint.module core +// @archlint.domain overbroad.example +func decideBroadDomain5() -> Bool { + true +} diff --git a/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision6.swift b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision6.swift new file mode 100644 index 0000000..3815430 --- /dev/null +++ b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision6.swift @@ -0,0 +1,5 @@ +// @archlint.module core +// @archlint.domain overbroad.example +func decideBroadDomain6() -> Bool { + true +} diff --git a/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision7.swift b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision7.swift new file mode 100644 index 0000000..10d70ee --- /dev/null +++ b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision7.swift @@ -0,0 +1,5 @@ +// @archlint.module core +// @archlint.domain overbroad.example +func decideBroadDomain7() -> Bool { + true +} diff --git a/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision8.swift b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision8.swift new file mode 100644 index 0000000..d2531ef --- /dev/null +++ b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision8.swift @@ -0,0 +1,5 @@ +// @archlint.module core +// @archlint.domain overbroad.example +func decideBroadDomain8() -> Bool { + true +} diff --git a/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision9.swift b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision9.swift new file mode 100644 index 0000000..4733b68 --- /dev/null +++ b/swift/fixtures/generic-domain/apps/ios/MailApp/Backend/BroadDomainDecision9.swift @@ -0,0 +1,5 @@ +// @archlint.module core +// @archlint.domain overbroad.example +func decideBroadDomain9() -> Bool { + true +} diff --git a/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain1.swift b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain1.swift new file mode 100644 index 0000000..a418224 --- /dev/null +++ b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain1.swift @@ -0,0 +1,5 @@ +// @archlint.module interface +// @archlint.domain overbroad.example +struct BroadDomain1 { + let value: String +} diff --git a/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain10.swift b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain10.swift new file mode 100644 index 0000000..98be313 --- /dev/null +++ b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain10.swift @@ -0,0 +1,5 @@ +// @archlint.module interface +// @archlint.domain overbroad.example +struct BroadDomain10 { + let value: String +} diff --git a/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain11.swift b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain11.swift new file mode 100644 index 0000000..21c0bfa --- /dev/null +++ b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain11.swift @@ -0,0 +1,5 @@ +// @archlint.module interface +// @archlint.domain overbroad.example +struct BroadDomain11 { + let value: String +} diff --git a/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain12.swift b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain12.swift new file mode 100644 index 0000000..50ddc80 --- /dev/null +++ b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain12.swift @@ -0,0 +1,5 @@ +// @archlint.module interface +// @archlint.domain overbroad.example +struct BroadDomain12 { + let value: String +} diff --git a/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain13.swift b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain13.swift new file mode 100644 index 0000000..3b8929f --- /dev/null +++ b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain13.swift @@ -0,0 +1,5 @@ +// @archlint.module interface +// @archlint.domain overbroad.example +struct BroadDomain13 { + let value: String +} diff --git a/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain14.swift b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain14.swift new file mode 100644 index 0000000..f6a57f7 --- /dev/null +++ b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain14.swift @@ -0,0 +1,5 @@ +// @archlint.module interface +// @archlint.domain overbroad.example +struct BroadDomain14 { + let value: String +} diff --git a/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain15.swift b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain15.swift new file mode 100644 index 0000000..27f3d0b --- /dev/null +++ b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain15.swift @@ -0,0 +1,5 @@ +// @archlint.module interface +// @archlint.domain overbroad.example +struct BroadDomain15 { + let value: String +} diff --git a/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain16.swift b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain16.swift new file mode 100644 index 0000000..c8d5710 --- /dev/null +++ b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain16.swift @@ -0,0 +1,5 @@ +// @archlint.module interface +// @archlint.domain overbroad.example +struct BroadDomain16 { + let value: String +} diff --git a/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain17.swift b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain17.swift new file mode 100644 index 0000000..9f9c8bd --- /dev/null +++ b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain17.swift @@ -0,0 +1,5 @@ +// @archlint.module interface +// @archlint.domain overbroad.example +struct BroadDomain17 { + let value: String +} diff --git a/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain2.swift b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain2.swift new file mode 100644 index 0000000..d5ed0f1 --- /dev/null +++ b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain2.swift @@ -0,0 +1,5 @@ +// @archlint.module interface +// @archlint.domain overbroad.example +struct BroadDomain2 { + let value: String +} diff --git a/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain3.swift b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain3.swift new file mode 100644 index 0000000..ff41e71 --- /dev/null +++ b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain3.swift @@ -0,0 +1,5 @@ +// @archlint.module interface +// @archlint.domain overbroad.example +struct BroadDomain3 { + let value: String +} diff --git a/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain4.swift b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain4.swift new file mode 100644 index 0000000..cc6e28c --- /dev/null +++ b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain4.swift @@ -0,0 +1,5 @@ +// @archlint.module interface +// @archlint.domain overbroad.example +struct BroadDomain4 { + let value: String +} diff --git a/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain5.swift b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain5.swift new file mode 100644 index 0000000..b065c81 --- /dev/null +++ b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain5.swift @@ -0,0 +1,5 @@ +// @archlint.module interface +// @archlint.domain overbroad.example +struct BroadDomain5 { + let value: String +} diff --git a/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain6.swift b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain6.swift new file mode 100644 index 0000000..9ff80b7 --- /dev/null +++ b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain6.swift @@ -0,0 +1,5 @@ +// @archlint.module interface +// @archlint.domain overbroad.example +struct BroadDomain6 { + let value: String +} diff --git a/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain7.swift b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain7.swift new file mode 100644 index 0000000..bbe51ed --- /dev/null +++ b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain7.swift @@ -0,0 +1,5 @@ +// @archlint.module interface +// @archlint.domain overbroad.example +struct BroadDomain7 { + let value: String +} diff --git a/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain8.swift b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain8.swift new file mode 100644 index 0000000..a2ca675 --- /dev/null +++ b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain8.swift @@ -0,0 +1,5 @@ +// @archlint.module interface +// @archlint.domain overbroad.example +struct BroadDomain8 { + let value: String +} diff --git a/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain9.swift b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain9.swift new file mode 100644 index 0000000..c31cb9c --- /dev/null +++ b/swift/fixtures/interface-generic-domain/apps/ios/MailApp/Backend/BroadDomain9.swift @@ -0,0 +1,5 @@ +// @archlint.module interface +// @archlint.domain overbroad.example +struct BroadDomain9 { + let value: String +} diff --git a/swift/fixtures/interface-with-logic/apps/ios/MailApp/Backend/AddAccountRequest.swift b/swift/fixtures/interface-with-logic/apps/ios/MailApp/Backend/AddAccountRequest.swift new file mode 100644 index 0000000..e00bb2e --- /dev/null +++ b/swift/fixtures/interface-with-logic/apps/ios/MailApp/Backend/AddAccountRequest.swift @@ -0,0 +1,9 @@ +// @archlint.module interface +// @archlint.domain backend.http +struct AddAccountRequest { + let displayName: String + + var normalizedDisplayName: String { + displayName.trimmingCharacters(in: .whitespacesAndNewlines) + } +} diff --git a/swift/fixtures/local-core-name-only/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift b/swift/fixtures/local-core-name-only/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift new file mode 100644 index 0000000..eefe1d0 --- /dev/null +++ b/swift/fixtures/local-core-name-only/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift @@ -0,0 +1,8 @@ +// @archlint.module shell +// @archlint.domain backend.http +struct HTTPMailBackendClient { + func makePath() -> String { + let decidePath = "/v1/accounts" + return decidePath + } +} diff --git a/swift/fixtures/local-core-name-only/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift b/swift/fixtures/local-core-name-only/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift new file mode 100644 index 0000000..24fb00f --- /dev/null +++ b/swift/fixtures/local-core-name-only/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift @@ -0,0 +1,7 @@ +// @archlint.module core +// @archlint.domain backend.http +enum HTTPMailBackendDecider { + static func decidePath() -> String { + "/v1/accounts" + } +} diff --git a/swift/fixtures/local-core-name-only/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift b/swift/fixtures/local-core-name-only/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift new file mode 100644 index 0000000..666ca93 --- /dev/null +++ b/swift/fixtures/local-core-name-only/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift @@ -0,0 +1,11 @@ +// @archlint.module test +// @archlint.domain backend.http +import PropertyBased +import Testing + +@Test +func pathProperty() async { + await propertyCheck(input: Gen.int(in: 0...10)) { _ in + #expect(HTTPMailBackendDecider.decidePath() == "/v1/accounts") + } +} diff --git a/swift/fixtures/malformed-domain/apps/ios/MailApp/Backend/RandomDecider.swift b/swift/fixtures/malformed-domain/apps/ios/MailApp/Backend/RandomDecider.swift new file mode 100644 index 0000000..cbe3890 --- /dev/null +++ b/swift/fixtures/malformed-domain/apps/ios/MailApp/Backend/RandomDecider.swift @@ -0,0 +1,7 @@ +// @archlint.module core +// @archlint.domain Backend_HTTP +enum RandomDecider { + static func decide() -> Bool { + true + } +} diff --git a/swift/fixtures/metadata-after-source/apps/ios/MailApp/Backend/Random.swift b/swift/fixtures/metadata-after-source/apps/ios/MailApp/Backend/Random.swift new file mode 100644 index 0000000..7a750e0 --- /dev/null +++ b/swift/fixtures/metadata-after-source/apps/ios/MailApp/Backend/Random.swift @@ -0,0 +1,3 @@ +struct RandomValue {} +// @archlint.module value +// @archlint.domain random.value diff --git a/swift/fixtures/missing-core-reference/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift b/swift/fixtures/missing-core-reference/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift new file mode 100644 index 0000000..6d16d85 --- /dev/null +++ b/swift/fixtures/missing-core-reference/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift @@ -0,0 +1,9 @@ +// @archlint.module shell +// @archlint.domain backend.http +import Foundation + +final class HTTPMailBackendClient { + func makeRequest() -> URLRequest { + URLRequest(url: URL(string: "http://localhost")!) + } +} diff --git a/swift/fixtures/missing-core-reference/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift b/swift/fixtures/missing-core-reference/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift new file mode 100644 index 0000000..99133d0 --- /dev/null +++ b/swift/fixtures/missing-core-reference/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift @@ -0,0 +1,7 @@ +// @archlint.module core +// @archlint.domain backend.http +enum HTTPMailBackendDecider { + static func decidePath(_ shard: Int) -> String { + shard >= 0 ? "/v1/accounts" : "/v1/accounts" + } +} diff --git a/swift/fixtures/missing-core-reference/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift b/swift/fixtures/missing-core-reference/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift new file mode 100644 index 0000000..578ee82 --- /dev/null +++ b/swift/fixtures/missing-core-reference/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift @@ -0,0 +1,7 @@ +// @archlint.module test +// @archlint.domain backend.http +import PropertyBased + +func pathProperty() { + _ = PropertyBased.self +} diff --git a/swift/fixtures/missing-exempt-reason/apps/ios/MailApp/Backend/InstallationCredential.swift b/swift/fixtures/missing-exempt-reason/apps/ios/MailApp/Backend/InstallationCredential.swift new file mode 100644 index 0000000..e3e6f0a --- /dev/null +++ b/swift/fixtures/missing-exempt-reason/apps/ios/MailApp/Backend/InstallationCredential.swift @@ -0,0 +1,4 @@ +// @archlint.module exempt +struct InstallationCredential { + let accessToken: String? +} diff --git a/swift/fixtures/missing-metadata/apps/ios/MailApp/Backend/Random.swift b/swift/fixtures/missing-metadata/apps/ios/MailApp/Backend/Random.swift new file mode 100644 index 0000000..bd3db78 --- /dev/null +++ b/swift/fixtures/missing-metadata/apps/ios/MailApp/Backend/Random.swift @@ -0,0 +1 @@ +struct RandomValue {} diff --git a/swift/fixtures/missing-property/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift b/swift/fixtures/missing-property/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift new file mode 100644 index 0000000..24fb00f --- /dev/null +++ b/swift/fixtures/missing-property/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift @@ -0,0 +1,7 @@ +// @archlint.module core +// @archlint.domain backend.http +enum HTTPMailBackendDecider { + static func decidePath() -> String { + "/v1/accounts" + } +} diff --git a/swift/fixtures/missing-property/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift b/swift/fixtures/missing-property/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift new file mode 100644 index 0000000..67bd63c --- /dev/null +++ b/swift/fixtures/missing-property/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift @@ -0,0 +1,9 @@ +// @archlint.module test +// @archlint.domain backend.http +import XCTest + +final class HTTPMailBackendDeciderTests: XCTestCase { + func testPath() { + XCTAssertEqual(HTTPMailBackendDecider.decidePath(), "/v1/accounts") + } +} diff --git a/swift/fixtures/non-decider-core/apps/ios/MailApp/Backend/HTTPMailBackendCore.swift b/swift/fixtures/non-decider-core/apps/ios/MailApp/Backend/HTTPMailBackendCore.swift new file mode 100644 index 0000000..1efe782 --- /dev/null +++ b/swift/fixtures/non-decider-core/apps/ios/MailApp/Backend/HTTPMailBackendCore.swift @@ -0,0 +1,7 @@ +// @archlint.module core +// @archlint.domain backend.http +enum HTTPMailBackendCore { + static func decidePath() -> String { + "/v1/accounts" + } +} diff --git a/swift/fixtures/ordinary-array-property/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift b/swift/fixtures/ordinary-array-property/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift new file mode 100644 index 0000000..5dba971 --- /dev/null +++ b/swift/fixtures/ordinary-array-property/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift @@ -0,0 +1,7 @@ +// @archlint.module core +// @archlint.domain backend.http +enum HTTPMailBackendDecider { + static func decode(_ values: [Int]) -> Int { + values.count + } +} diff --git a/swift/fixtures/ordinary-array-property/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift b/swift/fixtures/ordinary-array-property/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift new file mode 100644 index 0000000..895a130 --- /dev/null +++ b/swift/fixtures/ordinary-array-property/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift @@ -0,0 +1,13 @@ +// @archlint.module test +// @archlint.domain backend.http +import PropertyBased +import Testing + +enum HTTPMailBackendDeciderPropertySuite { + @Test + static func decodeAcceptsGeneratedArraysProperty() async { + await propertyCheck(input: Gen.int(in: 0...10).array(of: 0...20)) { values in + #expect(HTTPMailBackendDecider.decode(values) == values.count) + } + } +} diff --git a/swift/fixtures/passing/apps/ios/MailApp/Backend/AddAccountRequest.swift b/swift/fixtures/passing/apps/ios/MailApp/Backend/AddAccountRequest.swift new file mode 100644 index 0000000..1b9e177 --- /dev/null +++ b/swift/fixtures/passing/apps/ios/MailApp/Backend/AddAccountRequest.swift @@ -0,0 +1,6 @@ +// @archlint.module interface +// @archlint.domain backend.http +struct AddAccountRequest: Equatable { + let displayName: String + let emailAddress: String +} diff --git a/swift/fixtures/passing/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift b/swift/fixtures/passing/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift new file mode 100644 index 0000000..bb34591 --- /dev/null +++ b/swift/fixtures/passing/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift @@ -0,0 +1,11 @@ +// @archlint.module shell +// @archlint.domain backend.http +import Foundation + +final class HTTPMailBackendClient { + func makeRequest() -> URLRequest { + let path = HTTPMailBackendDecider.decidePath(0) + let url = URL(string: "http://localhost" + path)! + return URLRequest(url: url) + } +} diff --git a/swift/fixtures/passing/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift b/swift/fixtures/passing/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift new file mode 100644 index 0000000..99133d0 --- /dev/null +++ b/swift/fixtures/passing/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift @@ -0,0 +1,7 @@ +// @archlint.module core +// @archlint.domain backend.http +enum HTTPMailBackendDecider { + static func decidePath(_ shard: Int) -> String { + shard >= 0 ? "/v1/accounts" : "/v1/accounts" + } +} diff --git a/swift/fixtures/passing/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift b/swift/fixtures/passing/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift new file mode 100644 index 0000000..9ea4acd --- /dev/null +++ b/swift/fixtures/passing/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift @@ -0,0 +1,18 @@ +// @archlint.module test +// @archlint.domain backend.http +import PropertyBased +import Testing +import XCTest + +final class HTTPMailBackendDeciderTests: XCTestCase { + func testPath() { + XCTAssertEqual(HTTPMailBackendDecider.decidePath(0), "/v1/accounts") + } +} + +@Test +func pathProperty() async { + await propertyCheck(input: Gen.int(in: 0...10)) { shard in + #expect(HTTPMailBackendDecider.decidePath(shard) == "/v1/accounts") + } +} diff --git a/swift/fixtures/production-module-in-test/apps/ios/MailAppTests/RandomDecider.swift b/swift/fixtures/production-module-in-test/apps/ios/MailAppTests/RandomDecider.swift new file mode 100644 index 0000000..5247c89 --- /dev/null +++ b/swift/fixtures/production-module-in-test/apps/ios/MailAppTests/RandomDecider.swift @@ -0,0 +1,7 @@ +// @archlint.module core +// @archlint.domain random.value +enum RandomDecider { + static func decide() -> Bool { + true + } +} diff --git a/swift/fixtures/property-import-only/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift b/swift/fixtures/property-import-only/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift new file mode 100644 index 0000000..24fb00f --- /dev/null +++ b/swift/fixtures/property-import-only/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift @@ -0,0 +1,7 @@ +// @archlint.module core +// @archlint.domain backend.http +enum HTTPMailBackendDecider { + static func decidePath() -> String { + "/v1/accounts" + } +} diff --git a/swift/fixtures/property-import-only/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift b/swift/fixtures/property-import-only/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift new file mode 100644 index 0000000..54c167b --- /dev/null +++ b/swift/fixtures/property-import-only/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift @@ -0,0 +1,10 @@ +// @archlint.module test +// @archlint.domain backend.http +import PropertyBased +import XCTest + +enum HTTPMailBackendDeciderPropertySuite { + static func testPath() { + _ = PropertyBased.self + } +} diff --git a/swift/fixtures/property-local-core-name-only/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift b/swift/fixtures/property-local-core-name-only/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift new file mode 100644 index 0000000..24fb00f --- /dev/null +++ b/swift/fixtures/property-local-core-name-only/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift @@ -0,0 +1,7 @@ +// @archlint.module core +// @archlint.domain backend.http +enum HTTPMailBackendDecider { + static func decidePath() -> String { + "/v1/accounts" + } +} diff --git a/swift/fixtures/property-local-core-name-only/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift b/swift/fixtures/property-local-core-name-only/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift new file mode 100644 index 0000000..1fbf339 --- /dev/null +++ b/swift/fixtures/property-local-core-name-only/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift @@ -0,0 +1,14 @@ +// @archlint.module test +// @archlint.domain backend.http +import PropertyBased +import Testing + +enum HTTPMailBackendDeciderPropertySuite { + @Test + static func pathProperty() async { + await propertyCheck(input: Gen.int(in: 0...10)) { value in + let decidePath = value + #expect(decidePath == value) + } + } +} diff --git a/swift/fixtures/property-name-only/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift b/swift/fixtures/property-name-only/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift new file mode 100644 index 0000000..24fb00f --- /dev/null +++ b/swift/fixtures/property-name-only/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift @@ -0,0 +1,7 @@ +// @archlint.module core +// @archlint.domain backend.http +enum HTTPMailBackendDecider { + static func decidePath() -> String { + "/v1/accounts" + } +} diff --git a/swift/fixtures/property-name-only/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift b/swift/fixtures/property-name-only/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift new file mode 100644 index 0000000..37f02a9 --- /dev/null +++ b/swift/fixtures/property-name-only/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift @@ -0,0 +1,8 @@ +// @archlint.module test +// @archlint.domain backend.http +import Testing + +@Test +func pathProperty() { + #expect(HTTPMailBackendDecider.decidePath() == "/v1/accounts") +} diff --git a/swift/fixtures/property-reachable-helper/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift b/swift/fixtures/property-reachable-helper/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift new file mode 100644 index 0000000..24fb00f --- /dev/null +++ b/swift/fixtures/property-reachable-helper/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift @@ -0,0 +1,7 @@ +// @archlint.module core +// @archlint.domain backend.http +enum HTTPMailBackendDecider { + static func decidePath() -> String { + "/v1/accounts" + } +} diff --git a/swift/fixtures/property-reachable-helper/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift b/swift/fixtures/property-reachable-helper/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift new file mode 100644 index 0000000..62b15a1 --- /dev/null +++ b/swift/fixtures/property-reachable-helper/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift @@ -0,0 +1,17 @@ +// @archlint.module test +// @archlint.domain backend.http +import PropertyBased +import Testing + +enum HTTPMailBackendDeciderPropertySuite { + @Test + static func pathProperty() async { + await propertyCheck(input: Gen.int(in: 0...10)) { shard in + #expect(helperPath(shard) == "/v1/accounts") + } + } + + static func helperPath(_ shard: Int) -> String { + HTTPMailBackendDecider.decidePath(shard) + } +} diff --git a/swift/fixtures/property-unreachable-helper/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift b/swift/fixtures/property-unreachable-helper/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift new file mode 100644 index 0000000..24fb00f --- /dev/null +++ b/swift/fixtures/property-unreachable-helper/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift @@ -0,0 +1,7 @@ +// @archlint.module core +// @archlint.domain backend.http +enum HTTPMailBackendDecider { + static func decidePath() -> String { + "/v1/accounts" + } +} diff --git a/swift/fixtures/property-unreachable-helper/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift b/swift/fixtures/property-unreachable-helper/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift new file mode 100644 index 0000000..96f7ef9 --- /dev/null +++ b/swift/fixtures/property-unreachable-helper/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift @@ -0,0 +1,17 @@ +// @archlint.module test +// @archlint.domain backend.http +import PropertyBased +import Testing + +enum HTTPMailBackendDeciderPropertySuite { + @Test + static func pathProperty() async { + await propertyCheck(input: Gen.int(in: 0...10)) { value in + #expect(value == value) + } + } + + static func helperPath() -> String { + HTTPMailBackendDecider.decidePath() + } +} diff --git a/swift/fixtures/published-state-outside-state/apps/ios/MailApp/Backend/FakeMailBackendClient.swift b/swift/fixtures/published-state-outside-state/apps/ios/MailApp/Backend/FakeMailBackendClient.swift new file mode 100644 index 0000000..23f4a77 --- /dev/null +++ b/swift/fixtures/published-state-outside-state/apps/ios/MailApp/Backend/FakeMailBackendClient.swift @@ -0,0 +1,7 @@ +// @archlint.module shell +// @archlint.domain backend.http +import Combine + +final class FakeMailBackendClient { + @Published var accounts: [String] = [] +} diff --git a/swift/fixtures/pure-glue-exemption/apps/ios/MailApp/Features/Mailboxes/MailboxRoute.swift b/swift/fixtures/pure-glue-exemption/apps/ios/MailApp/Features/Mailboxes/MailboxRoute.swift new file mode 100644 index 0000000..aaab6d7 --- /dev/null +++ b/swift/fixtures/pure-glue-exemption/apps/ios/MailApp/Features/Mailboxes/MailboxRoute.swift @@ -0,0 +1,7 @@ +// @archlint.module exempt +// @archlint.exempt-reason pure-glue + +struct MailboxRoute { + let accountID: String + let mailboxID: String +} diff --git a/swift/fixtures/shared-state-comment-only/apps/ios/MailApp/Backend/FakeMailBackendClient.swift b/swift/fixtures/shared-state-comment-only/apps/ios/MailApp/Backend/FakeMailBackendClient.swift new file mode 100644 index 0000000..c1871d0 --- /dev/null +++ b/swift/fixtures/shared-state-comment-only/apps/ios/MailApp/Backend/FakeMailBackendClient.swift @@ -0,0 +1,9 @@ +// @archlint.module interface +// @archlint.domain backend.http +// actor CommentOnly { private var accounts: [String] = [] } +// @Published var commentOnly: String = "" +// DatabaseQueue and UserDefaults.standard are mentioned in prose only. + +struct FakeMailBackendClient { + let endpoint: String +} diff --git a/swift/fixtures/shared-state-outside-state/apps/ios/MailApp/Backend/FakeMailBackendClient.swift b/swift/fixtures/shared-state-outside-state/apps/ios/MailApp/Backend/FakeMailBackendClient.swift new file mode 100644 index 0000000..7d0ed65 --- /dev/null +++ b/swift/fixtures/shared-state-outside-state/apps/ios/MailApp/Backend/FakeMailBackendClient.swift @@ -0,0 +1,11 @@ +// @archlint.module shell +// @archlint.domain backend.http +import Foundation + +actor FakeMailBackendClient { + private var accounts: [String] = [] + + func add(_ account: String) { + accounts.append(account) + } +} diff --git a/swift/fixtures/shared-state-without-state-test/apps/ios/MailApp/Backend/FakeMailBackendClient.swift b/swift/fixtures/shared-state-without-state-test/apps/ios/MailApp/Backend/FakeMailBackendClient.swift new file mode 100644 index 0000000..92bff6a --- /dev/null +++ b/swift/fixtures/shared-state-without-state-test/apps/ios/MailApp/Backend/FakeMailBackendClient.swift @@ -0,0 +1,11 @@ +// @archlint.module state +// @archlint.domain backend.http +import Foundation + +actor FakeMailBackendClient { + private var accounts: [String] = [] + + func add(_ account: String) { + accounts.append(account) + } +} diff --git a/swift/fixtures/state-test-operation-sequence-refs/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift b/swift/fixtures/state-test-operation-sequence-refs/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift new file mode 100644 index 0000000..fd80fc9 --- /dev/null +++ b/swift/fixtures/state-test-operation-sequence-refs/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift @@ -0,0 +1,19 @@ +// @archlint.module stateTest +// @archlint.domain backend.sqlite +import PropertyBased +import Testing + +enum SQLiteMailSyncStateDeciderPropertySuite { + @Test + static func roundTripOperationSequencesProperty() async { + await propertyCheck(input: Gen.int(in: 0...10).array(of: 0...20)) { values in + for value in values { + #expect(helper(value) == value) + } + } + } + + static func helper(_ value: Int) -> Int { + SQLiteMailSyncStateDecider.reduce(value) + } +} diff --git a/swift/fixtures/state-test-operation-sequences-without-core-refs/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift b/swift/fixtures/state-test-operation-sequences-without-core-refs/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift new file mode 100644 index 0000000..b8999c4 --- /dev/null +++ b/swift/fixtures/state-test-operation-sequences-without-core-refs/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift @@ -0,0 +1,7 @@ +// @archlint.module core +// @archlint.domain backend.sqlite +enum SQLiteMailSyncStateDecider { + static func reduce(_ value: Int) -> Int { + value + } +} diff --git a/swift/fixtures/state-test-operation-sequences-without-core-refs/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift b/swift/fixtures/state-test-operation-sequences-without-core-refs/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift new file mode 100644 index 0000000..754a423 --- /dev/null +++ b/swift/fixtures/state-test-operation-sequences-without-core-refs/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift @@ -0,0 +1,26 @@ +// @archlint.module stateTest +// @archlint.domain backend.sqlite +import PropertyBased +import Testing + +enum SQLiteMailSyncStateDeciderPropertySuite { + @Test + static func reduceProperty() async { + await propertyCheck(input: Gen.int(in: 0...10)) { value in + #expect(SQLiteMailSyncStateDecider.reduce(value) == value) + } + } + + @Test + static func unrelatedOperationSequencesProperty() async { + await propertyCheck(input: Gen.int(in: 0...10).array(of: 0...20)) { values in + for value in values { + #expect(helper(value) == value) + } + } + } + + static func helper(_ value: Int) -> Int { + value + } +} diff --git a/swift/fixtures/state-test-operation-sequences-without-refs/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift b/swift/fixtures/state-test-operation-sequences-without-refs/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift new file mode 100644 index 0000000..1d10667 --- /dev/null +++ b/swift/fixtures/state-test-operation-sequences-without-refs/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift @@ -0,0 +1,15 @@ +// @archlint.module stateTest +// @archlint.domain backend.sqlite +import PropertyBased +import Testing + +enum SQLiteMailSyncStateDeciderPropertySuite { + @Test + static func roundTripOperationSequencesProperty() async { + await propertyCheck(input: Gen.int(in: 0...10).array(of: 0...20)) { values in + for value in values { + #expect(value == value) + } + } + } +} diff --git a/swift/fixtures/state-test-without-operation-sequences/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift b/swift/fixtures/state-test-without-operation-sequences/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift new file mode 100644 index 0000000..ddf5146 --- /dev/null +++ b/swift/fixtures/state-test-without-operation-sequences/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift @@ -0,0 +1,13 @@ +// @archlint.module stateTest +// @archlint.domain backend.sqlite +import PropertyBased +import Testing + +enum SQLiteMailSyncStateDeciderPropertySuite { + @Test + static func roundTripOperationSequencesProperty() async { + await propertyCheck(input: Gen.int(in: 0...10)) { value in + #expect(value == value) + } + } +} diff --git a/swift/fixtures/state-test-without-state-module/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift b/swift/fixtures/state-test-without-state-module/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift new file mode 100644 index 0000000..b8999c4 --- /dev/null +++ b/swift/fixtures/state-test-without-state-module/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift @@ -0,0 +1,7 @@ +// @archlint.module core +// @archlint.domain backend.sqlite +enum SQLiteMailSyncStateDecider { + static func reduce(_ value: Int) -> Int { + value + } +} diff --git a/swift/fixtures/state-test-without-state-module/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift b/swift/fixtures/state-test-without-state-module/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift new file mode 100644 index 0000000..d15fec9 --- /dev/null +++ b/swift/fixtures/state-test-without-state-module/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift @@ -0,0 +1,15 @@ +// @archlint.module stateTest +// @archlint.domain backend.sqlite +import PropertyBased +import Testing + +enum SQLiteMailSyncStateDeciderPropertySuite { + @Test + static func reduceOperationSequencesProperty() async { + await propertyCheck(input: Gen.int(in: 0...10).array(of: 0...20)) { values in + for value in values { + #expect(SQLiteMailSyncStateDecider.reduce(value) == value) + } + } + } +} diff --git a/swift/fixtures/state-unrelated-to-operation-sequences/apps/ios/MailApp/Backend/FakeMailBackendClient.swift b/swift/fixtures/state-unrelated-to-operation-sequences/apps/ios/MailApp/Backend/FakeMailBackendClient.swift new file mode 100644 index 0000000..92bff6a --- /dev/null +++ b/swift/fixtures/state-unrelated-to-operation-sequences/apps/ios/MailApp/Backend/FakeMailBackendClient.swift @@ -0,0 +1,11 @@ +// @archlint.module state +// @archlint.domain backend.http +import Foundation + +actor FakeMailBackendClient { + private var accounts: [String] = [] + + func add(_ account: String) { + accounts.append(account) + } +} diff --git a/swift/fixtures/state-unrelated-to-operation-sequences/apps/ios/MailAppTests/HTTPMailBackendStatePropertySuite.swift b/swift/fixtures/state-unrelated-to-operation-sequences/apps/ios/MailAppTests/HTTPMailBackendStatePropertySuite.swift new file mode 100644 index 0000000..c07a21f --- /dev/null +++ b/swift/fixtures/state-unrelated-to-operation-sequences/apps/ios/MailAppTests/HTTPMailBackendStatePropertySuite.swift @@ -0,0 +1,15 @@ +// @archlint.module stateTest +// @archlint.domain backend.http +import PropertyBased +import Testing + +enum HTTPMailBackendStatePropertySuite { + @Test + static func unrelatedOperationSequencesProperty() async { + await propertyCheck(input: Gen.int(in: 0...10).array(of: 0...20)) { values in + for value in values { + #expect(OtherDecider.reduce(value) == value) + } + } + } +} diff --git a/swift/fixtures/state-without-stateful-apis/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift b/swift/fixtures/state-without-stateful-apis/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift new file mode 100644 index 0000000..b8999c4 --- /dev/null +++ b/swift/fixtures/state-without-stateful-apis/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift @@ -0,0 +1,7 @@ +// @archlint.module core +// @archlint.domain backend.sqlite +enum SQLiteMailSyncStateDecider { + static func reduce(_ value: Int) -> Int { + value + } +} diff --git a/swift/fixtures/state-without-stateful-apis/apps/ios/MailApp/Backend/SQLiteMailSyncStateStore.swift b/swift/fixtures/state-without-stateful-apis/apps/ios/MailApp/Backend/SQLiteMailSyncStateStore.swift new file mode 100644 index 0000000..17cbdec --- /dev/null +++ b/swift/fixtures/state-without-stateful-apis/apps/ios/MailApp/Backend/SQLiteMailSyncStateStore.swift @@ -0,0 +1,7 @@ +// @archlint.module state +// @archlint.domain backend.sqlite +struct SQLiteMailSyncStateStore { + func snapshot(_ value: Int) -> Int { + SQLiteMailSyncStateDecider.reduce(value) + } +} diff --git a/swift/fixtures/state-without-stateful-apis/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift b/swift/fixtures/state-without-stateful-apis/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift new file mode 100644 index 0000000..d15fec9 --- /dev/null +++ b/swift/fixtures/state-without-stateful-apis/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift @@ -0,0 +1,15 @@ +// @archlint.module stateTest +// @archlint.domain backend.sqlite +import PropertyBased +import Testing + +enum SQLiteMailSyncStateDeciderPropertySuite { + @Test + static func reduceOperationSequencesProperty() async { + await propertyCheck(input: Gen.int(in: 0...10).array(of: 0...20)) { values in + for value in values { + #expect(SQLiteMailSyncStateDecider.reduce(value) == value) + } + } + } +} diff --git a/swift/fixtures/static-data-exemption/apps/ios/MailApp/Domain/MailStaticData.swift b/swift/fixtures/static-data-exemption/apps/ios/MailApp/Domain/MailStaticData.swift new file mode 100644 index 0000000..d60a719 --- /dev/null +++ b/swift/fixtures/static-data-exemption/apps/ios/MailApp/Domain/MailStaticData.swift @@ -0,0 +1,5 @@ +// @archlint.module exempt +// @archlint.exempt-reason static-data +enum MailStaticData { + static let account = "demo" +} diff --git a/swift/fixtures/static-property-decision-surface/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift b/swift/fixtures/static-property-decision-surface/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift new file mode 100644 index 0000000..e859067 --- /dev/null +++ b/swift/fixtures/static-property-decision-surface/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift @@ -0,0 +1,9 @@ +// @archlint.module core +// @archlint.domain backend.sqlite +enum SQLiteMailSyncStateDecider { + static let createTablesSQL: String = "CREATE TABLE messages(id TEXT)" + + static func loadedState(_ shard: Int) -> String { + shard >= 0 ? "loaded" : "loaded" + } +} diff --git a/swift/fixtures/static-property-decision-surface/apps/ios/MailApp/Backend/SQLiteMailSyncStateSchema.swift b/swift/fixtures/static-property-decision-surface/apps/ios/MailApp/Backend/SQLiteMailSyncStateSchema.swift new file mode 100644 index 0000000..c8b8042 --- /dev/null +++ b/swift/fixtures/static-property-decision-surface/apps/ios/MailApp/Backend/SQLiteMailSyncStateSchema.swift @@ -0,0 +1,8 @@ +// @archlint.module shell +// @archlint.domain backend.sqlite +import Foundation + +struct SQLiteMailSyncStateSchema { + let request = URLRequest(url: URL(string: "http://localhost")!) + let sql: String = SQLiteMailSyncStateDecider.createTablesSQL +} diff --git a/swift/fixtures/static-property-decision-surface/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderTests.swift b/swift/fixtures/static-property-decision-surface/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderTests.swift new file mode 100644 index 0000000..d27ffbf --- /dev/null +++ b/swift/fixtures/static-property-decision-surface/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderTests.swift @@ -0,0 +1,11 @@ +// @archlint.module test +// @archlint.domain backend.sqlite +import PropertyBased +import Testing + +@Test +func schemaProperty() async { + await propertyCheck(input: Gen.int(in: 0...10)) { shard in + #expect(SQLiteMailSyncStateDecider.loadedState(shard) == "loaded") + } +} diff --git a/swift/fixtures/test-module-in-production/apps/ios/MailApp/Backend/RandomTests.swift b/swift/fixtures/test-module-in-production/apps/ios/MailApp/Backend/RandomTests.swift new file mode 100644 index 0000000..4f0bcaa --- /dev/null +++ b/swift/fixtures/test-module-in-production/apps/ios/MailApp/Backend/RandomTests.swift @@ -0,0 +1,3 @@ +// @archlint.module test +// @archlint.domain random.value +struct RandomTests {} diff --git a/swift/fixtures/test-support-exemption/apps/ios/MailAppTests/FailingMailSyncStateStore.swift b/swift/fixtures/test-support-exemption/apps/ios/MailAppTests/FailingMailSyncStateStore.swift new file mode 100644 index 0000000..bbb6489 --- /dev/null +++ b/swift/fixtures/test-support-exemption/apps/ios/MailAppTests/FailingMailSyncStateStore.swift @@ -0,0 +1,5 @@ +// @archlint.module exempt +// @archlint.exempt-reason test-support +struct FailingMailSyncStateStore { + let reason: String +} diff --git a/swift/fixtures/type-only-decider/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecision.swift b/swift/fixtures/type-only-decider/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecision.swift new file mode 100644 index 0000000..09289bf --- /dev/null +++ b/swift/fixtures/type-only-decider/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecision.swift @@ -0,0 +1,3 @@ +// @archlint.module core +// @archlint.domain backend.http +struct SQLiteMailSyncStateDecision {} diff --git a/swift/fixtures/unknown-exempt-reason/apps/ios/MailApp/Backend/InstallationCredential.swift b/swift/fixtures/unknown-exempt-reason/apps/ios/MailApp/Backend/InstallationCredential.swift new file mode 100644 index 0000000..050fa39 --- /dev/null +++ b/swift/fixtures/unknown-exempt-reason/apps/ios/MailApp/Backend/InstallationCredential.swift @@ -0,0 +1,5 @@ +// @archlint.module exempt +// @archlint.exempt-reason miscellaneous +struct InstallationCredential { + let accessToken: String? +} diff --git a/swift/fixtures/unrelated-property-test/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift b/swift/fixtures/unrelated-property-test/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift new file mode 100644 index 0000000..24fb00f --- /dev/null +++ b/swift/fixtures/unrelated-property-test/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift @@ -0,0 +1,7 @@ +// @archlint.module core +// @archlint.domain backend.http +enum HTTPMailBackendDecider { + static func decidePath() -> String { + "/v1/accounts" + } +} diff --git a/swift/fixtures/unrelated-property-test/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift b/swift/fixtures/unrelated-property-test/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift new file mode 100644 index 0000000..0e6da8b --- /dev/null +++ b/swift/fixtures/unrelated-property-test/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift @@ -0,0 +1,13 @@ +// @archlint.module test +// @archlint.domain backend.http +import PropertyBased +import Testing + +enum HTTPMailBackendDeciderPropertySuite { + @Test + static func unrelatedProperty() async { + await propertyCheck(input: Gen.int(in: 0...10)) { value in + #expect(value == value) + } + } +} diff --git a/swift/fixtures/user-defaults-state-outside-state/apps/ios/MailApp/Backend/FakeMailBackendClient.swift b/swift/fixtures/user-defaults-state-outside-state/apps/ios/MailApp/Backend/FakeMailBackendClient.swift new file mode 100644 index 0000000..8df13ec --- /dev/null +++ b/swift/fixtures/user-defaults-state-outside-state/apps/ios/MailApp/Backend/FakeMailBackendClient.swift @@ -0,0 +1,9 @@ +// @archlint.module shell +// @archlint.domain backend.http +import Foundation + +struct FakeMailBackendClient { + func load() -> String { + UserDefaults.standard.string(forKey: "lastAccount") ?? "" + } +} diff --git a/swift/fixtures/value-with-branching-computed-property/apps/ios/MailApp/Backend/AddAccountRequest.swift b/swift/fixtures/value-with-branching-computed-property/apps/ios/MailApp/Backend/AddAccountRequest.swift new file mode 100644 index 0000000..90e59e0 --- /dev/null +++ b/swift/fixtures/value-with-branching-computed-property/apps/ios/MailApp/Backend/AddAccountRequest.swift @@ -0,0 +1,12 @@ +// @archlint.module value +// @archlint.domain backend.http +struct AddAccountRequest { + let displayName: String + + var normalizedDisplayName: String { + if displayName.isEmpty { + return "Mailbox" + } + return displayName + } +} diff --git a/swift/fixtures/value-with-callable-decision/apps/ios/MailApp/Backend/AddAccountRequest.swift b/swift/fixtures/value-with-callable-decision/apps/ios/MailApp/Backend/AddAccountRequest.swift new file mode 100644 index 0000000..7037658 --- /dev/null +++ b/swift/fixtures/value-with-callable-decision/apps/ios/MailApp/Backend/AddAccountRequest.swift @@ -0,0 +1,9 @@ +// @archlint.module value +// @archlint.domain backend.http +struct AddAccountRequest { + let displayName: String + + func normalizedDisplayName() -> String { + displayName.trimmingCharacters(in: .whitespacesAndNewlines) + } +} diff --git a/swift/fixtures/value-with-computed-property/apps/ios/MailApp/Backend/AddAccountRequest.swift b/swift/fixtures/value-with-computed-property/apps/ios/MailApp/Backend/AddAccountRequest.swift new file mode 100644 index 0000000..f528e20 --- /dev/null +++ b/swift/fixtures/value-with-computed-property/apps/ios/MailApp/Backend/AddAccountRequest.swift @@ -0,0 +1,9 @@ +// @archlint.module value +// @archlint.domain backend.http +struct AddAccountRequest { + let displayName: String + + var normalizedDisplayName: String { + displayName.trimmingCharacters(in: .whitespacesAndNewlines) + } +} diff --git a/swift/fixtures/wrong-domain-test/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift b/swift/fixtures/wrong-domain-test/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift new file mode 100644 index 0000000..24fb00f --- /dev/null +++ b/swift/fixtures/wrong-domain-test/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift @@ -0,0 +1,7 @@ +// @archlint.module core +// @archlint.domain backend.http +enum HTTPMailBackendDecider { + static func decidePath() -> String { + "/v1/accounts" + } +} diff --git a/swift/fixtures/wrong-domain-test/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift b/swift/fixtures/wrong-domain-test/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift new file mode 100644 index 0000000..7ae67ee --- /dev/null +++ b/swift/fixtures/wrong-domain-test/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift @@ -0,0 +1,7 @@ +// @archlint.module test +// @archlint.domain mail.sync +import PropertyBased + +func pathProperty() { + _ = PropertyBased.self +} diff --git a/swift/fixtures/wrong-test-module-type/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift b/swift/fixtures/wrong-test-module-type/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift new file mode 100644 index 0000000..24fb00f --- /dev/null +++ b/swift/fixtures/wrong-test-module-type/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift @@ -0,0 +1,7 @@ +// @archlint.module core +// @archlint.domain backend.http +enum HTTPMailBackendDecider { + static func decidePath() -> String { + "/v1/accounts" + } +} diff --git a/swift/fixtures/wrong-test-module-type/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift b/swift/fixtures/wrong-test-module-type/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift new file mode 100644 index 0000000..c4c36aa --- /dev/null +++ b/swift/fixtures/wrong-test-module-type/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift @@ -0,0 +1,7 @@ +// @archlint.module interface +// @archlint.domain backend.http +import PropertyBased + +func pathProperty() { + _ = PropertyBased.self +} diff --git a/swift/test.sh b/swift/test.sh index 1dd7f0d..e43493b 100644 --- a/swift/test.sh +++ b/swift/test.sh @@ -5,6 +5,7 @@ package_root="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" repo_root="$(CDPATH= cd -- "$package_root/.." && pwd)" tmp_root="$(mktemp -d)" trap 'rm -rf "$tmp_root"' EXIT INT TERM +export UV_CACHE_DIR="${UV_CACHE_DIR:-$tmp_root/uv-cache}" ARCHLINT_ROOT="$repo_root" . "$repo_root/test/lib.sh" @@ -20,74 +21,12 @@ run_adapter() { new_fixture() { name="$1" fixture="$tmp_root/$name" - mkdir -p "$fixture/apps/ios/MailApp/Backend" "$fixture/apps/ios/MailAppTests" - cat > "$fixture/apps/ios/project.yml" <<'EOF' -targets: - MailApp: - type: application - sources: - - MailApp - MailAppTests: - type: bundle.unit-test - sources: - - MailAppTests - dependencies: - - target: MailApp -EOF + copy_fixture "$package_root/fixtures/_shared" "$fixture" + copy_fixture "$package_root/fixtures/$name" "$fixture" printf '%s\n' "$fixture" } passing_fixture="$(new_fixture passing)" -cat > "$passing_fixture/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift" <<'EOF' -// @archlint.module core -// @archlint.domain backend.http -enum HTTPMailBackendDecider { - static func decidePath(_ shard: Int) -> String { - shard >= 0 ? "/v1/accounts" : "/v1/accounts" - } -} -EOF -cat > "$passing_fixture/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift" <<'EOF' -// @archlint.module test -// @archlint.domain backend.http -import PropertyBased -import Testing -import XCTest - -final class HTTPMailBackendDeciderTests: XCTestCase { - func testPath() { - XCTAssertEqual(HTTPMailBackendDecider.decidePath(0), "/v1/accounts") - } -} - -@Test -func pathProperty() async { - await propertyCheck(input: Gen.int(in: 0...10)) { shard in - #expect(HTTPMailBackendDecider.decidePath(shard) == "/v1/accounts") - } -} -EOF -cat > "$passing_fixture/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift" <<'EOF' -// @archlint.module shell -// @archlint.domain backend.http -import Foundation - -final class HTTPMailBackendClient { - func makeRequest() -> URLRequest { - let path = HTTPMailBackendDecider.decidePath(0) - let url = URL(string: "http://localhost" + path)! - return URLRequest(url: url) - } -} -EOF -cat > "$passing_fixture/apps/ios/MailApp/Backend/AddAccountRequest.swift" <<'EOF' -// @archlint.module interface -// @archlint.domain backend.http -struct AddAccountRequest: Equatable { - let displayName: String - let emailAddress: String -} -EOF assert_passes "$passing_fixture" run_adapter "$passing_fixture" > "$tmp_root/passing-facts.json" @@ -105,45 +44,6 @@ assert core["qualifiedReferences"] == [], core PY cross_file_bare_reference_fixture="$(new_fixture cross-file-bare-reference)" -cat > "$cross_file_bare_reference_fixture/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift" <<'EOF' -// @archlint.module core -// @archlint.domain backend.http -enum HTTPMailBackendDecider { - static func decidePath(_ shard: Int) -> String { - shard >= 0 ? "/v1/accounts" : "/v1/accounts" - } -} -EOF -cat > "$cross_file_bare_reference_fixture/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift" <<'EOF' -// @archlint.module test -// @archlint.domain backend.http -import PropertyBased -import Testing - -@Test -func pathProperty() async { - await propertyCheck(input: Gen.int(in: 0...10)) { shard in - #expect(HTTPMailBackendDecider.decidePath(shard) == "/v1/accounts") - } -} -EOF -cat > "$cross_file_bare_reference_fixture/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift" <<'EOF' -// @archlint.module shell -// @archlint.domain backend.http -import Foundation - -func decidePathForRequest(_ shard: Int) -> String { - HTTPMailBackendDecider.decidePath(shard) -} - -final class HTTPMailBackendClient { - func makeRequest() -> URLRequest { - let path = decidePathForRequest(0) - let url = URL(string: "http://localhost" + path)! - return URLRequest(url: url) - } -} -EOF run_adapter "$cross_file_bare_reference_fixture" > "$tmp_root/cross-file-bare-reference-facts.json" assert_facts "$tmp_root/cross-file-bare-reference-facts.json" <<'PY' import json @@ -157,723 +57,147 @@ assert "HTTPMailBackendDecider.decidePathForRequest" not in shell["qualifiedRefe PY missing_core_reference_fixture="$(new_fixture missing-core-reference)" -cat > "$missing_core_reference_fixture/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift" <<'EOF' -// @archlint.module core -// @archlint.domain backend.http -enum HTTPMailBackendDecider { - static func decidePath(_ shard: Int) -> String { - shard >= 0 ? "/v1/accounts" : "/v1/accounts" - } -} -EOF -cat > "$missing_core_reference_fixture/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift" <<'EOF' -// @archlint.module test -// @archlint.domain backend.http -import PropertyBased - -func pathProperty() { - _ = PropertyBased.self -} -EOF -cat > "$missing_core_reference_fixture/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift" <<'EOF' -// @archlint.module shell -// @archlint.domain backend.http -import Foundation - -final class HTTPMailBackendClient { - func makeRequest() -> URLRequest { - URLRequest(url: URL(string: "http://localhost")!) - } -} -EOF assert_fails_with "$missing_core_reference_fixture" \ "shell module must reference a core API in the same @archlint.domain" local_core_name_only_fixture="$(new_fixture local-core-name-only)" -cat > "$local_core_name_only_fixture/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift" <<'EOF' -// @archlint.module core -// @archlint.domain backend.http -enum HTTPMailBackendDecider { - static func decidePath() -> String { - "/v1/accounts" - } -} -EOF -cat > "$local_core_name_only_fixture/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift" <<'EOF' -// @archlint.module test -// @archlint.domain backend.http -import PropertyBased -import Testing - -@Test -func pathProperty() async { - await propertyCheck(input: Gen.int(in: 0...10)) { _ in - #expect(HTTPMailBackendDecider.decidePath() == "/v1/accounts") - } -} -EOF -cat > "$local_core_name_only_fixture/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift" <<'EOF' -// @archlint.module shell -// @archlint.domain backend.http -struct HTTPMailBackendClient { - func makePath() -> String { - let decidePath = "/v1/accounts" - return decidePath - } -} -EOF assert_fails_with "$local_core_name_only_fixture" \ "shell module must reference a core API in the same @archlint.domain" static_property_decision_surface_fixture="$(new_fixture static-property-decision-surface)" -cat > "$static_property_decision_surface_fixture/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift" <<'EOF' -// @archlint.module core -// @archlint.domain backend.sqlite -enum SQLiteMailSyncStateDecider { - static let createTablesSQL: String = "CREATE TABLE messages(id TEXT)" - - static func loadedState(_ shard: Int) -> String { - shard >= 0 ? "loaded" : "loaded" - } -} -EOF -cat > "$static_property_decision_surface_fixture/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderTests.swift" <<'EOF' -// @archlint.module test -// @archlint.domain backend.sqlite -import PropertyBased -import Testing - -@Test -func schemaProperty() async { - await propertyCheck(input: Gen.int(in: 0...10)) { shard in - #expect(SQLiteMailSyncStateDecider.loadedState(shard) == "loaded") - } -} -EOF -cat > "$static_property_decision_surface_fixture/apps/ios/MailApp/Backend/SQLiteMailSyncStateSchema.swift" <<'EOF' -// @archlint.module shell -// @archlint.domain backend.sqlite -import Foundation - -struct SQLiteMailSyncStateSchema { - let request = URLRequest(url: URL(string: "http://localhost")!) - let sql: String = SQLiteMailSyncStateDecider.createTablesSQL -} -EOF assert_passes "$static_property_decision_surface_fixture" closure_fixture="$(new_fixture closure)" -cat > "$closure_fixture/apps/ios/MailApp/Backend/ClosureDecider.swift" <<'EOF' -// @archlint.module core -// @archlint.domain backend.closure -enum ClosureDecider { - static func decideA(_ shard: Int) -> Bool { - shard >= 0 - } - - static func decideB(_ shard: Int) -> Bool { - shard < 0 - } - - static func decideC(_ shard: Int) -> Bool { - shard == 0 - } -} -EOF -cat > "$closure_fixture/apps/ios/MailAppTests/ClosureDeciderTests.swift" <<'EOF' -// @archlint.module test -// @archlint.domain backend.closure -import PropertyBased -import Testing - -func helper(_ shard: Int) -> Bool { - ClosureDecider.decideC(shard) -} - -@Test -func closureProperty() async { - await propertyCheck( - input: Gen.int(in: 0...10).map { shard in - _ = ClosureDecider.decideB(shard) - return shard - } - ) { shard in - #expect(ClosureDecider.decideA(shard) || helper(shard) || shard >= 0) - } -} -EOF -cat > "$closure_fixture/apps/ios/MailApp/Backend/ClosureClient.swift" <<'EOF' -// @archlint.module shell -// @archlint.domain backend.closure -import Foundation - -struct ClosureClient { - func handle() -> URLRequest { - let path = ClosureDecider.decideA(1) ? "/v1/accounts" : "/v1/accounts" - let url = URL(string: "http://localhost" + path)! - return URLRequest(url: url) - } -} -EOF assert_passes "$closure_fixture" arbitrary_core_type_reference_fixture="$(new_fixture arbitrary-core-type-reference)" -cat > "$arbitrary_core_type_reference_fixture/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift" <<'EOF' -// @archlint.module core -// @archlint.domain backend.http -struct CoreVocabulary {} - -enum HTTPMailBackendDecider { - static func decidePath() -> String { - "/v1/accounts" - } -} -EOF -cat > "$arbitrary_core_type_reference_fixture/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift" <<'EOF' -// @archlint.module test -// @archlint.domain backend.http -import PropertyBased -import Testing - -@Test -func pathProperty() async { - await propertyCheck(input: Gen.int(in: 0...10)) { _ in - #expect(HTTPMailBackendDecider.decidePath() == "/v1/accounts") - } -} -EOF -cat > "$arbitrary_core_type_reference_fixture/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift" <<'EOF' -// @archlint.module shell -// @archlint.domain backend.http -struct HTTPMailBackendClient { - func handle(_ value: CoreVocabulary) { - _ = value - } -} -EOF assert_fails_with "$arbitrary_core_type_reference_fixture" \ "shell module must reference a core API in the same @archlint.domain" missing_metadata_fixture="$(new_fixture missing-metadata)" -cat > "$missing_metadata_fixture/apps/ios/MailApp/Backend/Random.swift" <<'EOF' -struct RandomValue {} -EOF assert_fails_with "$missing_metadata_fixture" "module must declare @archlint.module" metadata_after_source_fixture="$(new_fixture metadata-after-source)" -cat > "$metadata_after_source_fixture/apps/ios/MailApp/Backend/Random.swift" <<'EOF' -struct RandomValue {} -// @archlint.module value -// @archlint.domain random.value -EOF assert_fails_with "$metadata_after_source_fixture" "module must declare @archlint.module" duplicate_module_metadata_fixture="$(new_fixture duplicate-module-metadata)" -cat > "$duplicate_module_metadata_fixture/apps/ios/MailApp/Backend/Random.swift" <<'EOF' -// @archlint.module core -// @archlint.module value -// @archlint.domain random.value -struct RandomValue {} -EOF assert_fails_with "$duplicate_module_metadata_fixture" "module must declare @archlint.module" duplicate_domain_metadata_fixture="$(new_fixture duplicate-domain-metadata)" -cat > "$duplicate_domain_metadata_fixture/apps/ios/MailApp/Backend/Random.swift" <<'EOF' -// @archlint.module value -// @archlint.domain random.value -// @archlint.domain random.other -struct RandomValue {} -EOF assert_fails_with "$duplicate_domain_metadata_fixture" "module must declare @archlint.domain" duplicate_exempt_reason_metadata_fixture="$(new_fixture duplicate-exempt-reason-metadata)" -cat > "$duplicate_exempt_reason_metadata_fixture/apps/ios/MailApp/Backend/MailPrototypeData.swift" <<'EOF' -// @archlint.module exempt -// @archlint.exempt-reason static-data -// @archlint.exempt-reason test-support -enum MailPrototypeData { - static let account = "demo" -} -EOF assert_fails_with "$duplicate_exempt_reason_metadata_fixture" \ "exempt module must declare @archlint.exempt-reason" test_module_in_production_fixture="$(new_fixture test-module-in-production)" -cat > "$test_module_in_production_fixture/apps/ios/MailApp/Backend/RandomTests.swift" <<'EOF' -// @archlint.module test -// @archlint.domain random.value -struct RandomTests {} -EOF assert_fails_with "$test_module_in_production_fixture" \ "test module must be declared in a test scope" malformed_domain_fixture="$(new_fixture malformed-domain)" -cat > "$malformed_domain_fixture/apps/ios/MailApp/Backend/RandomDecider.swift" <<'EOF' -// @archlint.module core -// @archlint.domain Backend_HTTP -enum RandomDecider { - static func decide() -> Bool { - true - } -} -EOF assert_fails_with "$malformed_domain_fixture" \ "module domain must be lowercase dot-or-kebab segments" production_module_in_test_fixture="$(new_fixture production-module-in-test)" -cat > "$production_module_in_test_fixture/apps/ios/MailAppTests/RandomDecider.swift" <<'EOF' -// @archlint.module core -// @archlint.domain random.value -enum RandomDecider { - static func decide() -> Bool { - true - } -} -EOF assert_fails_with "$production_module_in_test_fixture" \ "production module must not be declared in a test scope" missing_exempt_reason_fixture="$(new_fixture missing-exempt-reason)" -cat > "$missing_exempt_reason_fixture/apps/ios/MailApp/Backend/InstallationCredential.swift" <<'EOF' -// @archlint.module exempt -struct InstallationCredential { - let accessToken: String? -} -EOF assert_fails_with "$missing_exempt_reason_fixture" \ "exempt module must declare @archlint.exempt-reason" exempt_domain_fixture="$(new_fixture exempt-domain)" -cat > "$exempt_domain_fixture/apps/ios/MailApp/Backend/MailPrototypeData.swift" <<'EOF' -// @archlint.module exempt -// @archlint.domain mail.sync -// @archlint.exempt-reason static-data -enum MailPrototypeData { - static let account = "demo" -} -EOF assert_fails_with "$exempt_domain_fixture" \ "exempt module must not declare @archlint.domain" unknown_exempt_reason_fixture="$(new_fixture unknown-exempt-reason)" -cat > "$unknown_exempt_reason_fixture/apps/ios/MailApp/Backend/InstallationCredential.swift" <<'EOF' -// @archlint.module exempt -// @archlint.exempt-reason miscellaneous -struct InstallationCredential { - let accessToken: String? -} -EOF assert_fails_with "$unknown_exempt_reason_fixture" "metadata.exemptReason" exempt_reason_outside_exempt_fixture="$(new_fixture exempt-reason-outside-exempt)" -cat > "$exempt_reason_outside_exempt_fixture/apps/ios/MailApp/Backend/AddAccountRequest.swift" <<'EOF' -// @archlint.module interface -// @archlint.domain backend.http -// @archlint.exempt-reason static-data -struct AddAccountRequest { - let displayName: String -} -EOF assert_fails_with "$exempt_reason_outside_exempt_fixture" \ "@archlint.exempt-reason is only valid on exempt modules" entrypoint_exemption_fixture="$(new_fixture entrypoint-exemption)" -mkdir -p "$entrypoint_exemption_fixture/apps/ios/MailApp/App" -cat > "$entrypoint_exemption_fixture/apps/ios/MailApp/App/MailApp.swift" <<'EOF' -// @archlint.module exempt -// @archlint.exempt-reason entrypoint -import SwiftUI - -@main -struct MailApp: App { - var body: some Scene { - WindowGroup { - Text("Mail") - } - } -} -EOF assert_passes "$entrypoint_exemption_fixture" pure_glue_exemption_fixture="$(new_fixture pure-glue-exemption)" -mkdir -p "$pure_glue_exemption_fixture/apps/ios/MailApp/Features/Mailboxes" -cat > "$pure_glue_exemption_fixture/apps/ios/MailApp/Features/Mailboxes/MailboxRoute.swift" <<'EOF' -// @archlint.module exempt -// @archlint.exempt-reason pure-glue - -struct MailboxRoute { - let accountID: String - let mailboxID: String -} -EOF assert_passes "$pure_glue_exemption_fixture" effect_boundary_exemption_fixture="$(new_fixture effect-boundary-exemption)" -cat > "$effect_boundary_exemption_fixture/apps/ios/MailApp/Backend/SystemKeychainClient.swift" <<'EOF' -// @archlint.module exempt -// @archlint.exempt-reason effect-boundary -import Security - -struct SystemKeychainClient { - let keychainClass: CFString -} -EOF assert_passes "$effect_boundary_exemption_fixture" effect_facade_exemption_fixture="$(new_fixture effect-facade-exemption)" -cat > "$effect_facade_exemption_fixture/apps/ios/MailApp/Backend/MailBackendClient.swift" <<'EOF' -// @archlint.module exempt -// @archlint.exempt-reason effect-facade -protocol MailBackendClient { - func sync() async throws -} -EOF assert_passes "$effect_facade_exemption_fixture" static_data_exemption_fixture="$(new_fixture static-data-exemption)" -mkdir -p "$static_data_exemption_fixture/apps/ios/MailApp/Domain" -cat > "$static_data_exemption_fixture/apps/ios/MailApp/Domain/MailStaticData.swift" <<'EOF' -// @archlint.module exempt -// @archlint.exempt-reason static-data -enum MailStaticData { - static let account = "demo" -} -EOF assert_passes "$static_data_exemption_fixture" test_support_exemption_fixture="$(new_fixture test-support-exemption)" -cat > "$test_support_exemption_fixture/apps/ios/MailAppTests/FailingMailSyncStateStore.swift" <<'EOF' -// @archlint.module exempt -// @archlint.exempt-reason test-support -struct FailingMailSyncStateStore { - let reason: String -} -EOF assert_passes "$test_support_exemption_fixture" generic_domain_fixture="$(new_fixture generic-domain)" -for index in $(seq 1 17); do - cat > "$generic_domain_fixture/apps/ios/MailApp/Backend/BroadDomainDecision$index.swift" < Bool { - true -} -EOF -done assert_fails_with "$generic_domain_fixture" \ "domain has 17 production modules; maximum is 16" interface_generic_domain_fixture="$(new_fixture interface-generic-domain)" -for index in $(seq 1 17); do - cat > "$interface_generic_domain_fixture/apps/ios/MailApp/Backend/BroadDomain$index.swift" < "$interface_with_logic_fixture/apps/ios/MailApp/Backend/AddAccountRequest.swift" <<'EOF' -// @archlint.module interface -// @archlint.domain backend.http -struct AddAccountRequest { - let displayName: String - - var normalizedDisplayName: String { - displayName.trimmingCharacters(in: .whitespacesAndNewlines) - } -} -EOF assert_fails_with "$interface_with_logic_fixture" \ "interface module must not contain derived value bodies" value_with_computed_property_fixture="$(new_fixture value-with-computed-property)" -cat > "$value_with_computed_property_fixture/apps/ios/MailApp/Backend/AddAccountRequest.swift" <<'EOF' -// @archlint.module value -// @archlint.domain backend.http -struct AddAccountRequest { - let displayName: String - - var normalizedDisplayName: String { - displayName.trimmingCharacters(in: .whitespacesAndNewlines) - } -} -EOF assert_passes "$value_with_computed_property_fixture" value_with_branching_computed_property_fixture="$(new_fixture value-with-branching-computed-property)" -cat > "$value_with_branching_computed_property_fixture/apps/ios/MailApp/Backend/AddAccountRequest.swift" <<'EOF' -// @archlint.module value -// @archlint.domain backend.http -struct AddAccountRequest { - let displayName: String - - var normalizedDisplayName: String { - if displayName.isEmpty { - return "Mailbox" - } - return displayName - } -} -EOF assert_fails_with "$value_with_branching_computed_property_fixture" \ "value module must not contain control flow" value_with_callable_decision_fixture="$(new_fixture value-with-callable-decision)" -cat > "$value_with_callable_decision_fixture/apps/ios/MailApp/Backend/AddAccountRequest.swift" <<'EOF' -// @archlint.module value -// @archlint.domain backend.http -struct AddAccountRequest { - let displayName: String - - func normalizedDisplayName() -> String { - displayName.trimmingCharacters(in: .whitespacesAndNewlines) - } -} -EOF assert_fails_with "$value_with_callable_decision_fixture" \ "value module must not contain function bodies" core_enum_case_matching_shell_method_fixture="$(new_fixture core-enum-case-matching-shell-method)" -cat > "$core_enum_case_matching_shell_method_fixture/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift" <<'EOF' -// @archlint.module core -// @archlint.domain backend.http -enum HTTPMailBackendDecider { - enum Decision { - case add - } - - static func decide(_ shard: Int) -> Decision { - shard >= 0 ? .add : .add - } -} -EOF -cat > "$core_enum_case_matching_shell_method_fixture/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift" <<'EOF' -// @archlint.module test -// @archlint.domain backend.http -import PropertyBased -import Testing - -@Test -func decisionProperty() async { - await propertyCheck(input: Gen.int(in: 0...10)) { shard in - #expect(HTTPMailBackendDecider.decide(shard) == .add) - } -} -EOF -cat > "$core_enum_case_matching_shell_method_fixture/apps/ios/MailApp/Backend/HTTPMailBackendClient.swift" <<'EOF' -// @archlint.module shell -// @archlint.domain backend.http -import Foundation - -struct HTTPMailBackendClient { - func add() -> URLRequest { - return URLRequest(url: URL(string: "http://localhost")!) - } -} -EOF assert_fails_with "$core_enum_case_matching_shell_method_fixture" \ "shell module must reference a core API in the same @archlint.domain" empty_decider_fixture="$(new_fixture empty-decider)" -cat > "$empty_decider_fixture/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift" <<'EOF' -// @archlint.module core -// @archlint.domain backend.http -enum SQLiteMailSyncStateDecider {} -EOF assert_fails_with "$empty_decider_fixture" \ "core module must declare a callable decision API" assert_fails_with "$empty_decider_fixture" \ "core module must have a same-domain test or stateTest module" type_only_decider_fixture="$(new_fixture type-only-decider)" -cat > "$type_only_decider_fixture/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecision.swift" <<'EOF' -// @archlint.module core -// @archlint.domain backend.http -struct SQLiteMailSyncStateDecision {} -EOF assert_fails_with "$type_only_decider_fixture" \ "core module must declare a callable decision API" effectful_decider_fixture="$(new_fixture effectful-decider)" -cat > "$effectful_decider_fixture/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift" <<'EOF' -// @archlint.module core -// @archlint.domain backend.http -import Foundation - -enum SQLiteMailSyncStateDecider { - static func decideRequest() -> URLRequest { - URLRequest(url: URL(string: "http://localhost")!) - } -} -EOF assert_fails_with "$effectful_decider_fixture" \ "effectful identifiers may only appear in shell, state, interface, test, stateTest, or exempt modules" assert_fails_with "$effectful_decider_fixture" \ "core module must have a same-domain test or stateTest module" missing_property_fixture="$(new_fixture missing-property)" -cat > "$missing_property_fixture/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift" <<'EOF' -// @archlint.module core -// @archlint.domain backend.http -enum HTTPMailBackendDecider { - static func decidePath() -> String { - "/v1/accounts" - } -} -EOF -cat > "$missing_property_fixture/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift" <<'EOF' -// @archlint.module test -// @archlint.domain backend.http -import XCTest - -final class HTTPMailBackendDeciderTests: XCTestCase { - func testPath() { - XCTAssertEqual(HTTPMailBackendDecider.decidePath(), "/v1/accounts") - } -} -EOF assert_fails_with "$missing_property_fixture" \ "core module test must contain at least one property test" property_import_only_fixture="$(new_fixture property-import-only)" -cat > "$property_import_only_fixture/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift" <<'EOF' -// @archlint.module core -// @archlint.domain backend.http -enum HTTPMailBackendDecider { - static func decidePath() -> String { - "/v1/accounts" - } -} -EOF -cat > "$property_import_only_fixture/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift" <<'EOF' -// @archlint.module test -// @archlint.domain backend.http -import PropertyBased -import XCTest - -enum HTTPMailBackendDeciderPropertySuite { - static func testPath() { - _ = PropertyBased.self - } -} -EOF assert_fails_with "$property_import_only_fixture" \ "core module test must contain at least one property test" property_name_only_fixture="$(new_fixture property-name-only)" -cat > "$property_name_only_fixture/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift" <<'EOF' -// @archlint.module core -// @archlint.domain backend.http -enum HTTPMailBackendDecider { - static func decidePath() -> String { - "/v1/accounts" - } -} -EOF -cat > "$property_name_only_fixture/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift" <<'EOF' -// @archlint.module test -// @archlint.domain backend.http -import Testing - -@Test -func pathProperty() { - #expect(HTTPMailBackendDecider.decidePath() == "/v1/accounts") -} -EOF assert_fails_with "$property_name_only_fixture" \ "core module test must contain at least one property test" unrelated_property_test_fixture="$(new_fixture unrelated-property-test)" -cat > "$unrelated_property_test_fixture/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift" <<'EOF' -// @archlint.module core -// @archlint.domain backend.http -enum HTTPMailBackendDecider { - static func decidePath() -> String { - "/v1/accounts" - } -} -EOF -cat > "$unrelated_property_test_fixture/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift" <<'EOF' -// @archlint.module test -// @archlint.domain backend.http -import PropertyBased -import Testing - -enum HTTPMailBackendDeciderPropertySuite { - @Test - static func unrelatedProperty() async { - await propertyCheck(input: Gen.int(in: 0...10)) { value in - #expect(value == value) - } - } -} -EOF assert_fails_with "$unrelated_property_test_fixture" \ "core module property tests must reference every decision API: decidePath" property_local_core_name_only_fixture="$(new_fixture property-local-core-name-only)" -cat > "$property_local_core_name_only_fixture/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift" <<'EOF' -// @archlint.module core -// @archlint.domain backend.http -enum HTTPMailBackendDecider { - static func decidePath() -> String { - "/v1/accounts" - } -} -EOF -cat > "$property_local_core_name_only_fixture/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift" <<'EOF' -// @archlint.module test -// @archlint.domain backend.http -import PropertyBased -import Testing - -enum HTTPMailBackendDeciderPropertySuite { - @Test - static func pathProperty() async { - await propertyCheck(input: Gen.int(in: 0...10)) { value in - let decidePath = value - #expect(decidePath == value) - } - } -} -EOF assert_fails_with "$property_local_core_name_only_fixture" \ "core module property tests must reference every decision API: decidePath" property_reachable_helper_fixture="$(new_fixture property-reachable-helper)" -cat > "$property_reachable_helper_fixture/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift" <<'EOF' -// @archlint.module core -// @archlint.domain backend.http -enum HTTPMailBackendDecider { - static func decidePath() -> String { - "/v1/accounts" - } -} -EOF -cat > "$property_reachable_helper_fixture/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift" <<'EOF' -// @archlint.module test -// @archlint.domain backend.http -import PropertyBased -import Testing - -enum HTTPMailBackendDeciderPropertySuite { - @Test - static func pathProperty() async { - await propertyCheck(input: Gen.int(in: 0...10)) { shard in - #expect(helperPath(shard) == "/v1/accounts") - } - } - - static func helperPath(_ shard: Int) -> String { - HTTPMailBackendDecider.decidePath(shard) - } -} -EOF assert_passes "$property_reachable_helper_fixture" assert_fact_refs_contain \ "$property_reachable_helper_fixture" \ @@ -882,30 +206,6 @@ assert_fact_refs_contain \ "decidePath" ordinary_array_property_fixture="$(new_fixture ordinary-array-property)" -cat > "$ordinary_array_property_fixture/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift" <<'EOF' -// @archlint.module core -// @archlint.domain backend.http -enum HTTPMailBackendDecider { - static func decode(_ values: [Int]) -> Int { - values.count - } -} -EOF -cat > "$ordinary_array_property_fixture/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift" <<'EOF' -// @archlint.module test -// @archlint.domain backend.http -import PropertyBased -import Testing - -enum HTTPMailBackendDeciderPropertySuite { - @Test - static func decodeAcceptsGeneratedArraysProperty() async { - await propertyCheck(input: Gen.int(in: 0...10).array(of: 0...20)) { values in - #expect(HTTPMailBackendDecider.decode(values) == values.count) - } - } -} -EOF assert_passes "$ordinary_array_property_fixture" assert_fact_refs_contain \ "$ordinary_array_property_fixture" \ @@ -919,227 +219,38 @@ assert_fact_refs_not_contain \ "decode" property_unreachable_helper_fixture="$(new_fixture property-unreachable-helper)" -cat > "$property_unreachable_helper_fixture/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift" <<'EOF' -// @archlint.module core -// @archlint.domain backend.http -enum HTTPMailBackendDecider { - static func decidePath() -> String { - "/v1/accounts" - } -} -EOF -cat > "$property_unreachable_helper_fixture/apps/ios/MailAppTests/HTTPMailBackendDeciderPropertySuite.swift" <<'EOF' -// @archlint.module test -// @archlint.domain backend.http -import PropertyBased -import Testing - -enum HTTPMailBackendDeciderPropertySuite { - @Test - static func pathProperty() async { - await propertyCheck(input: Gen.int(in: 0...10)) { value in - #expect(value == value) - } - } - - static func helperPath() -> String { - HTTPMailBackendDecider.decidePath() - } -} -EOF assert_fails_with "$property_unreachable_helper_fixture" \ "core module property tests must reference every decision API: decidePath" wrong_domain_test_fixture="$(new_fixture wrong-domain-test)" -cat > "$wrong_domain_test_fixture/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift" <<'EOF' -// @archlint.module core -// @archlint.domain backend.http -enum HTTPMailBackendDecider { - static func decidePath() -> String { - "/v1/accounts" - } -} -EOF -cat > "$wrong_domain_test_fixture/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift" <<'EOF' -// @archlint.module test -// @archlint.domain mail.sync -import PropertyBased - -func pathProperty() { - _ = PropertyBased.self -} -EOF assert_fails_with "$wrong_domain_test_fixture" \ "core module must have a same-domain test or stateTest module" wrong_test_module_type_fixture="$(new_fixture wrong-test-module-type)" -cat > "$wrong_test_module_type_fixture/apps/ios/MailApp/Backend/HTTPMailBackendDecider.swift" <<'EOF' -// @archlint.module core -// @archlint.domain backend.http -enum HTTPMailBackendDecider { - static func decidePath() -> String { - "/v1/accounts" - } -} -EOF -cat > "$wrong_test_module_type_fixture/apps/ios/MailAppTests/HTTPMailBackendDeciderTests.swift" <<'EOF' -// @archlint.module interface -// @archlint.domain backend.http -import PropertyBased - -func pathProperty() { - _ = PropertyBased.self -} -EOF assert_fails_with "$wrong_test_module_type_fixture" \ "core module must have a same-domain test or stateTest module" non_decider_core_fixture="$(new_fixture non-decider-core)" -cat > "$non_decider_core_fixture/apps/ios/MailApp/Backend/HTTPMailBackendCore.swift" <<'EOF' -// @archlint.module core -// @archlint.domain backend.http -enum HTTPMailBackendCore { - static func decidePath() -> String { - "/v1/accounts" - } -} -EOF assert_fails_with "$non_decider_core_fixture" \ "core module must have a same-domain test or stateTest module" state_test_without_operation_sequences_fixture="$(new_fixture state-test-without-operation-sequences)" -cat > "$state_test_without_operation_sequences_fixture/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift" <<'EOF' -// @archlint.module stateTest -// @archlint.domain backend.sqlite -import PropertyBased -import Testing - -enum SQLiteMailSyncStateDeciderPropertySuite { - @Test - static func roundTripOperationSequencesProperty() async { - await propertyCheck(input: Gen.int(in: 0...10)) { value in - #expect(value == value) - } - } -} -EOF assert_fails_with "$state_test_without_operation_sequences_fixture" \ "stateTest module must contain property operation sequences" state_test_operation_sequences_without_refs_fixture="$(new_fixture state-test-operation-sequences-without-refs)" -cat > "$state_test_operation_sequences_without_refs_fixture/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift" <<'EOF' -// @archlint.module stateTest -// @archlint.domain backend.sqlite -import PropertyBased -import Testing - -enum SQLiteMailSyncStateDeciderPropertySuite { - @Test - static func roundTripOperationSequencesProperty() async { - await propertyCheck(input: Gen.int(in: 0...10).array(of: 0...20)) { values in - for value in values { - #expect(value == value) - } - } - } -} -EOF assert_fails_with "$state_test_operation_sequences_without_refs_fixture" \ "stateTest module must contain property operation sequences" state_test_operation_sequences_without_core_refs_fixture="$(new_fixture state-test-operation-sequences-without-core-refs)" -cat > "$state_test_operation_sequences_without_core_refs_fixture/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift" <<'EOF' -// @archlint.module core -// @archlint.domain backend.sqlite -enum SQLiteMailSyncStateDecider { - static func reduce(_ value: Int) -> Int { - value - } -} -EOF -cat > "$state_test_operation_sequences_without_core_refs_fixture/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift" <<'EOF' -// @archlint.module stateTest -// @archlint.domain backend.sqlite -import PropertyBased -import Testing - -enum SQLiteMailSyncStateDeciderPropertySuite { - @Test - static func reduceProperty() async { - await propertyCheck(input: Gen.int(in: 0...10)) { value in - #expect(SQLiteMailSyncStateDecider.reduce(value) == value) - } - } - - @Test - static func unrelatedOperationSequencesProperty() async { - await propertyCheck(input: Gen.int(in: 0...10).array(of: 0...20)) { values in - for value in values { - #expect(helper(value) == value) - } - } - } - - static func helper(_ value: Int) -> Int { - value - } -} -EOF assert_fails_with "$state_test_operation_sequences_without_core_refs_fixture" \ "stateTest module operation sequences must reference same-domain core decision APIs" state_test_without_state_module_fixture="$(new_fixture state-test-without-state-module)" -cat > "$state_test_without_state_module_fixture/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift" <<'EOF' -// @archlint.module core -// @archlint.domain backend.sqlite -enum SQLiteMailSyncStateDecider { - static func reduce(_ value: Int) -> Int { - value - } -} -EOF -cat > "$state_test_without_state_module_fixture/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift" <<'EOF' -// @archlint.module stateTest -// @archlint.domain backend.sqlite -import PropertyBased -import Testing - -enum SQLiteMailSyncStateDeciderPropertySuite { - @Test - static func reduceOperationSequencesProperty() async { - await propertyCheck(input: Gen.int(in: 0...10).array(of: 0...20)) { values in - for value in values { - #expect(SQLiteMailSyncStateDecider.reduce(value) == value) - } - } - } -} -EOF assert_fails_with "$state_test_without_state_module_fixture" \ "stateTest module must have a same-domain state module" state_test_operation_sequence_refs_fixture="$(new_fixture state-test-operation-sequence-refs)" -cat > "$state_test_operation_sequence_refs_fixture/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift" <<'EOF' -// @archlint.module stateTest -// @archlint.domain backend.sqlite -import PropertyBased -import Testing - -enum SQLiteMailSyncStateDeciderPropertySuite { - @Test - static func roundTripOperationSequencesProperty() async { - await propertyCheck(input: Gen.int(in: 0...10).array(of: 0...20)) { values in - for value in values { - #expect(helper(value) == value) - } - } - } - - static func helper(_ value: Int) -> Int { - SQLiteMailSyncStateDecider.reduce(value) - } -} -EOF assert_fact_refs_contain \ "$state_test_operation_sequence_refs_fixture" \ "SQLiteMailSyncStateDeciderPropertySuite.swift" \ @@ -1147,170 +258,39 @@ assert_fact_refs_contain \ "reduce" state_unrelated_to_operation_sequences_fixture="$(new_fixture state-unrelated-to-operation-sequences)" -cat > "$state_unrelated_to_operation_sequences_fixture/apps/ios/MailApp/Backend/FakeMailBackendClient.swift" <<'EOF' -// @archlint.module state -// @archlint.domain backend.http -import Foundation - -actor FakeMailBackendClient { - private var accounts: [String] = [] - - func add(_ account: String) { - accounts.append(account) - } -} -EOF -cat > "$state_unrelated_to_operation_sequences_fixture/apps/ios/MailAppTests/HTTPMailBackendStatePropertySuite.swift" <<'EOF' -// @archlint.module stateTest -// @archlint.domain backend.http -import PropertyBased -import Testing - -enum HTTPMailBackendStatePropertySuite { - @Test - static func unrelatedOperationSequencesProperty() async { - await propertyCheck(input: Gen.int(in: 0...10).array(of: 0...20)) { values in - for value in values { - #expect(OtherDecider.reduce(value) == value) - } - } - } -} -EOF assert_fails_with "$state_unrelated_to_operation_sequences_fixture" \ "state module must reference a core decision API reached by same-domain property operation sequences" state_without_stateful_apis_fixture="$(new_fixture state-without-stateful-apis)" -cat > "$state_without_stateful_apis_fixture/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift" <<'EOF' -// @archlint.module core -// @archlint.domain backend.sqlite -enum SQLiteMailSyncStateDecider { - static func reduce(_ value: Int) -> Int { - value - } -} -EOF -cat > "$state_without_stateful_apis_fixture/apps/ios/MailApp/Backend/SQLiteMailSyncStateStore.swift" <<'EOF' -// @archlint.module state -// @archlint.domain backend.sqlite -struct SQLiteMailSyncStateStore { - func snapshot(_ value: Int) -> Int { - SQLiteMailSyncStateDecider.reduce(value) - } -} -EOF -cat > "$state_without_stateful_apis_fixture/apps/ios/MailAppTests/SQLiteMailSyncStateDeciderPropertySuite.swift" <<'EOF' -// @archlint.module stateTest -// @archlint.domain backend.sqlite -import PropertyBased -import Testing - -enum SQLiteMailSyncStateDeciderPropertySuite { - @Test - static func reduceOperationSequencesProperty() async { - await propertyCheck(input: Gen.int(in: 0...10).array(of: 0...20)) { values in - for value in values { - #expect(SQLiteMailSyncStateDecider.reduce(value) == value) - } - } - } -} -EOF assert_fails_with "$state_without_stateful_apis_fixture" \ "state module must own stateful APIs" shared_state_without_state_test_fixture="$(new_fixture shared-state-without-state-test)" -cat > "$shared_state_without_state_test_fixture/apps/ios/MailApp/Backend/FakeMailBackendClient.swift" <<'EOF' -// @archlint.module state -// @archlint.domain backend.http -import Foundation - -actor FakeMailBackendClient { - private var accounts: [String] = [] - - func add(_ account: String) { - accounts.append(account) - } -} -EOF assert_fails_with "$shared_state_without_state_test_fixture" \ "state module must have a same-domain stateTest with property operation sequences" shared_state_outside_state_fixture="$(new_fixture shared-state-outside-state)" -cat > "$shared_state_outside_state_fixture/apps/ios/MailApp/Backend/FakeMailBackendClient.swift" <<'EOF' -// @archlint.module shell -// @archlint.domain backend.http -import Foundation - -actor FakeMailBackendClient { - private var accounts: [String] = [] - - func add(_ account: String) { - accounts.append(account) - } -} -EOF assert_fails_with "$shared_state_outside_state_fixture" \ "shared mutable state may only appear in state, test, or stateTest modules" assert_shared_state_contains "$shared_state_outside_state_fixture" \ "FakeMailBackendClient.swift" "swift-actor-var" "FakeMailBackendClient" shared_state_comment_only_fixture="$(new_fixture shared-state-comment-only)" -cat > "$shared_state_comment_only_fixture/apps/ios/MailApp/Backend/FakeMailBackendClient.swift" <<'EOF' -// @archlint.module interface -// @archlint.domain backend.http -// actor CommentOnly { private var accounts: [String] = [] } -// @Published var commentOnly: String = "" -// DatabaseQueue and UserDefaults.standard are mentioned in prose only. - -struct FakeMailBackendClient { - let endpoint: String -} -EOF assert_passes "$shared_state_comment_only_fixture" published_state_outside_state_fixture="$(new_fixture published-state-outside-state)" -cat > "$published_state_outside_state_fixture/apps/ios/MailApp/Backend/FakeMailBackendClient.swift" <<'EOF' -// @archlint.module shell -// @archlint.domain backend.http -import Combine - -final class FakeMailBackendClient { - @Published var accounts: [String] = [] -} -EOF assert_fails_with "$published_state_outside_state_fixture" \ "shared mutable state may only appear in state, test, or stateTest modules" assert_shared_state_contains "$published_state_outside_state_fixture" \ "FakeMailBackendClient.swift" "swift-published" "@Published" database_queue_state_outside_state_fixture="$(new_fixture database-queue-state-outside-state)" -cat > "$database_queue_state_outside_state_fixture/apps/ios/MailApp/Backend/FakeMailBackendClient.swift" <<'EOF' -// @archlint.module shell -// @archlint.domain backend.http -import GRDB - -struct FakeMailBackendClient { - let database: DatabaseQueue -} -EOF assert_fails_with "$database_queue_state_outside_state_fixture" \ "shared mutable state may only appear in state, test, or stateTest modules" assert_shared_state_contains "$database_queue_state_outside_state_fixture" \ "FakeMailBackendClient.swift" "swift-database-queue" "DatabaseQueue" user_defaults_state_outside_state_fixture="$(new_fixture user-defaults-state-outside-state)" -cat > "$user_defaults_state_outside_state_fixture/apps/ios/MailApp/Backend/FakeMailBackendClient.swift" <<'EOF' -// @archlint.module shell -// @archlint.domain backend.http -import Foundation - -struct FakeMailBackendClient { - func load() -> String { - UserDefaults.standard.string(forKey: "lastAccount") ?? "" - } -} -EOF assert_fails_with "$user_defaults_state_outside_state_fixture" \ "shared mutable state may only appear in state, test, or stateTest modules" assert_shared_state_contains "$user_defaults_state_outside_state_fixture" \ diff --git a/test/lib.sh b/test/lib.sh index 7911f7e..e7ddddf 100644 --- a/test/lib.sh +++ b/test/lib.sh @@ -29,6 +29,21 @@ assert_facts() { uv run --project "$ARCHLINT_ROOT" python - "$1" } +# copy_fixture SOURCE_DIR DEST_DIR +# +# Copies a checked-in fixture tree into a temporary test directory. Existing +# files in DEST_DIR may be overwritten by overlay fixtures. +copy_fixture() { + source_dir="$1" + dest_dir="$2" + if [ ! -d "$source_dir" ]; then + printf '%s\n' "missing fixture: $source_dir" >&2 + exit 1 + fi + mkdir -p "$dest_dir" + cp -R "$source_dir/." "$dest_dir" +} + # expect_violation EXPECTED_SUBSTRING COMMAND [ARGS...] # # Runs COMMAND expecting it to FAIL (non-zero) and to print EXPECTED_SUBSTRING diff --git a/typescript/fixtures/base/node_modules/@types/local-env/index.d.ts b/typescript/fixtures/base/node_modules/@types/local-env/index.d.ts new file mode 100644 index 0000000..3e365f5 --- /dev/null +++ b/typescript/fixtures/base/node_modules/@types/local-env/index.d.ts @@ -0,0 +1 @@ +declare const LOCAL_ENV_FLAG: boolean; diff --git a/typescript/fixtures/base/node_modules/@types/local-env/package.json b/typescript/fixtures/base/node_modules/@types/local-env/package.json new file mode 100644 index 0000000..8618ff5 --- /dev/null +++ b/typescript/fixtures/base/node_modules/@types/local-env/package.json @@ -0,0 +1,5 @@ +{ + "name": "@types/local-env", + "version": "1.0.0", + "types": "index.d.ts" +} diff --git a/typescript/fixtures/base/src/closure.ts b/typescript/fixtures/base/src/closure.ts new file mode 100644 index 0000000..969826f --- /dev/null +++ b/typescript/fixtures/base/src/closure.ts @@ -0,0 +1,14 @@ +// @archlint.module core +// @archlint.domain demo.closure + +export function decideA(value: number): boolean { + return value >= 0; +} + +export function decideB(value: number): boolean { + return value < 0; +} + +export function decideC(value: number): boolean { + return value === 0; +} diff --git a/typescript/fixtures/base/src/consumer.ts b/typescript/fixtures/base/src/consumer.ts new file mode 100644 index 0000000..91a1ffe --- /dev/null +++ b/typescript/fixtures/base/src/consumer.ts @@ -0,0 +1,13 @@ +// @archlint.module exempt +// @archlint.exempt-reason test-support + +import { run } from "./handler.js"; + +function localRun(): string { + return "local"; +} + +export function consume(): string { + localRun(); + return run(); +} diff --git a/typescript/fixtures/base/src/decision.ts b/typescript/fixtures/base/src/decision.ts new file mode 100644 index 0000000..df2e088 --- /dev/null +++ b/typescript/fixtures/base/src/decision.ts @@ -0,0 +1,15 @@ +// @archlint.module core +// @archlint.domain demo.decision + +export function decide(x: number): string { + return x >= 0 ? "positive" : "negative"; +} + +export const decisionNode = (value: number): { kind: "node"; value: number } => ({ + kind: "node", + value, +}); + +export function wrappedDecide(x: number): string { + return decide(x); +} diff --git a/typescript/fixtures/base/src/handler.ts b/typescript/fixtures/base/src/handler.ts new file mode 100644 index 0000000..f9a40d1 --- /dev/null +++ b/typescript/fixtures/base/src/handler.ts @@ -0,0 +1,14 @@ +// @archlint.module shell +// @archlint.domain demo.decision + +import { Command } from "commander"; +import { decide } from "./decision.js"; + +function createCommand(): Command { + return new Command(); +} + +export function run(): string { + createCommand(); + return decide(1); +} diff --git a/typescript/fixtures/base/tests/closure.test.mts b/typescript/fixtures/base/tests/closure.test.mts new file mode 100644 index 0000000..ad6b08d --- /dev/null +++ b/typescript/fixtures/base/tests/closure.test.mts @@ -0,0 +1,28 @@ +// @archlint.module test +// @archlint.domain demo.closure + +import { test } from "node:test"; +import assert from "node:assert/strict"; +import * as fc from "fast-check"; +import { decideA, decideB, decideC } from "../src/closure.js"; + +const genB = fc.integer().map((value) => { + decideB(value); + return value; +}); + +function makeProp(decide: (value: number) => boolean) { + return (value: number) => { + decide(value); + return true; + }; +} + +test("closure properties", () => { + fc.assert( + fc.property(fc.integer(), (value) => { + assert.equal(typeof decideA(value), "boolean"); + }), + ); + fc.assert(fc.property(genB, makeProp(decideC))); +}); diff --git a/typescript/fixtures/base/tests/decision.test.mts b/typescript/fixtures/base/tests/decision.test.mts new file mode 100644 index 0000000..64b15db --- /dev/null +++ b/typescript/fixtures/base/tests/decision.test.mts @@ -0,0 +1,19 @@ +// @archlint.module test +// @archlint.domain demo.decision + +import { test } from "node:test"; +import assert from "node:assert/strict"; +import * as fc from "fast-check"; +import { wrappedDecide } from "../src/decision.js"; + +function checkDecision(value: number): string { + return wrappedDecide(value); +} + +test("decision property", () => { + fc.assert( + fc.property(fc.integer(), (value) => { + assert.equal(typeof checkDecision(value), "string"); + }), + ); +}); diff --git a/typescript/fixtures/base/tsconfig.json b/typescript/fixtures/base/tsconfig.json new file mode 100644 index 0000000..aac9d0f --- /dev/null +++ b/typescript/fixtures/base/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "skipLibCheck": true, + "types": ["local-env"] + }, + "include": ["src/**/*", "tests/**/*"] +} diff --git a/typescript/fixtures/constant-only/src/constant-only.ts b/typescript/fixtures/constant-only/src/constant-only.ts new file mode 100644 index 0000000..6e22fac --- /dev/null +++ b/typescript/fixtures/constant-only/src/constant-only.ts @@ -0,0 +1,6 @@ +// @archlint.module core +// @archlint.domain demo.constant + +export function decideConstant(x: number): string { + return x >= 0 ? "positive" : "negative"; +} diff --git a/typescript/fixtures/constant-only/tests/constant-only.test.mts b/typescript/fixtures/constant-only/tests/constant-only.test.mts new file mode 100644 index 0000000..c0e684b --- /dev/null +++ b/typescript/fixtures/constant-only/tests/constant-only.test.mts @@ -0,0 +1,15 @@ +// @archlint.module test +// @archlint.domain demo.constant + +import { test } from "node:test"; +import assert from "node:assert/strict"; +import * as fc from "fast-check"; +import { decideConstant } from "../src/constant-only.js"; + +test("constant property", () => { + fc.assert( + fc.property(fc.constant(undefined), () => { + assert.equal(decideConstant(1), "positive"); + }), + ); +}); diff --git a/typescript/test.sh b/typescript/test.sh index 4861377..d245fb2 100755 --- a/typescript/test.sh +++ b/typescript/test.sh @@ -6,152 +6,9 @@ ARCHLINT_ROOT="$ROOT" . "$ROOT/test/lib.sh" TMPDIR="${TMPDIR:-/tmp}/archlint-typescript-fixture-$$" trap 'rm -rf "$TMPDIR"' EXIT +export UV_CACHE_DIR="${UV_CACHE_DIR:-$TMPDIR/uv-cache}" -mkdir -p "$TMPDIR/src" "$TMPDIR/tests" - -cat > "$TMPDIR/src/decision.ts" <<'TS' -// @archlint.module core -// @archlint.domain demo.decision - -export function decide(x: number): string { - return x >= 0 ? "positive" : "negative"; -} - -export const decisionNode = (value: number): { kind: "node"; value: number } => ({ - kind: "node", - value, -}); - -export function wrappedDecide(x: number): string { - return decide(x); -} -TS - -cat > "$TMPDIR/src/handler.ts" <<'TS' -// @archlint.module shell -// @archlint.domain demo.decision - -import { Command } from "commander"; -import { decide } from "./decision.js"; - -export function run(): string { - new Command(); - return decide(1); -} -TS - -cat > "$TMPDIR/src/consumer.ts" <<'TS' -// @archlint.module exempt -// @archlint.exempt-reason test-support - -import { run } from "./handler.js"; - -function localRun(): string { - return "local"; -} - -export function consume(): string { - localRun(); - return run(); -} -TS - -cat > "$TMPDIR/tests/decision.test.mts" <<'TS' -// @archlint.module test -// @archlint.domain demo.decision - -import { test } from "node:test"; -import assert from "node:assert/strict"; -import * as fc from "fast-check"; -import { wrappedDecide } from "../src/decision.js"; - -function checkDecision(value: number): string { - return wrappedDecide(value); -} - -test("decision property", () => { - fc.assert( - fc.property(fc.integer(), (value) => { - assert.equal(typeof checkDecision(value), "string"); - }), - ); -}); -TS - -cat > "$TMPDIR/src/closure.ts" <<'TS' -// @archlint.module core -// @archlint.domain demo.closure - -export function decideA(value: number): boolean { - return value >= 0; -} - -export function decideB(value: number): boolean { - return value < 0; -} - -export function decideC(value: number): boolean { - return value === 0; -} -TS - -cat > "$TMPDIR/tests/closure.test.mts" <<'TS' -// @archlint.module test -// @archlint.domain demo.closure - -import { test } from "node:test"; -import assert from "node:assert/strict"; -import * as fc from "fast-check"; -import { decideA, decideB, decideC } from "../src/closure.js"; - -const genB = fc.integer().map((value) => { - decideB(value); - return value; -}); - -function makeProp(decide: (value: number) => boolean) { - return (value: number) => { - decide(value); - return true; - }; -} - -test("closure properties", () => { - fc.assert( - fc.property(fc.integer(), (value) => { - assert.equal(typeof decideA(value), "boolean"); - }), - ); - fc.assert(fc.property(genB, makeProp(decideC))); -}); -TS - -mkdir -p "$TMPDIR/node_modules/@types/local-env" -cat > "$TMPDIR/node_modules/@types/local-env/index.d.ts" <<'TS' -declare const LOCAL_ENV_FLAG: boolean; -TS - -cat > "$TMPDIR/node_modules/@types/local-env/package.json" <<'JSON' -{ - "name": "@types/local-env", - "version": "1.0.0", - "types": "index.d.ts" -} -JSON - -cat > "$TMPDIR/tsconfig.json" <<'JSON' -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "strict": true, - "skipLibCheck": true, - "types": ["local-env"] - }, - "include": ["src/**/*", "tests/**/*"] -} -JSON +copy_fixture "$ROOT/typescript/fixtures/base" "$TMPDIR" npm --prefix "$ROOT/typescript" install >/dev/null npm --prefix "$ROOT/typescript" run --silent typecheck @@ -176,6 +33,8 @@ assert check["generatedInputs"] == [{"name": "value", "uses": ["value"]}], check handler = [item for item in document["files"] if item["path"].endswith("handler.ts")][0] assert "commander" in handler["effectfulImports"], handler assert "Command" in handler["effectfulIdentifiers"], handler +assert "createCommand" in handler["effectfulIdentifiers"], handler +assert "run" in handler["effectfulIdentifiers"], handler # moduleName is the capitalized file basename and qualifiedReferences use the # same prefix for semantic cross-file references. assert handler["moduleName"] == "Handler", handler @@ -190,33 +49,14 @@ assert "decisionNode" not in core["propertyTestSurface"], core assert "decide" in core["propertyTestSurface"], core PY -cat > "$TMPDIR/src/constant-only.ts" <<'TS' -// @archlint.module core -// @archlint.domain demo.constant - -export function decideConstant(x: number): string { - return x >= 0 ? "positive" : "negative"; -} -TS - -cat > "$TMPDIR/tests/constant-only.test.mts" <<'TS' -// @archlint.module test -// @archlint.domain demo.constant +copy_fixture "$ROOT/typescript/fixtures/constant-only" "$TMPDIR" -import { test } from "node:test"; -import assert from "node:assert/strict"; -import * as fc from "fast-check"; -import { decideConstant } from "../src/constant-only.js"; +expect_violation "core module property tests must reference every decision API: decideConstant" \ + uv run --project "$ROOT" python "$ROOT/evaluate.py" \ + --repo-root "$TMPDIR" --adapter typescript --typescript-root . -test("constant property", () => { - fc.assert( - fc.property(fc.constant(undefined), () => { - assert.equal(decideConstant(1), "positive"); - }), - ); -}); -TS +copy_fixture "$ROOT/typescript/fixtures/effectful-expansion" "$TMPDIR" -expect_violation "core module property tests must reference every decision API: decideConstant" \ +expect_violation "effectful identifiers may only appear in shell, state, interface, test, stateTest, or exempt modules" \ uv run --project "$ROOT" python "$ROOT/evaluate.py" \ --repo-root "$TMPDIR" --adapter typescript --typescript-root . From 6efbc5078fb09ec43f219825bc59b0d9dc40085e Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Mon, 8 Jun 2026 17:38:35 -0400 Subject: [PATCH 2/2] Add TypeScript dependency effect summaries --- evaluate.py | 64 +- evaluate_test.py | 79 + fact.schema.json | 27 + gameplans/semantic-reference-resolution.json | 706 +++++++++ typescript/dependency-summaries/npm.json | 164 +++ .../node_modules/@anthropic-ai/sdk/index.d.ts | 10 + .../@anthropic-ai/sdk/package.json | 5 + .../src/anthropic-core.ts | 25 + .../tests/anthropic-core.test.mts | 14 + .../anthropic-dependency/tsconfig.json | 11 + .../node_modules/@types/local-env/index.d.ts | 5 + typescript/fixtures/base/src/handler.ts | 9 + .../node_modules/@effect/sql-pg/index.d.ts | 10 + .../node_modules/@effect/sql-pg/package.json | 5 + .../node_modules/@effect/sql/index.d.ts | 9 + .../node_modules/@effect/sql/package.json | 5 + .../node_modules/effect/index.d.ts | 35 + .../node_modules/effect/package.json | 5 + .../effect-requirements/src/effect-core.ts | 60 + .../tests/effect-core.test.mts | 12 + .../effect-requirements/tsconfig.json | 11 + .../effectful-expansion/src/effectful-core.ts | 13 + .../tests/effectful-core.test.mts | 15 + .../effectful-expansion/tsconfig.json | 11 + .../local-relative-expansion/src/core.ts | 9 + .../support/effects.ts | 3 + .../local-relative-expansion/tsconfig.json | 11 + .../node_modules/openai/index.d.ts | 16 + .../node_modules/openai/package.json | 5 + .../openai-dependency/src/openai-core.ts | 36 + .../tests/openai-core.test.mts | 16 + .../fixtures/openai-dependency/tsconfig.json | 11 + .../node_modules/@sentry/node/index.d.ts | 12 + .../node_modules/@sentry/node/package.json | 5 + .../sentry-dependency/src/sentry-core.ts | 27 + .../tests/sentry-core.test.mts | 14 + .../fixtures/sentry-dependency/tsconfig.json | 11 + .../@trigger.dev/core/dist/esm/v3/index.d.ts | 1 + .../core/dist/esm/v3/logger-api.d.ts | 3 + .../core/dist/esm/v3/logger/index.d.ts | 4 + .../@trigger.dev/core/package.json | 10 + .../@trigger.dev/sdk/dist/esm/v3/index.d.ts | 2 + .../sdk/dist/esm/v3/metadata.d.ts | 5 + .../@trigger.dev/sdk/package.json | 10 + .../trigger-dependency/src/trigger-core.ts | 18 + .../fixtures/trigger-dependency/tsconfig.json | 11 + .../workspace-package-expansion/package.json | 4 + .../packages/consumer/package.json | 5 + .../packages/consumer/src/core.ts | 9 + .../packages/consumer/tsconfig.json | 16 + .../packages/provider/package.json | 5 + .../packages/provider/src/index.ts | 3 + typescript/src/main.ts | 1289 ++++++++++++++++- typescript/test.sh | 263 ++++ 54 files changed, 3122 insertions(+), 22 deletions(-) create mode 100644 gameplans/semantic-reference-resolution.json create mode 100644 typescript/dependency-summaries/npm.json create mode 100644 typescript/fixtures/anthropic-dependency/node_modules/@anthropic-ai/sdk/index.d.ts create mode 100644 typescript/fixtures/anthropic-dependency/node_modules/@anthropic-ai/sdk/package.json create mode 100644 typescript/fixtures/anthropic-dependency/src/anthropic-core.ts create mode 100644 typescript/fixtures/anthropic-dependency/tests/anthropic-core.test.mts create mode 100644 typescript/fixtures/anthropic-dependency/tsconfig.json create mode 100644 typescript/fixtures/effect-requirements/node_modules/@effect/sql-pg/index.d.ts create mode 100644 typescript/fixtures/effect-requirements/node_modules/@effect/sql-pg/package.json create mode 100644 typescript/fixtures/effect-requirements/node_modules/@effect/sql/index.d.ts create mode 100644 typescript/fixtures/effect-requirements/node_modules/@effect/sql/package.json create mode 100644 typescript/fixtures/effect-requirements/node_modules/effect/index.d.ts create mode 100644 typescript/fixtures/effect-requirements/node_modules/effect/package.json create mode 100644 typescript/fixtures/effect-requirements/src/effect-core.ts create mode 100644 typescript/fixtures/effect-requirements/tests/effect-core.test.mts create mode 100644 typescript/fixtures/effect-requirements/tsconfig.json create mode 100644 typescript/fixtures/effectful-expansion/src/effectful-core.ts create mode 100644 typescript/fixtures/effectful-expansion/tests/effectful-core.test.mts create mode 100644 typescript/fixtures/effectful-expansion/tsconfig.json create mode 100644 typescript/fixtures/local-relative-expansion/src/core.ts create mode 100644 typescript/fixtures/local-relative-expansion/support/effects.ts create mode 100644 typescript/fixtures/local-relative-expansion/tsconfig.json create mode 100644 typescript/fixtures/openai-dependency/node_modules/openai/index.d.ts create mode 100644 typescript/fixtures/openai-dependency/node_modules/openai/package.json create mode 100644 typescript/fixtures/openai-dependency/src/openai-core.ts create mode 100644 typescript/fixtures/openai-dependency/tests/openai-core.test.mts create mode 100644 typescript/fixtures/openai-dependency/tsconfig.json create mode 100644 typescript/fixtures/sentry-dependency/node_modules/@sentry/node/index.d.ts create mode 100644 typescript/fixtures/sentry-dependency/node_modules/@sentry/node/package.json create mode 100644 typescript/fixtures/sentry-dependency/src/sentry-core.ts create mode 100644 typescript/fixtures/sentry-dependency/tests/sentry-core.test.mts create mode 100644 typescript/fixtures/sentry-dependency/tsconfig.json create mode 100644 typescript/fixtures/trigger-dependency/node_modules/@trigger.dev/core/dist/esm/v3/index.d.ts create mode 100644 typescript/fixtures/trigger-dependency/node_modules/@trigger.dev/core/dist/esm/v3/logger-api.d.ts create mode 100644 typescript/fixtures/trigger-dependency/node_modules/@trigger.dev/core/dist/esm/v3/logger/index.d.ts create mode 100644 typescript/fixtures/trigger-dependency/node_modules/@trigger.dev/core/package.json create mode 100644 typescript/fixtures/trigger-dependency/node_modules/@trigger.dev/sdk/dist/esm/v3/index.d.ts create mode 100644 typescript/fixtures/trigger-dependency/node_modules/@trigger.dev/sdk/dist/esm/v3/metadata.d.ts create mode 100644 typescript/fixtures/trigger-dependency/node_modules/@trigger.dev/sdk/package.json create mode 100644 typescript/fixtures/trigger-dependency/src/trigger-core.ts create mode 100644 typescript/fixtures/trigger-dependency/tsconfig.json create mode 100644 typescript/fixtures/workspace-package-expansion/package.json create mode 100644 typescript/fixtures/workspace-package-expansion/packages/consumer/package.json create mode 100644 typescript/fixtures/workspace-package-expansion/packages/consumer/src/core.ts create mode 100644 typescript/fixtures/workspace-package-expansion/packages/consumer/tsconfig.json create mode 100644 typescript/fixtures/workspace-package-expansion/packages/provider/package.json create mode 100644 typescript/fixtures/workspace-package-expansion/packages/provider/src/index.ts diff --git a/evaluate.py b/evaluate.py index d3831f0..33d8589 100644 --- a/evaluate.py +++ b/evaluate.py @@ -95,6 +95,19 @@ class SharedStateEvidence: references: set[str] +@dataclass(frozen=True) +class EffectfulReference: + kind: str + category: str + reference: str + origin: str + enclosing_identifier: str + package_name: str + summary_id: str + receiver_type: str + evidence: tuple[str, ...] + + @dataclass(frozen=True) class SourceFile: path: str @@ -111,6 +124,7 @@ class SourceFile: qualified_references: set[str] effectful_imports: set[str] effectful_identifiers: set[str] + effectful_references: tuple[EffectfulReference, ...] has_effectful_imports: bool has_effectful_identifiers: bool shared_state: tuple[SharedStateEvidence, ...] @@ -339,6 +353,20 @@ def parse_source_file(item: dict[str, Any]) -> SourceFile: ) for evidence in item["sharedState"] ) + effectful_references = tuple( + EffectfulReference( + kind=evidence["kind"], + category=evidence["category"], + reference=evidence["reference"], + origin=evidence["origin"], + enclosing_identifier=evidence["enclosingIdentifier"], + package_name=evidence["packageName"], + summary_id=evidence["summaryId"], + receiver_type=evidence["receiverType"], + evidence=tuple(evidence["evidence"]), + ) + for evidence in item.get("effectfulReferences", []) + ) return SourceFile( path=item["path"], test_scope=item["testScope"], @@ -358,8 +386,11 @@ def parse_source_file(item: dict[str, Any]) -> SourceFile: qualified_references=set(item["qualifiedReferences"]), effectful_imports=set(item["effectfulImports"]), effectful_identifiers=set(item["effectfulIdentifiers"]), - has_effectful_imports=bool(item["effectfulImports"]), - has_effectful_identifiers=bool(item["effectfulIdentifiers"]), + effectful_references=effectful_references, + has_effectful_imports=bool(item["effectfulImports"]) + or any(effect.kind == "import" for effect in effectful_references), + has_effectful_identifiers=bool(item["effectfulIdentifiers"]) + or any(effect.kind != "import" for effect in effectful_references), shared_state=shared_state, has_shared_mutable_state=bool(shared_state), property_checks=property_checks, @@ -421,6 +452,35 @@ def evaluate_fact_consistency(files: list[SourceFile]) -> list[Violation]: + ", ".join(effectful_identifiers_outside_identifiers), ) ) + effect_enclosing_identifiers_outside_identifiers = sorted( + { + effect.enclosing_identifier + for effect in source_file.effectful_references + if effect.enclosing_identifier + } + - source_file.identifiers + ) + if effect_enclosing_identifiers_outside_identifiers: + violations.append( + Violation( + source_file.path, + "effectful reference enclosing identifiers must be structural identifiers: " + + ", ".join(effect_enclosing_identifiers_outside_identifiers), + ) + ) + dependency_effects_without_summary = sorted( + effect.reference + for effect in source_file.effectful_references + if effect.origin == "dependency-summary" and (not effect.package_name or not effect.summary_id) + ) + if dependency_effects_without_summary: + violations.append( + Violation( + source_file.path, + "dependency effectful references must include packageName and summaryId: " + + ", ".join(dependency_effects_without_summary), + ) + ) references_outside_property = sorted( source_file.property_test_references - source_file.api_references ) diff --git a/evaluate_test.py b/evaluate_test.py index dc6b9e3..c556512 100644 --- a/evaluate_test.py +++ b/evaluate_test.py @@ -32,6 +32,7 @@ def source_file(**overrides): "qualifiedReferences": [], "effectfulImports": [], "effectfulIdentifiers": [], + "effectfulReferences": [], "sharedState": [], "propertyChecks": [], "interfaceLogicEvidence": empty_interface_logic_evidence(), @@ -110,6 +111,82 @@ def test_effectful_identifiers_must_be_structural_identifiers(self): [violation.message for violation in violations], ) + def test_effectful_reference_enclosing_identifier_must_be_structural_identifier(self): + source = source_file( + path="/repo/Core.swift", + identifiers=[], + effectfulReferences=[ + { + "kind": "call", + "category": "network", + "reference": "messages.create", + "origin": "dependency-summary", + "enclosingIdentifier": "run", + "packageName": "@anthropic-ai/sdk", + "summaryId": "anthropic-sdk-messages-create-network", + "receiverType": "Anthropic", + "evidence": ["summary"], + } + ], + ) + + violations = evaluate.evaluate([source]) + + self.assertIn( + "effectful reference enclosing identifiers must be structural identifiers: run", + [violation.message for violation in violations], + ) + + def test_dependency_effectful_reference_requires_summary_identity(self): + source = source_file( + path="/repo/Core.swift", + effectfulReferences=[ + { + "kind": "call", + "category": "network", + "reference": "messages.create", + "origin": "dependency-summary", + "enclosingIdentifier": "decideSync", + "packageName": "", + "summaryId": "", + "receiverType": "Anthropic", + "evidence": ["summary"], + } + ], + ) + + violations = evaluate.evaluate([source]) + + self.assertIn( + "dependency effectful references must include packageName and summaryId: messages.create", + [violation.message for violation in violations], + ) + + def test_structured_effectful_reference_counts_as_effectful_identifier(self): + source = source_file( + path="/repo/Core.swift", + effectfulReferences=[ + { + "kind": "call", + "category": "network", + "reference": "messages.create", + "origin": "dependency-summary", + "enclosingIdentifier": "decideSync", + "packageName": "@anthropic-ai/sdk", + "summaryId": "anthropic-sdk-messages-create-network", + "receiverType": "Anthropic", + "evidence": ["summary"], + } + ], + ) + + violations = evaluate.evaluate([source]) + + self.assertIn( + "effectful identifiers may only appear in shell, state, interface, test, stateTest, or exempt modules", + [violation.message for violation in violations], + ) + def test_property_test_references_must_be_api_references(self): source = source_file( path="/repo/CoreTests.swift", @@ -1432,6 +1509,7 @@ def test_run_adapter_parses_adapter_json_without_evaluating_policy(self): "qualifiedReferences": [], "effectfulImports": [], "effectfulIdentifiers": [], + "effectfulReferences": [], "sharedState": [], "propertyChecks": [], "interfaceLogicEvidence": empty_interface_logic_evidence(), @@ -1573,6 +1651,7 @@ def valid_fact_document(): "qualifiedReferences": [], "effectfulImports": [], "effectfulIdentifiers": [], + "effectfulReferences": [], "sharedState": [], "propertyChecks": [], "interfaceLogicEvidence": empty_interface_logic_evidence(), diff --git a/fact.schema.json b/fact.schema.json index 07e914e..345b44f 100644 --- a/fact.schema.json +++ b/fact.schema.json @@ -85,6 +85,28 @@ "references": { "$ref": "#/$defs/stringList" } } }, + "effectfulReference": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "category", "reference", "origin", "enclosingIdentifier", "packageName", "summaryId", "receiverType", "evidence"], + "properties": { + "kind": { + "enum": ["call", "new", "import", "identifier"] + }, + "category": { + "enum": ["network", "filesystem", "database", "process", "timer", "console", "storage", "browser", "unknown"] + }, + "reference": { "$ref": "#/$defs/nonEmptyString" }, + "origin": { + "enum": ["standard-library", "global", "dependency-summary", "local-call-expansion"] + }, + "enclosingIdentifier": { "type": "string" }, + "packageName": { "type": "string" }, + "summaryId": { "type": "string" }, + "receiverType": { "type": "string" }, + "evidence": { "$ref": "#/$defs/stringList" } + } + }, "interfaceLogicEvidence": { "type": "object", "additionalProperties": false, @@ -140,6 +162,11 @@ "qualifiedReferences": { "$ref": "#/$defs/stringList" }, "effectfulImports": { "$ref": "#/$defs/stringList" }, "effectfulIdentifiers": { "$ref": "#/$defs/stringList" }, + "effectfulReferences": { + "type": "array", + "uniqueItems": true, + "items": { "$ref": "#/$defs/effectfulReference" } + }, "sharedState": { "type": "array", "uniqueItems": true, diff --git a/gameplans/semantic-reference-resolution.json b/gameplans/semantic-reference-resolution.json new file mode 100644 index 0000000..e3b1807 --- /dev/null +++ b/gameplans/semantic-reference-resolution.json @@ -0,0 +1,706 @@ +{ + "projectName": "semantic-reference-resolution", + "owner": "flowglad", + "repo": "archlint", + "specFile": null, + "workstream": null, + "problemStatement": "archlint's adapters emit reference facts by syntactic extraction, so qualifiedReferences is incomplete by construction: a symbol used bare after open/import (or within the same module or package) cannot be attributed to its defining module and is omitted. Rule 15 (dependency direction) consumes qualifiedReferences, so for every adapter that emits an incomplete set the prohibition silently under-fires (false negatives). Today only OCaml emits qualifiedReferences at all; Go's in-flight emission is syntactic, and the TypeScript and Swift adapters do not emit the now-required moduleName/qualifiedReferences fields, so their fact documents cannot even pass the schema.", + "solutionSummary": "Upgrade all four adapters to resolve identifiers semantically to their defining module/symbol and emit them as '.'. Go gains go/types type info (info.Uses) over its already-loaded packages; TypeScript gains a ts.Program + TypeChecker with real module resolution; OCaml consumes the Typedtree (.cmt) instead of the raw Parsetree; Swift consumes SourceKit/index semantic data. Each adapter is an independent track of an INFRA patch (wire in the semantic machinery or the empty-field schema conformance, a rule-15 no-op) followed by a BEHAVIOR patch (populate qualifiedReferences semantically). A final patch repoints the shell-to-core linkage rule onto qualifiedReferences. evaluate.py's rule 15 and fact.schema.json are unchanged baseline and already consume the qualified contract. The cross-cutting invariant every adapter must honour is prefix consistency: the qualifier written into a referrer's qualifiedReferences for a symbol must equal the moduleName that same adapter assigns to that symbol's owner.", + "currentStateAnalysis": "Baseline (assuming the uncommitted working-tree changes land): fact.schema.json requires moduleName and qualifiedReferences on every source file; evaluate.py's evaluate_dependency_direction builds an implementation surface as {module_name}.{decision_reference} over shell/state/exempt files and intersects it with each core/value file's qualified_references. OCaml emits moduleName (capitalised basename) and syntactic qualifiedReferences (multi-part longidents only) via record_longident. Go emits moduleName (package name) and syntactic qualifiedReferences (selector exprs whose base is an imported package name) and loads packages with NeedName|NeedFiles|NeedCompiledGoFiles|NeedImports|NeedSyntax (no NeedTypes). TypeScript parses each file standalone with ts.createSourceFile and emits neither field. Swift parses with SwiftSyntax and emits neither field. Positive rules (shell linkage, core coverage, state coverage) still match bare apiReferences. Target: all four adapters emit semantically-resolved qualifiedReferences so rule 15 is effective everywhere, TypeScript/Swift become schema-conformant, and the shell-to-core linkage rule is repointed at qualifiedReferences (Patch 9). Core property-test coverage and state coverage stay on bare apiReferences because their evidence (propertyChecks references and operation sequences) is emitted bare and is not made qualified by this gameplan.", + "operationalConsiderations": { + "externalSystemAccess": "No network services, object storage, DBs, or secret stores are added. The new dependency is on each language's type-checker / build toolchain being available wherever the adapter runs (local `just test` and the composite GitHub Action in action.yml). Go's BEHAVIOR patch (Patch 2) adds NeedTypes|NeedTypesInfo (and NeedDeps) to packages.Load, which requires the consumer module's dependencies to be resolvable (go.mod, module cache); the Go adapter already invokes packages.Load, so this is a mode escalation, not new access. TypeScript's Patch 6 needs ts.createProgram to resolve tsconfig + node_modules. OCaml's Patch 4 needs the consumer's compiled .cmt artifacts and load path (Patch 3 owns making dune emit bin_annot and discovering the build dir). Swift's Patch 8 needs SourceKit/index data (Patch 7 owns the toolchain wiring). Each adapter's BEHAVIOR patch owns bringing its toolchain capability online; the just setup recipes and action.yml install steps are the provisioning surface.", + "crossRuntimeContracts": "The fact JSON document (fact.schema.json) is the contract between the adapters (producers) and evaluate.py (consumer). This gameplan changes the *content* of qualifiedReferences (incomplete-syntactic to complete-semantic) but not the schema shape — moduleName and qualifiedReferences already exist in the baseline schema and evaluate.py already consumes them for rule 15, so the consumer needs no change. The format contract every producer must preserve is '.', where the prefix equals the owner file's own moduleName as that same adapter computes it (package name for Go; capitalised basename for OCaml/TypeScript/Swift). Producer and consumer both live in this repo; the consumer is unchanged, so there is no producer-ships-consumer-breaks skew.", + "failureBehavior": "Semantic resolution introduces new failure modes per adapter: Go packages.Load type errors / unresolved deps; TypeScript program creation failures or unresolved tsconfig/modules; OCaml missing or stale .cmt artifacts and load-path gaps; Swift SourceKit crashes or absent index. The adapters already surface load/parse errors as violations on stderr and exit 1 (Go's loadGoPackages collects package errors; the others abort the fixture run). The decision this gameplan commits to is fail-closed: when semantic information is unavailable the adapter must error rather than silently emit an empty/partial qualifiedReferences set, because a silent partial set would make rule 15 under-fire — reintroducing the exact false-negative hole this gameplan closes. Each BEHAVIOR patch must emit a violation (non-zero exit) when resolution cannot complete, not degrade quietly to syntactic output.", + "concurrencyAndIdempotency": "`just test` fans the four adapter suites out concurrently, but each adapter is a separate process that reads source and writes a fact document to stdout — a pure function of its inputs, idempotent across re-runs, with no shared mutable state between adapters. The new build/index artifacts (Go module cache, TypeScript program, OCaml _build with .cmt, Swift .build index) live in per-language directories and are not shared across suites, so parallel runs do not race. No new code path is entered under retry or concurrent write to a shared resource.", + "rollbackStrategy": "No persistent state, schema migration, or external mutation is introduced — the adapters are pure CLIs and the only artifacts are regenerable build caches (_build, .build, node_modules, the Go build cache). Reverting any adapter's BEHAVIOR patch reverts its qualifiedReferences to the prior (syntactic for Go/OCaml, empty for TypeScript/Swift) set, which only relaxes rule 15 for that adapter; reverting an INFRA patch removes the field/toolchain wiring. Because each track is independent and additive, a revert strands no data and leaves the other adapters unaffected." + }, + "mergabilityStrategy": { + "featureFlagStrategy": "No feature flag. The adapters have no flag infrastructure, and gating is structural instead: each adapter's INFRA patch is a provable rule-15 no-op (Go/OCaml: semantic machinery wired in but emitted facts unchanged; TypeScript/Swift: moduleName plus an empty qualifiedReferences array), and each adapter's BEHAVIOR patch activates semantic resolution only for that adapter's own facts. qualifiedReferences and module_name are consumed solely by rule 15, so an empty/unchanged set cannot alter any other rule. Every patch is therefore independently mergeable.", + "featureFlags": [], + "patchOrderingStrategy": "Four independent per-adapter tracks, each ordered INFRA (early) then BEHAVIOR (late); the tracks have no cross-dependencies and run in parallel. Within a track the BEHAVIOR patch depends only on its own INFRA patch." + }, + "requiredChanges": [ + { + "file": "go/main.go", + "line": 186, + "description": "Add NeedTypes|NeedTypesInfo (and NeedDeps) to the packages.Config mode so loaded packages carry types.Info.", + "signature": "Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedDeps" + }, + { + "file": "go/main.go", + "line": 224, + "description": "Compute file facts from each loaded package's syntax tree + TypesInfo (keyed by file path) instead of a standalone parser.ParseFile, so identifiers can be resolved.", + "signature": "func goArchitectureFileFactFromPackage(pkg *packages.Package, file *ast.File, path string) (architectureFileFact, []violation)" + }, + { + "file": "go/main.go", + "line": 322, + "description": "Replace syntactic goQualifiedReferences with a semantic resolver: walk idents, look up info.Uses[ident] -> types.Object, and emit obj.Pkg().Name()+\".\"+obj.Name() for cross-package references (skip same-package and local objects).", + "signature": "func goSemanticQualifiedReferences(file *ast.File, info *types.Info, selfPkg *types.Package) map[string]struct{}" + }, + { + "file": "go/main_test.go", + "line": 191, + "description": "Add a semantic test covering a bare/dot-imported use that resolves to a foreign package; keep TestGoModuleNameAndQualifiedReferences green.", + "signature": null + }, + { + "file": "ocaml/dune", + "line": 1, + "description": "Emit binary annotations (.cmt/.cmti) for the adapter and configure consumption of the consumer project's Typedtree.", + "signature": "(executable (name main) (libraries compiler-libs.common yojson) (flags (:standard -bin-annot)))" + }, + { + "file": "ocaml/main.ml", + "line": 271, + "description": "Resolve references from the Typedtree (Path.t / Env) to their defining module instead of recording multi-part longidents syntactically, populating qualified_references with open'd/bare uses included.", + "signature": "val record_typedtree_reference : facts -> Typedtree.expression -> unit" + }, + { + "file": "typescript/src/main.ts", + "line": 47, + "description": "Add moduleName and qualifiedReferences to SourceFact and the internal Facts type.", + "signature": "moduleName: string; qualifiedReferences: string[];" + }, + { + "file": "typescript/src/main.ts", + "line": 167, + "description": "Replace per-file ts.createSourceFile with a shared ts.Program + TypeChecker; resolve each reference's symbol to its declaring source file and emit '.'.", + "signature": "function buildProgram(filePaths: string[]): { program: ts.Program; checker: ts.TypeChecker }" + }, + { + "file": "swift/Sources/SwiftArchLint/main.swift", + "line": 110, + "description": "Add moduleName and qualifiedReferences to SourceFact (and the file-info struct).", + "signature": "let moduleName: String; let qualifiedReferences: [String]" + }, + { + "file": "swift/Sources/SwiftArchLint/main.swift", + "line": 285, + "description": "Resolve references via SourceKit/index semantic data to their declaring file and emit '.'.", + "signature": "static func semanticQualifiedReferences(for fileInfo: SwiftFileInfo, index: SemanticIndex) -> [String]" + } + ], + "functionalChanges": [ + { + "id": "FC-1", + "description": "The Go adapter loads go/types type information for the packages it analyses (packages.Load runs with NeedTypes|NeedTypesInfo) without changing emitted facts.", + "ownedBy": 1 + }, + { + "id": "FC-2", + "description": "The Go adapter's qualifiedReferences resolve cross-package and bare/dot-imported uses to their defining package via info.Uses, emitted as '.', so rule 15 fires for a genuine Go cross-module reach and ignores same-package locals and incidental bare names.", + "ownedBy": 2 + }, + { + "id": "FC-3", + "description": "The OCaml adapter's qualifiedReferences are resolved from the Typedtree, attributing uses written bare after open (or via the same package) to their true owning module rather than omitting them.", + "ownedBy": 4 + }, + { + "id": "FC-4", + "description": "The TypeScript adapter emits moduleName and a qualifiedReferences array, making its fact document schema-conformant (qualifiedReferences initially empty; rule-15 no-op).", + "ownedBy": 5 + }, + { + "id": "FC-5", + "description": "The TypeScript adapter populates qualifiedReferences by resolving each reference's symbol (via a ts.Program TypeChecker) to its declaring module, emitted as '.', so rule 15 fires for a genuine TypeScript cross-module reach including named-import bare uses.", + "ownedBy": 6 + }, + { + "id": "FC-6", + "description": "The Swift adapter emits moduleName and a qualifiedReferences array, making its fact document schema-conformant (qualifiedReferences initially empty; rule-15 no-op).", + "ownedBy": 7 + }, + { + "id": "FC-7", + "description": "The Swift adapter populates qualifiedReferences by resolving references via SourceKit/index semantic data to their declaring file, emitted as '.', so rule 15 fires for a genuine Swift cross-module reach.", + "ownedBy": 8 + }, + { + "id": "FC-8", + "description": "Each adapter fails closed (emits a violation and exits non-zero) when semantic resolution cannot complete, rather than silently emitting a partial/empty qualifiedReferences set that would make rule 15 under-fire.", + "ownedBy": 2 + }, + { + "id": "FC-9", + "description": "The shell-to-core linkage rule (evaluate_shell_modules) matches a shell module's qualifiedReferences against the same-domain core module's qualified decision surface ({coreModuleName}.{decisionSurface|decisionProducts}) instead of bare apiReferences, so an unrelated same-named bare function no longer satisfies the rule.", + "ownedBy": 9 + } + ], + "contextResources": [ + { + "id": "rule15-contract", + "kind": "contract", + "paths": [ + "evaluate.py" + ], + "why": "evaluate_dependency_direction builds the implementation surface as {module_name}.{decision_reference} over shell/state/exempt files and intersects it with each core/value file's qualified_references. Every adapter must emit moduleName and qualifiedReferences in the exact '.' form, with the qualifier equal to the owner's moduleName, or the intersection silently never matches.", + "consumedBy": [ + 2, + 4, + 6, + 8, + 9 + ] + }, + { + "id": "fact-schema", + "kind": "contract", + "paths": [ + "fact.schema.json" + ], + "why": "Defines the required moduleName (string) and qualifiedReferences (unique non-empty string list) fields on every source file. TypeScript and Swift must add these fields to become schema-conformant.", + "consumedBy": [ + 5, + 7 + ] + }, + { + "id": "ocaml-syntactic-impl", + "kind": "existing-implementation", + "paths": [ + "ocaml/main.ml" + ], + "why": "record_longident plus module_name_of_path show the current syntactic qualifiedReferences/moduleName derivation (capitalised basename; multi-part longidents only). Patch 4 replaces the syntactic qualified logic with Typedtree resolution; other adapters' BEHAVIOR patches use this as the reference exemplar of the qualified format.", + "consumedBy": [ + 2, + 4, + 6, + 8 + ] + }, + { + "id": "go-syntactic-impl", + "kind": "existing-implementation", + "paths": [ + "go/main.go" + ], + "why": "loadGoPackages, goModuleName, goQualifiedReferences, and goArchitectureFileFact show how packages are loaded and how the syntactic qualified set and package-name moduleName are computed today. The Go track escalates the load mode and replaces the syntactic resolver with a types.Info-based one.", + "consumedBy": [ + 1, + 2 + ] + }, + { + "id": "rule15-test-fixtures", + "kind": "test-or-static-check", + "paths": [ + "evaluate_test.py", + "go/main_test.go" + ], + "why": "The test_dependency_direction_* cases pin the expected '.' format and the false-positive class (a `t` type and a bare `send` must not flag); go/main_test.go's TestGoModuleNameAndQualifiedReferences pins package-name prefixing and exclusion of locals. BEHAVIOR patches must keep these green and add semantic cases; Patch 9 mirrors the surface construction for shell linkage.", + "consumedBy": [ + 2, + 4, + 6, + 8, + 9 + ] + } + ], + "acceptanceCriteria": [ + "All four adapters emit moduleName and qualifiedReferences and their fact documents validate against fact.schema.json (`just test` passes every suite).", + "For every adapter, each entry in qualifiedReferences has the form '.' where ownerModuleName is exactly the moduleName that adapter assigns to the symbol's defining module within the same language universe.", + "For each adapter, rule 15 (dependency direction) fires for a planted cross-module reach written bare after open/import (or, for Go, cross-package), proving semantic resolution closes the syntactic hole.", + "For each adapter, rule 15 does NOT fire on an incidental shared bare name (e.g. a `t` type or a `send` local), preserving the false-positive avoidance that motivated qualified matching.", + "When semantic resolution cannot complete (type errors, missing build/index artifacts), the adapter emits a violation and exits non-zero rather than emitting a partial/empty qualifiedReferences set.", + "Shell-to-core linkage (evaluate_shell_modules) matches a shell module's qualifiedReferences against the same-domain core module's qualified decision surface, so a planted unrelated same-named bare function no longer satisfies the rule while a genuine open'd/imported/cross-package core call still does.", + "Core property-test coverage and state coverage continue to match bare apiReferences (their propertyChecks/operation-sequence evidence is not made qualified by this gameplan)." + ], + "openQuestions": [], + "explicitOpinions": [ + { + "opinion": "Each adapter is split into an INFRA patch (a provable rule-15 no-op) and a BEHAVIOR patch (semantic population), rather than a single patch per adapter.", + "rationale": "It maximises INFRA surface and keeps each BEHAVIOR patch minimal and independently reviewable; the INFRA patch also lets TypeScript/Swift reach schema conformance without yet committing to semantic behaviour." + }, + { + "opinion": "Adapters must fail closed when semantic resolution is unavailable.", + "rationale": "A silently empty/partial qualifiedReferences set turns rule 15 into a no-op (false negatives) — the precise hole issue 6 exists to close — so degrading to syntactic output quietly would defeat the gameplan's purpose." + }, + { + "opinion": "Unify the shell-to-core linkage rule onto qualifiedReferences (Patch 9), but leave core property-test coverage and state coverage on bare apiReferences.", + "rationale": "Resolved with the user: positive rules should be made precise. Only shell linkage matches file-level apiReferences and can be repointed with the qualified data this gameplan produces; core property-test coverage and state coverage match propertyChecks/operation-sequence references, which the schema emits bare. Unifying those would require adapters to emit qualified property references (a schema + every-adapter extraction change) and is deferred as a documented follow-on. Patch 9 depends on every adapter's semantic BEHAVIOR patch so a legitimately-linked shell is not regressed." + }, + { + "opinion": "Swift resolves references via IndexStoreDB (swiftlang/indexstore-db) over a built index store.", + "rationale": "Resolved with the user: IndexStoreDB gives the most precise occurrence-to-definition mapping (USR-based) across files, at the cost of a build+index step that Patch 7 owns. SourceKitten/SourceKit-LSP were the lighter but less complete alternatives." + }, + { + "opinion": "The OCaml adapter drives a dune build of the consumer project (with bin-annot) to produce .cmt under _build, then reads the Typedtree.", + "rationale": "Resolved with the user: this keeps the adapter self-contained rather than pushing build responsibility onto the caller/CI, at the cost of a larger Patch 3 ('build the world') that owns invoking dune and discovering the load path." + }, + { + "opinion": "Go emits only cross-package references in qualifiedReferences; same-package and local objects are excluded.", + "rationale": "Resolved with the user: obj.Pkg() != selfPkg gating matches the existing TestGoModuleNameAndQualifiedReferences expectations (a bare `send` local and same-package type `t` must not surface) and keeps rule 15 focused on genuine cross-module reaches." + }, + { + "opinion": "OCaml and Swift semantic resolution are included as committed complexity-3 tracks despite being 'evaluate feasibility' items in the issue.", + "rationale": "The user directed a single gameplan with four parallel adapter tracks rather than a multi-milestone workstream; the feasibility/effort risk is recorded here rather than silently resolved, and the tracks are independent so a stall in one does not block reviewing the others (though atomic merge still requires all)." + } + ], + "patches": [ + { + "number": 1, + "classification": "INFRA", + "complexity": 2, + "title": "Go: load go/types type information (no fact change)", + "files": [ + { + "path": "go/main.go", + "action": "modify", + "description": "Add NeedTypes|NeedTypesInfo|NeedDeps to the packages.Config mode; thread each loaded package's *ast.File + types.Info into file-fact construction (keyed by file path) while still emitting the existing syntactic qualifiedReferences." + }, + { + "path": "go/main_test.go", + "action": "modify", + "description": "Add a skipped semantic test TestGoSemanticQualifiedReferencesResolveBareUses (t.Skip(\"PENDING: Patch 2\")) documenting the expected resolution of a dot-imported/bare cross-package use." + } + ], + "changes": [ + "In loadGoPackages, extend Mode with packages.NeedTypes | packages.NeedTypesInfo | packages.NeedDeps.", + "Refactor fact construction so file facts are built from the loaded package's Syntax + TypesInfo (mapping CompiledGoFiles/GoFiles to *ast.File), keeping the standalone parser.ParseFile path only as a fallback; emitted facts are byte-for-byte unchanged this patch (still goQualifiedReferences).", + "Add go/main_test.go test TestGoSemanticQualifiedReferencesResolveBareUses with t.Skip(\"PENDING: Patch 2\") describing a fixture where a symbol is dot-imported and used bare and must resolve to '.Symbol'." + ], + "requiredContext": [ + "go-syntactic-impl" + ], + "testStubsIntroduced": [ + "go: TestGoSemanticQualifiedReferencesResolveBareUses" + ], + "testStubsImplemented": null, + "spec": "module SEMANTIC_REFERENCE_RESOLUTION_PATCH_1.\n\n> Patch 1 wires Go go/packages type semantic infrastructure (type/index information)\n> into the adapter without changing emitted facts. qualifiedReferences\n> for Go go/packages type remain exactly the prior set, so rule 15 behaviour is unchanged.\n\nSourceFile.\nModule.\nSymbol.\nModuleType.\nDomain.\n\nmodule-name f: SourceFile => Module.\nmodule-type f: SourceFile => ModuleType.\ndomain f: SourceFile => Domain.\nowner s: Symbol => Module.\nreferences f: SourceFile, s: Symbol => Bool.\nqualified-reference f: SourceFile, m: Module, s: Symbol => Bool.\ndecision-reference f: SourceFile, s: Symbol => Bool.\n---\n> No behavioural delta; the contribution is the available semantic data.\ntrue.\n", + "precedents": [ + { + "kind": "documentation", + "name": "golang.org/x/tools/go/packages (NeedTypes/NeedTypesInfo)", + "url": "https://pkg.go.dev/golang.org/x/tools/go/packages", + "whyApplicable": "Defines the LoadMode bits; NeedTypes and NeedTypesInfo populate Package.Types and Package.TypesInfo, and NeedDeps is required for cross-package object resolution. Use these to make info.Uses populated in Patch 2." + } + ] + }, + { + "number": 2, + "classification": "BEHAVIOR", + "complexity": 3, + "title": "Go: semantic qualifiedReferences via info.Uses", + "files": [ + { + "path": "go/main.go", + "action": "modify", + "description": "Replace syntactic goQualifiedReferences with goSemanticQualifiedReferences resolving idents via info.Uses[ident] -> types.Object, emitting obj.Pkg().Name()+\".\"+obj.Name() for cross-package objects and skipping same-package/local objects; fail closed if TypesInfo is unavailable." + }, + { + "path": "go/main_test.go", + "action": "modify", + "description": "Implement and unskip TestGoSemanticQualifiedReferencesResolveBareUses; keep TestGoModuleNameAndQualifiedReferences green." + } + ], + "changes": [ + "Implement goSemanticQualifiedReferences(file *ast.File, info *types.Info, selfPkg *types.Package): for each *ast.Ident with an entry in info.Uses, take the types.Object; if obj.Pkg() != nil and obj.Pkg() != selfPkg, emit obj.Pkg().Name()+\".\"+obj.Name().", + "Skip objects whose Pkg() is nil (builtins) or equals selfPkg (same-package), and local vars (their Pkg is selfPkg), so a bare local `send` and a same-package type `t` never surface — matching TestGoModuleNameAndQualifiedReferences.", + "Wire goSemanticQualifiedReferences into file-fact construction using the package TypesInfo threaded in Patch 1; remove the syntactic goQualifiedReferences (or keep it unused/deleted).", + "Fail closed: if a file has no resolvable TypesInfo (load produced type errors), append a violation and do not emit a fact for that file (consistent with existing load-error handling).", + "Unskip and implement the Patch 1 stub test; assert the dot-imported bare use resolves to '.Symbol' and that locals/same-package names are absent." + ], + "requiredContext": [ + "rule15-contract", + "go-syntactic-impl", + "ocaml-syntactic-impl", + "rule15-test-fixtures" + ], + "testStubsIntroduced": null, + "testStubsImplemented": [ + "go: TestGoSemanticQualifiedReferencesResolveBareUses" + ], + "spec": "module SEMANTIC_REFERENCE_RESOLUTION_PATCH_2.\n\n> Patch 2 makes the Go adapter populate qualifiedReferences by semantic\n> resolution: each referenced symbol is resolved to its defining module\n> and emitted as owner-module.symbol, including bare/open'd/imported and\n> (for Go) cross-package uses. Same-module and local references are not\n> emitted. After this patch rule 15 fires for a genuine Go cross-module\n> reach and ignores incidental shared bare names.\n\nSourceFile.\nModule.\nSymbol.\nModuleType.\nDomain.\n\nmodule-name f: SourceFile => Module.\nmodule-type f: SourceFile => ModuleType.\ndomain f: SourceFile => Domain.\nowner s: Symbol => Module.\nreferences f: SourceFile, s: Symbol => Bool.\nqualified-reference f: SourceFile, m: Module, s: Symbol => Bool.\ndecision-reference f: SourceFile, s: Symbol => Bool.\n---\n> Semantic completeness for Go: foreign-owned references are qualified\n> by their true owner even when written bare.\nall f: SourceFile, s: Symbol, references f s and owner s ~= module-name f | qualified-reference f (owner s) s.\n> Prefix consistency: the qualifier equals the owner module's module-name.\nall f: SourceFile, m: Module, s: Symbol, qualified-reference f m s | m = owner s.\n", + "precedents": [ + { + "kind": "documentation", + "name": "go/types types.Info.Uses", + "url": "https://pkg.go.dev/go/types#Info", + "whyApplicable": "Info.Uses maps each identifier use to the types.Object it denotes; obj.Pkg() gives the defining package. This is the exact API the issue cites ('info.Uses[ident] -> types.Object') for resolving bare/dot-imported uses to their owner." + } + ] + }, + { + "number": 3, + "classification": "INFRA", + "complexity": 2, + "title": "OCaml: emit and consume Typedtree (.cmt) infrastructure", + "files": [ + { + "path": "ocaml/dune", + "action": "modify", + "description": "Add -bin-annot so the adapter build emits .cmt/.cmti, and add the cmt_format-capable libraries; no change to emitted facts." + }, + { + "path": "ocaml/main.ml", + "action": "modify", + "description": "Add logic to drive a dune build of the consumer project (with bin-annot) producing .cmt under _build, plus Cmt_format.read_cmt loading and load-path discovery, gated so the existing syntactic record_longident output is unchanged this patch." + }, + { + "path": "ocaml/test.sh", + "action": "modify", + "description": "Add a PENDING-marked fixture for an open'd bare cross-module use (assertion guarded by a 'PENDING: Patch 4' marker)." + } + ], + "changes": [ + "Update ocaml/dune to (flags (:standard -bin-annot)) so Typedtree annotations are produced for the adapter, and add any dune dependency needed to invoke a consumer build.", + "Add logic to drive a dune build of the consumer project (the source roots passed to the adapter) so .cmt artifacts exist under the consumer's _build, then discover the load path and read .cmt via Cmt_format.read_cmt; do not yet use the Typedtree output (emitted facts remain the syntactic set).", + "Add a PENDING fixture+assertion to ocaml/test.sh (a core that uses an open'd shell symbol bare) marked '# PENDING: Patch 4', not yet asserted." + ], + "requiredContext": [], + "testStubsIntroduced": [ + "ocaml: open'd bare reference resolves to owner module" + ], + "testStubsImplemented": null, + "spec": "module SEMANTIC_REFERENCE_RESOLUTION_PATCH_3.\n\n> Patch 3 wires OCaml Typedtree (.cmt) semantic infrastructure (type/index information)\n> into the adapter without changing emitted facts. qualifiedReferences\n> for OCaml Typedtree (.cmt) remain exactly the prior set, so rule 15 behaviour is unchanged.\n\nSourceFile.\nModule.\nSymbol.\nModuleType.\nDomain.\n\nmodule-name f: SourceFile => Module.\nmodule-type f: SourceFile => ModuleType.\ndomain f: SourceFile => Domain.\nowner s: Symbol => Module.\nreferences f: SourceFile, s: Symbol => Bool.\nqualified-reference f: SourceFile, m: Module, s: Symbol => Bool.\ndecision-reference f: SourceFile, s: Symbol => Bool.\n---\n> No behavioural delta; the contribution is the available semantic data.\ntrue.\n", + "precedents": [ + { + "kind": "library", + "name": "OCaml compiler-libs Cmt_format / Typedtree", + "url": null, + "whyApplicable": "Cmt_format.read_cmt reads the binary annotations produced by -bin-annot, giving the Typedtree whose Path.t nodes carry resolved module paths. This is the mechanism the issue names for attributing open'd/bare uses to their owner." + }, + { + "kind": "documentation", + "name": "Merlin / ocaml-index (Typedtree-based name resolution)", + "url": "https://github.com/ocaml/merlin", + "whyApplicable": "Merlin and ocaml-index resolve identifiers to their definitions over the Typedtree and the load path; their approach to mapping a Path.t to a defining compilation unit is the model for record_typedtree_reference in Patch 4." + } + ] + }, + { + "number": 4, + "classification": "BEHAVIOR", + "complexity": 3, + "title": "OCaml: resolve qualifiedReferences from the Typedtree", + "files": [ + { + "path": "ocaml/main.ml", + "action": "modify", + "description": "Walk the Typedtree, resolve each reference's Path.t/Env to its defining module, and populate qualified_references including open'd/bare uses; replace the syntactic multi-part-longident logic in record_longident." + }, + { + "path": "ocaml/test.sh", + "action": "modify", + "description": "Enable and assert the Patch 3 PENDING fixture: the open'd bare cross-module use surfaces as 'Owner.symbol'." + } + ], + "changes": [ + "Implement record_typedtree_reference: for each resolved Path.t, derive the owning module (compilation unit) and emit '.' into qualified_references, including identifiers used bare after open.", + "Keep moduleName = capitalised basename; ensure the owner-module name derived for a reference matches the owner file's moduleName (prefix consistency).", + "Fail closed: if the .cmt/Typedtree for a source file is unavailable, emit a violation and exit non-zero rather than falling back to the syntactic set.", + "Enable the Patch 3 PENDING assertion and verify the open'd bare use resolves; verify an incidental shared bare name does not surface." + ], + "requiredContext": [ + "rule15-contract", + "ocaml-syntactic-impl", + "rule15-test-fixtures" + ], + "testStubsIntroduced": null, + "testStubsImplemented": [ + "ocaml: open'd bare reference resolves to owner module" + ], + "spec": "module SEMANTIC_REFERENCE_RESOLUTION_PATCH_4.\n\n> Patch 4 makes the OCaml adapter populate qualifiedReferences by semantic\n> resolution: each referenced symbol is resolved to its defining module\n> and emitted as owner-module.symbol, including bare/open'd/imported and\n> (for Go) cross-package uses. Same-module and local references are not\n> emitted. After this patch rule 15 fires for a genuine OCaml cross-module\n> reach and ignores incidental shared bare names.\n\nSourceFile.\nModule.\nSymbol.\nModuleType.\nDomain.\n\nmodule-name f: SourceFile => Module.\nmodule-type f: SourceFile => ModuleType.\ndomain f: SourceFile => Domain.\nowner s: Symbol => Module.\nreferences f: SourceFile, s: Symbol => Bool.\nqualified-reference f: SourceFile, m: Module, s: Symbol => Bool.\ndecision-reference f: SourceFile, s: Symbol => Bool.\n---\n> Semantic completeness for OCaml: foreign-owned references are qualified\n> by their true owner even when written bare.\nall f: SourceFile, s: Symbol, references f s and owner s ~= module-name f | qualified-reference f (owner s) s.\n> Prefix consistency: the qualifier equals the owner module's module-name.\nall f: SourceFile, m: Module, s: Symbol, qualified-reference f m s | m = owner s.\n", + "precedents": [ + { + "kind": "library", + "name": "OCaml compiler-libs Typedtree / Path", + "url": null, + "whyApplicable": "Typedtree expression nodes carry a resolved Path.t whose head identifies the defining module; walk the typed AST (Tast_iterator) and map each Path to its owning compilation unit to build qualified references." + } + ] + }, + { + "number": 5, + "classification": "INFRA", + "complexity": 2, + "title": "TypeScript: emit moduleName + empty qualifiedReferences (schema conformance)", + "files": [ + { + "path": "typescript/src/main.ts", + "action": "modify", + "description": "Add moduleName (capitalised file basename) and qualifiedReferences (empty array) to SourceFact and the Facts type; emit them so the fact document validates." + }, + { + "path": "typescript/test.sh", + "action": "modify", + "description": "Add a PENDING-marked fixture for a named-import bare use (assertion guarded by 'PENDING: Patch 6')." + } + ], + "changes": [ + "Add moduleName: string and qualifiedReferences: string[] to the SourceFact interface and internal Facts; compute moduleName from the file basename (capitalised), qualifiedReferences = [].", + "Emit both fields in sourceFact so the TypeScript fact document passes fact.schema.json (rule-15 no-op while empty).", + "Add a PENDING fixture+assertion to typescript/test.sh using node:test test.skip semantics or a '# PENDING: Patch 6' marker describing a core importing a shell symbol via a named import and using it bare." + ], + "requiredContext": [ + "fact-schema" + ], + "testStubsIntroduced": [ + "typescript: named-import bare reference resolves to owner module" + ], + "testStubsImplemented": null, + "spec": "module SEMANTIC_REFERENCE_RESOLUTION_PATCH_5.\n\n> Patch 5 brings the TypeScript adapter onto the schema contract: it emits\n> moduleName and an (initially empty) qualifiedReferences array. An\n> empty qualifiedReferences set is a rule-15 no-op, so observable policy\n> behaviour is unchanged while the adapter becomes schema-conformant.\n\nSourceFile.\nModule.\nSymbol.\nModuleType.\nDomain.\n\nmodule-name f: SourceFile => Module.\nmodule-type f: SourceFile => ModuleType.\ndomain f: SourceFile => Domain.\nowner s: Symbol => Module.\nreferences f: SourceFile, s: Symbol => Bool.\nqualified-reference f: SourceFile, m: Module, s: Symbol => Bool.\ndecision-reference f: SourceFile, s: Symbol => Bool.\n---\n> No qualified references are emitted yet, so rule 15 is a no-op for the\n> TypeScript adapter; the contribution is the moduleName field and the field shape.\nall f: SourceFile, m: Module, s: Symbol | ~ qualified-reference f m s.\n", + "precedents": null + }, + { + "number": 6, + "classification": "BEHAVIOR", + "complexity": 3, + "title": "TypeScript: semantic qualifiedReferences via Program + TypeChecker", + "files": [ + { + "path": "typescript/src/main.ts", + "action": "modify", + "description": "Build a ts.Program + TypeChecker with real tsconfig/module resolution; resolve each reference's symbol to its declaring source file and emit '.'; replace per-file ts.createSourceFile." + }, + { + "path": "typescript/test.sh", + "action": "modify", + "description": "Enable and assert the Patch 5 PENDING fixture: the named-import bare use surfaces as 'Owner.symbol'." + } + ], + "changes": [ + "Replace per-file ts.createSourceFile with buildProgram(filePaths): create a ts.Program (reading tsconfig, resolving node_modules) and obtain getTypeChecker().", + "For each reference identifier, checker.getSymbolAtLocation -> resolve aliases (getAliasedSymbol) -> declaration's SourceFile -> owner moduleName (capitalised basename); emit '.' for symbols owned by another module; skip same-file/local symbols.", + "Ensure the owner moduleName derivation matches the owner file's emitted moduleName (prefix consistency).", + "Fail closed: if program creation or symbol resolution fails, emit a violation and exit non-zero rather than emitting empty qualifiedReferences.", + "Enable the Patch 5 PENDING assertion; verify the named-import bare use resolves and that a same-file local name does not surface." + ], + "requiredContext": [ + "rule15-contract", + "ocaml-syntactic-impl", + "rule15-test-fixtures" + ], + "testStubsIntroduced": null, + "testStubsImplemented": [ + "typescript: named-import bare reference resolves to owner module" + ], + "spec": "module SEMANTIC_REFERENCE_RESOLUTION_PATCH_6.\n\n> Patch 6 makes the TypeScript adapter populate qualifiedReferences by semantic\n> resolution: each referenced symbol is resolved to its defining module\n> and emitted as owner-module.symbol, including bare/open'd/imported and\n> (for Go) cross-package uses. Same-module and local references are not\n> emitted. After this patch rule 15 fires for a genuine TypeScript cross-module\n> reach and ignores incidental shared bare names.\n\nSourceFile.\nModule.\nSymbol.\nModuleType.\nDomain.\n\nmodule-name f: SourceFile => Module.\nmodule-type f: SourceFile => ModuleType.\ndomain f: SourceFile => Domain.\nowner s: Symbol => Module.\nreferences f: SourceFile, s: Symbol => Bool.\nqualified-reference f: SourceFile, m: Module, s: Symbol => Bool.\ndecision-reference f: SourceFile, s: Symbol => Bool.\n---\n> Semantic completeness for TypeScript: foreign-owned references are qualified\n> by their true owner even when written bare.\nall f: SourceFile, s: Symbol, references f s and owner s ~= module-name f | qualified-reference f (owner s) s.\n> Prefix consistency: the qualifier equals the owner module's module-name.\nall f: SourceFile, m: Module, s: Symbol, qualified-reference f m s | m = owner s.\n", + "precedents": [ + { + "kind": "documentation", + "name": "TypeScript Compiler API (Program, TypeChecker, getSymbolAtLocation)", + "url": "https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API", + "whyApplicable": "createProgram + getTypeChecker + getSymbolAtLocation/getAliasedSymbol resolve an identifier (including a named import used bare) to its declaring symbol and source file, which is exactly the resolution the issue prescribes for TypeScript." + } + ] + }, + { + "number": 7, + "classification": "INFRA", + "complexity": 2, + "title": "Swift: emit moduleName + empty qualifiedReferences (schema conformance)", + "files": [ + { + "path": "swift/Sources/SwiftArchLint/main.swift", + "action": "modify", + "description": "Add moduleName (capitalised file basename) and qualifiedReferences (empty array) to SourceFact and the file-info struct; emit them so the fact document validates." + }, + { + "path": "swift/test.sh", + "action": "modify", + "description": "Add a PENDING-marked fixture for a bare cross-file reference (assertion guarded by 'PENDING: Patch 8')." + } + ], + "changes": [ + "Add moduleName: String and qualifiedReferences: [String] to SourceFact (and the SwiftFileInfo struct); compute moduleName from the file basename (capitalised), qualifiedReferences = [].", + "Emit both fields in sourceFact so the Swift fact document passes fact.schema.json (rule-15 no-op while empty).", + "Add a PENDING fixture+assertion to swift/test.sh marked '# PENDING: Patch 8' describing a core referencing a shell symbol declared in another file." + ], + "requiredContext": [ + "fact-schema" + ], + "testStubsIntroduced": [ + "swift: cross-file bare reference resolves to owner module" + ], + "testStubsImplemented": null, + "spec": "module SEMANTIC_REFERENCE_RESOLUTION_PATCH_7.\n\n> Patch 7 brings the Swift adapter onto the schema contract: it emits\n> moduleName and an (initially empty) qualifiedReferences array. An\n> empty qualifiedReferences set is a rule-15 no-op, so observable policy\n> behaviour is unchanged while the adapter becomes schema-conformant.\n\nSourceFile.\nModule.\nSymbol.\nModuleType.\nDomain.\n\nmodule-name f: SourceFile => Module.\nmodule-type f: SourceFile => ModuleType.\ndomain f: SourceFile => Domain.\nowner s: Symbol => Module.\nreferences f: SourceFile, s: Symbol => Bool.\nqualified-reference f: SourceFile, m: Module, s: Symbol => Bool.\ndecision-reference f: SourceFile, s: Symbol => Bool.\n---\n> No qualified references are emitted yet, so rule 15 is a no-op for the\n> Swift adapter; the contribution is the moduleName field and the field shape.\nall f: SourceFile, m: Module, s: Symbol | ~ qualified-reference f m s.\n", + "precedents": null + }, + { + "number": 8, + "classification": "BEHAVIOR", + "complexity": 3, + "title": "Swift: semantic qualifiedReferences via IndexStoreDB", + "files": [ + { + "path": "swift/Sources/SwiftArchLint/main.swift", + "action": "modify", + "description": "Build an index store for the target and read it with IndexStoreDB to map each reference occurrence to its defining file/USR; emit '.' and populate qualifiedReferences." + }, + { + "path": "swift/Package.swift", + "action": "modify", + "description": "Add the swiftlang/indexstore-db package dependency." + }, + { + "path": "swift/test.sh", + "action": "modify", + "description": "Enable and assert the Patch 7 PENDING fixture: the cross-file bare reference surfaces as 'Owner.symbol'." + } + ], + "changes": [ + "Produce an index store for the analysed sources (build with -index-store-path) and open it via IndexStoreDB.", + "For each reference occurrence, look up its definition's file via the index, derive the owner moduleName (capitalised basename), and emit '.' for cross-file references; skip same-file/local symbols.", + "Ensure prefix consistency: the owner moduleName matches the owner file's emitted moduleName.", + "Fail closed: if the index build or IndexStoreDB query fails or is unavailable, emit a violation and exit non-zero rather than emitting empty qualifiedReferences.", + "Enable the Patch 7 PENDING assertion; verify the cross-file reference resolves and an incidental shared name does not surface." + ], + "requiredContext": [ + "rule15-contract", + "ocaml-syntactic-impl", + "rule15-test-fixtures" + ], + "testStubsIntroduced": null, + "testStubsImplemented": [ + "swift: cross-file bare reference resolves to owner module" + ], + "spec": "module SEMANTIC_REFERENCE_RESOLUTION_PATCH_8.\n\n> Patch 8 makes the Swift adapter populate qualifiedReferences by semantic\n> resolution: each referenced symbol is resolved to its defining module\n> and emitted as owner-module.symbol, including bare/open'd/imported and\n> (for Go) cross-package uses. Same-module and local references are not\n> emitted. After this patch rule 15 fires for a genuine Swift cross-module\n> reach and ignores incidental shared bare names.\n\nSourceFile.\nModule.\nSymbol.\nModuleType.\nDomain.\n\nmodule-name f: SourceFile => Module.\nmodule-type f: SourceFile => ModuleType.\ndomain f: SourceFile => Domain.\nowner s: Symbol => Module.\nreferences f: SourceFile, s: Symbol => Bool.\nqualified-reference f: SourceFile, m: Module, s: Symbol => Bool.\ndecision-reference f: SourceFile, s: Symbol => Bool.\n---\n> Semantic completeness for Swift: foreign-owned references are qualified\n> by their true owner even when written bare.\nall f: SourceFile, s: Symbol, references f s and owner s ~= module-name f | qualified-reference f (owner s) s.\n> Prefix consistency: the qualifier equals the owner module's module-name.\nall f: SourceFile, m: Module, s: Symbol, qualified-reference f m s | m = owner s.\n", + "precedents": [ + { + "kind": "library", + "name": "IndexStoreDB (swiftlang/indexstore-db)", + "url": "https://github.com/swiftlang/indexstore-db", + "whyApplicable": "IndexStoreDB reads the compiler's index store (produced with -index-store-path) to map a symbol occurrence to its defining file/USR — the semantic resolution Swift lacks from SwiftSyntax alone. Use its occurrence/symbol queries to find each reference's declaring file." + } + ] + }, + { + "number": 9, + "classification": "BEHAVIOR", + "complexity": 3, + "title": "Unify shell-to-core linkage onto qualifiedReferences", + "files": [ + { + "path": "evaluate.py", + "action": "modify", + "description": "Change evaluate_shell_modules to build the core handler surface as {core.module_name}.{decision_surface|decision_products} and intersect it with the shell file's qualified_references instead of bare api_references." + }, + { + "path": "evaluate_test.py", + "action": "modify", + "description": "Update the shell-linkage tests to the qualified form and add a case proving an unrelated same-named bare function no longer satisfies the rule while a qualified core call does." + } + ], + "changes": [ + "In evaluate_shell_modules, build core_handler_references_by_domain as the set of f'{core.module_name}.{s}' for s in (decision_surface | decision_products) over core files with a module_name.", + "Flag a shell module when its qualified_references does NOT intersect that same-domain qualified surface (mirroring rule 15's surface construction, but as a positive requirement).", + "Leave evaluate_core_modules and evaluate_state_modules unchanged (they match propertyChecks/operation-sequence evidence emitted bare).", + "Update evaluate_test.py shell-linkage cases to the qualified surface; add a regression test where a shell shares a bare name with core but holds no qualified reference and is therefore (correctly) flagged, and a positive case where a qualified core reference satisfies the rule." + ], + "requiredContext": [ + "rule15-contract", + "rule15-test-fixtures" + ], + "testStubsIntroduced": null, + "testStubsImplemented": null, + "spec": "module SEMANTIC_REFERENCE_RESOLUTION_PATCH_9.\n\n> Patch 9 unifies the shell-to-core linkage rule onto qualifiedReferences:\n> a shell module satisfies the linkage requirement only via a qualified\n> reference to a same-domain core module's declared decision surface, so an\n> unrelated same-named bare function no longer satisfies it. It depends on\n> every adapter's semantic BEHAVIOR patch so a legitimately-linked shell\n> (calling a core API via open / import / cross-package) is not regressed.\n\nSourceFile.\nModule.\nSymbol.\nModuleType.\nDomain.\n\nmodule-name f: SourceFile => Module.\nmodule-type f: SourceFile => ModuleType.\ndomain f: SourceFile => Domain.\nowner s: Symbol => Module.\nreferences f: SourceFile, s: Symbol => Bool.\nqualified-reference f: SourceFile, m: Module, s: Symbol => Bool.\ndecision-reference f: SourceFile, s: Symbol => Bool.\ndecision-surface? f: SourceFile, s: Symbol => Bool.\nshell? f: SourceFile => Bool.\ncore? f: SourceFile => Bool.\nshell-linked? f: SourceFile => Bool.\n---\n> A shell module is linked iff it qualified-references a same-domain core\n> module's declared decision surface.\nall f: SourceFile, shell? f and shell-linked? f | some c: SourceFile, s: Symbol | core? c and domain c = domain f and decision-surface? c s and qualified-reference f (module-name c) s.\n", + "precedents": null + } + ], + "testMap": [ + { + "testName": "go: TestGoSemanticQualifiedReferencesResolveBareUses", + "file": "go/main_test.go", + "stubPatch": 1, + "implPatch": 2 + }, + { + "testName": "ocaml: open'd bare reference resolves to owner module", + "file": "ocaml/test.sh", + "stubPatch": 3, + "implPatch": 4 + }, + { + "testName": "typescript: named-import bare reference resolves to owner module", + "file": "typescript/test.sh", + "stubPatch": 5, + "implPatch": 6 + }, + { + "testName": "swift: cross-file bare reference resolves to owner module", + "file": "swift/test.sh", + "stubPatch": 7, + "implPatch": 8 + } + ], + "dependencyGraph": [ + { + "patch": 1, + "classification": "INFRA", + "dependsOn": [] + }, + { + "patch": 2, + "classification": "BEHAVIOR", + "dependsOn": [ + 1 + ] + }, + { + "patch": 3, + "classification": "INFRA", + "dependsOn": [] + }, + { + "patch": 4, + "classification": "BEHAVIOR", + "dependsOn": [ + 3 + ] + }, + { + "patch": 5, + "classification": "INFRA", + "dependsOn": [] + }, + { + "patch": 6, + "classification": "BEHAVIOR", + "dependsOn": [ + 5 + ] + }, + { + "patch": 7, + "classification": "INFRA", + "dependsOn": [] + }, + { + "patch": 8, + "classification": "BEHAVIOR", + "dependsOn": [ + 7 + ] + }, + { + "patch": 9, + "classification": "BEHAVIOR", + "dependsOn": [ + 2, + 4, + 6, + 8 + ] + } + ], + "mergabilityChecklist": { + "featureFlagStrategyDocumented": true, + "earlyPatchesNonFunctional": true, + "testStubsInInfraPatches": true, + "testImplsColocated": true, + "testMapComplete": true, + "testMapImplPatchMatchesCode": true, + "behaviorPatchesMinimal": true, + "dependencyGraphOrdered": true, + "behaviorPatchesJustified": true, + "functionalChangesOwnedByExactlyOnePatch": true, + "operationalConsiderationsSubstantive": true, + "gameplanIsAtomicAndAutonomous": true + }, + "mergabilityInsight": "4 of 9 patches are INFRA (rule-15 no-ops): Go/OCaml wire in semantic machinery with unchanged output, and TypeScript/Swift reach schema conformance with an empty qualifiedReferences set. Of the 5 BEHAVIOR patches, four each sharpen exactly one adapter's own facts (independently mergeable), and the fifth (Patch 9) repoints shell linkage onto qualifiedReferences and so joins on all four semantic patches. The gameplan is atomic and autonomous: no patch is conditional on another's runtime outcome and no human action is required between patches; all design choices were resolved up front, so openQuestions is empty before execution begins.", + "finalStateSpec": "module SEMANTIC_REFERENCE_RESOLUTION.\n\n> ══════════════════════════════════════════\n> THE GUARANTEE\n> ══════════════════════════════════════════\n> After this gameplan all four adapters (Go, OCaml, TypeScript,\n> Swift) resolve references semantically. Every reference to a\n> symbol owned by another module in the same language universe is\n> emitted in qualifiedReferences as \"owner-module.symbol\", including\n> uses written bare after open/import or within the same package.\n> The dependency-direction rule (rule 15) therefore fires for a\n> genuine cross-module reach in every adapter, not only OCaml, and\n> never fires on an incidental shared bare name.\n\nSourceFile.\nModule.\nSymbol.\nModuleType.\nDomain.\n\nmodule-name f: SourceFile => Module.\nmodule-type f: SourceFile => ModuleType.\ndomain f: SourceFile => Domain.\nowner s: Symbol => Module.\nreferences f: SourceFile, s: Symbol => Bool.\nqualified-reference f: SourceFile, m: Module, s: Symbol => Bool.\ndecision-reference f: SourceFile, s: Symbol => Bool.\n---\n> Semantic completeness: a reference to a symbol owned by a foreign\n> module is always emitted qualified by that owner, even when the\n> source writes it bare (after open / import / same package). This\n> closes the syntactic hole described in issue 6.\nall f: SourceFile, s: Symbol, references f s and owner s ~= module-name f | qualified-reference f (owner s) s.\n\n> Prefix consistency: the qualifier on any emitted qualified\n> reference is exactly the owner module's own module-name. Without\n> this, rule 15's intersection silently never matches.\nall f: SourceFile, m: Module, s: Symbol, qualified-reference f m s | m = owner s.\n\nwhere\n\n> ══════════════════════════════════════════\n> RULE 15 (dependency direction) effectiveness\n> ══════════════════════════════════════════\n> A core or value module is flagged exactly when it holds a\n> qualified reference to a same-domain implementation module's\n> declared decision reference. reaches-implementation c holds when\n> some same-domain shell/state/exempt file i declares a symbol s\n> with qualified-reference c (module-name i) s. Incidental bare\n> name collisions never flag because they are not emitted as\n> qualified references.\n\nimplementation? mt: ModuleType => Bool.\ncore-or-value? mt: ModuleType => Bool.\nreaches-implementation? c: SourceFile => Bool.\nflagged? c: SourceFile => Bool.\n\ncore => ModuleType.\nvalue => ModuleType.\nshell => ModuleType.\nstate => ModuleType.\nexempt => ModuleType.\n---\nall mt: ModuleType | implementation? mt = (mt = shell or mt = state or mt = exempt).\nall mt: ModuleType | core-or-value? mt = (mt = core or mt = value).\n\n> The flag fires iff a core/value file actually reaches a same-domain\n> implementation surface via a qualified reference.\nall c: SourceFile, core-or-value? (module-type c) | flagged? c = reaches-implementation? c.\n\n> Soundness of the prohibition: a flag implies a real qualified\n> cross-module reach into a same-domain implementation decision\n> reference.\nall c: SourceFile, reaches-implementation? c | some i: SourceFile | implementation? (module-type i) and domain i = domain c.\n" +} \ No newline at end of file diff --git a/typescript/dependency-summaries/npm.json b/typescript/dependency-summaries/npm.json new file mode 100644 index 0000000..aeb1a79 --- /dev/null +++ b/typescript/dependency-summaries/npm.json @@ -0,0 +1,164 @@ +{ + "dependencies": [ + { + "packageName": "@anthropic-ai/sdk", + "versionRange": "^0.91.0", + "rules": [ + { + "id": "anthropic-sdk-messages-create-network", + "effectCategory": "network", + "receiverTypeNames": ["Anthropic"], + "memberPaths": ["messages.create"], + "evidence": [ + "src/resources/messages/messages.ts: Messages.create delegates to client.post('/v1/messages')", + "src/client.ts: makeRequest delegates to fetchWithTimeout", + "src/client.ts: fetchWithTimeout reaches fetch, setTimeout, and clearTimeout" + ] + }, + { + "id": "anthropic-sdk-messages-stream-network", + "effectCategory": "network", + "receiverTypeNames": ["Anthropic"], + "memberPaths": ["messages.stream"], + "evidence": [ + "src/resources/messages/messages.ts: Messages.stream delegates to Messages.create with stream enabled", + "src/client.ts: fetchWithTimeout reaches fetch, setTimeout, and clearTimeout" + ] + }, + { + "id": "anthropic-sdk-messages-count-tokens-network", + "effectCategory": "network", + "receiverTypeNames": ["Anthropic"], + "memberPaths": ["messages.countTokens"], + "evidence": [ + "src/resources/messages/messages.ts: countTokens delegates to client.post('/v1/messages/count_tokens')", + "src/client.ts: fetchWithTimeout reaches fetch, setTimeout, and clearTimeout" + ] + } + ] + }, + { + "packageName": "@sentry/node", + "versionRange": "^10.0.0", + "rules": [ + { + "id": "sentry-node-init-setup-effect", + "effectCategory": "network", + "receiverTypeNames": [], + "memberPaths": ["init"], + "evidence": [ + "src/sdk/index.ts: init installs the Node SDK client and integrations", + "src/client.ts: NodeClient sends events through the configured Sentry transport" + ] + }, + { + "id": "sentry-node-capture-exception-network", + "effectCategory": "network", + "receiverTypeNames": [], + "memberPaths": ["captureException"], + "evidence": [ + "src/exports.ts: captureException records an exception on the current scope/client", + "src/client.ts: NodeClient sends captured events through the configured transport" + ] + }, + { + "id": "sentry-node-flush-network", + "effectCategory": "network", + "receiverTypeNames": [], + "memberPaths": ["flush"], + "evidence": [ + "src/exports.ts: flush waits for pending event delivery", + "src/client.ts: NodeClient flushes pending transport work" + ] + } + ] + }, + { + "packageName": "openai", + "versionRange": "^6.0.0", + "rules": [ + { + "id": "openai-chat-completions-create-network", + "effectCategory": "network", + "receiverTypeNames": ["OpenAI"], + "memberPaths": ["chat.completions.create"], + "evidence": [ + "src/resources/chat/completions/completions.ts: create posts chat completion requests", + "src/core/api-client.ts: request execution reaches the configured HTTP fetch implementation" + ] + }, + { + "id": "openai-embeddings-create-network", + "effectCategory": "network", + "receiverTypeNames": ["OpenAI"], + "memberPaths": ["embeddings.create"], + "evidence": [ + "src/resources/embeddings.ts: create posts embedding requests", + "src/core/api-client.ts: request execution reaches the configured HTTP fetch implementation" + ] + } + ] + }, + { + "packageName": "@trigger.dev/sdk", + "versionRange": "^4.4.0", + "rules": [ + { + "id": "trigger-sdk-metadata-append-runtime-metadata", + "effectCategory": "storage", + "receiverTypeNames": [], + "memberPaths": ["append"], + "evidence": [ + "dist/esm/v3/metadata.d.ts: metadata.append appends a value to an array in the current run metadata", + "dist/esm/v3/metadata.d.ts: metadata.flush flushes metadata to the Trigger.dev instance" + ] + }, + { + "id": "trigger-sdk-metadata-set-runtime-metadata", + "effectCategory": "storage", + "receiverTypeNames": [], + "memberPaths": ["set", "del", "remove", "increment", "decrement", "replace", "save"], + "evidence": [ + "dist/esm/v3/metadata.d.ts: metadata mutation APIs update current run metadata", + "dist/esm/v3/metadata.d.ts: metadata.flush flushes metadata to the Trigger.dev instance" + ] + }, + { + "id": "trigger-sdk-metadata-refresh-network", + "effectCategory": "network", + "receiverTypeNames": [], + "memberPaths": ["flush", "refresh"], + "evidence": [ + "dist/esm/v3/metadata.d.ts: metadata.flush flushes metadata to the Trigger.dev instance", + "dist/esm/v3/metadata.d.ts: metadata.refresh refreshes metadata from the Trigger.dev instance" + ] + } + ] + }, + { + "packageName": "@trigger.dev/core", + "versionRange": "^4.4.0", + "rules": [ + { + "id": "trigger-core-logger-runtime-log", + "effectCategory": "console", + "receiverTypeNames": ["LoggerAPI"], + "memberPaths": ["debug", "log", "info", "warn", "error"], + "evidence": [ + "dist/esm/v3/logger-api.d.ts: logger is the LoggerAPI entrypoint", + "dist/esm/v3/logger/index.d.ts: LoggerAPI exposes debug/log/info/warn/error task logging methods" + ] + }, + { + "id": "trigger-core-logger-trace-runtime-telemetry", + "effectCategory": "unknown", + "receiverTypeNames": ["LoggerAPI"], + "memberPaths": ["trace", "startSpan"], + "evidence": [ + "dist/esm/v3/logger/index.d.ts: LoggerAPI exposes trace and startSpan telemetry methods" + ] + } + ] + } + ] +} diff --git a/typescript/fixtures/anthropic-dependency/node_modules/@anthropic-ai/sdk/index.d.ts b/typescript/fixtures/anthropic-dependency/node_modules/@anthropic-ai/sdk/index.d.ts new file mode 100644 index 0000000..d2c35aa --- /dev/null +++ b/typescript/fixtures/anthropic-dependency/node_modules/@anthropic-ai/sdk/index.d.ts @@ -0,0 +1,10 @@ +declare class Anthropic { + readonly messages: { + create(body: unknown, options?: unknown): Promise<{ id: string }>; + stream(body: unknown): AsyncIterable; + countTokens(body: unknown): Promise<{ input_tokens: number }>; + }; +} + +export { Anthropic }; +export default Anthropic; diff --git a/typescript/fixtures/anthropic-dependency/node_modules/@anthropic-ai/sdk/package.json b/typescript/fixtures/anthropic-dependency/node_modules/@anthropic-ai/sdk/package.json new file mode 100644 index 0000000..5a8aff4 --- /dev/null +++ b/typescript/fixtures/anthropic-dependency/node_modules/@anthropic-ai/sdk/package.json @@ -0,0 +1,5 @@ +{ + "name": "@anthropic-ai/sdk", + "version": "0.91.0", + "types": "index.d.ts" +} diff --git a/typescript/fixtures/anthropic-dependency/src/anthropic-core.ts b/typescript/fixtures/anthropic-dependency/src/anthropic-core.ts new file mode 100644 index 0000000..7786db4 --- /dev/null +++ b/typescript/fixtures/anthropic-dependency/src/anthropic-core.ts @@ -0,0 +1,25 @@ +import type Anthropic from "@anthropic-ai/sdk"; + +/** + * @archlint.module core + * @archlint.domain anthropic.effects + */ +export async function runModelTurn(params: { readonly anthropic: Anthropic }): Promise { + const response = await createMessage(params); + return response.id; +} + +async function createMessage(params: { readonly anthropic: Anthropic }): Promise<{ id: string }> { + const message = await params.anthropic.messages.create( + { max_tokens: 1, messages: [], model: "claude-test" }, + { signal: undefined }, + ); + return message; +} + +export async function localShape(params: { + readonly anthropic: { readonly messages: { readonly create: () => Promise<{ id: string }> } }; +}): Promise { + const response = await params.anthropic.messages.create(); + return response.id; +} diff --git a/typescript/fixtures/anthropic-dependency/tests/anthropic-core.test.mts b/typescript/fixtures/anthropic-dependency/tests/anthropic-core.test.mts new file mode 100644 index 0000000..2d1749e --- /dev/null +++ b/typescript/fixtures/anthropic-dependency/tests/anthropic-core.test.mts @@ -0,0 +1,14 @@ +import fc from "fast-check"; +import { localShape, runModelTurn } from "../src/anthropic-core"; + +/** + * @archlint.module test + * @archlint.domain anthropic.effects + */ +fc.assert( + fc.property(fc.string(), (value) => { + void runModelTurn; + void localShape; + return value.length >= 0; + }), +); diff --git a/typescript/fixtures/anthropic-dependency/tsconfig.json b/typescript/fixtures/anthropic-dependency/tsconfig.json new file mode 100644 index 0000000..0edb017 --- /dev/null +++ b/typescript/fixtures/anthropic-dependency/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true + }, + "include": ["src/**/*.ts", "tests/**/*.mts"] +} diff --git a/typescript/fixtures/base/node_modules/@types/local-env/index.d.ts b/typescript/fixtures/base/node_modules/@types/local-env/index.d.ts index 3e365f5..12162f1 100644 --- a/typescript/fixtures/base/node_modules/@types/local-env/index.d.ts +++ b/typescript/fixtures/base/node_modules/@types/local-env/index.d.ts @@ -1 +1,6 @@ declare const LOCAL_ENV_FLAG: boolean; +declare const Bun: { + file(path: string): { text(): Promise }; + write(path: string, content: string): Promise; + spawn(command: string[]): unknown; +}; diff --git a/typescript/fixtures/base/src/handler.ts b/typescript/fixtures/base/src/handler.ts index f9a40d1..c764f25 100644 --- a/typescript/fixtures/base/src/handler.ts +++ b/typescript/fixtures/base/src/handler.ts @@ -2,13 +2,22 @@ // @archlint.domain demo.decision import { Command } from "commander"; +import { lookup as dnsLookup } from "node:dns/promises"; import { decide } from "./decision.js"; function createCommand(): Command { return new Command(); } +async function usePlatformEffects(): Promise { + await fetch("https://example.com"); + await dnsLookup("example.com"); + await Bun.file("fixture.txt").text(); + setTimeout(() => {}, 0); +} + export function run(): string { createCommand(); + void usePlatformEffects(); return decide(1); } diff --git a/typescript/fixtures/effect-requirements/node_modules/@effect/sql-pg/index.d.ts b/typescript/fixtures/effect-requirements/node_modules/@effect/sql-pg/index.d.ts new file mode 100644 index 0000000..1285532 --- /dev/null +++ b/typescript/fixtures/effect-requirements/node_modules/@effect/sql-pg/index.d.ts @@ -0,0 +1,10 @@ +import type { Context } from "effect"; +import type { SqlClient } from "@effect/sql"; + +export namespace PgClient { + export interface PgClient extends SqlClient.SqlClient { + readonly notify: (channel: string, payload: string) => Promise; + } + + export const PgClient: Context.Tag; +} diff --git a/typescript/fixtures/effect-requirements/node_modules/@effect/sql-pg/package.json b/typescript/fixtures/effect-requirements/node_modules/@effect/sql-pg/package.json new file mode 100644 index 0000000..ad00284 --- /dev/null +++ b/typescript/fixtures/effect-requirements/node_modules/@effect/sql-pg/package.json @@ -0,0 +1,5 @@ +{ + "name": "@effect/sql-pg", + "version": "0.50.3", + "types": "index.d.ts" +} diff --git a/typescript/fixtures/effect-requirements/node_modules/@effect/sql/index.d.ts b/typescript/fixtures/effect-requirements/node_modules/@effect/sql/index.d.ts new file mode 100644 index 0000000..44162b9 --- /dev/null +++ b/typescript/fixtures/effect-requirements/node_modules/@effect/sql/index.d.ts @@ -0,0 +1,9 @@ +import type { Context } from "effect"; + +export namespace SqlClient { + export interface SqlClient { + readonly unsafe: (sql: string) => Promise; + } + + export const SqlClient: Context.Tag; +} diff --git a/typescript/fixtures/effect-requirements/node_modules/@effect/sql/package.json b/typescript/fixtures/effect-requirements/node_modules/@effect/sql/package.json new file mode 100644 index 0000000..24ce0bf --- /dev/null +++ b/typescript/fixtures/effect-requirements/node_modules/@effect/sql/package.json @@ -0,0 +1,5 @@ +{ + "name": "@effect/sql", + "version": "0.49.0", + "types": "index.d.ts" +} diff --git a/typescript/fixtures/effect-requirements/node_modules/effect/index.d.ts b/typescript/fixtures/effect-requirements/node_modules/effect/index.d.ts new file mode 100644 index 0000000..ae1f65e --- /dev/null +++ b/typescript/fixtures/effect-requirements/node_modules/effect/index.d.ts @@ -0,0 +1,35 @@ +export namespace Effect { + export interface Effect { + readonly _A?: A; + readonly _E?: E; + readonly _R?: R; + } + + export function runPromise(effect: Effect): Promise; + export function sync(evaluate: () => A): Effect; + export function tryPromise( + evaluate: + | (() => Promise) + | { + readonly try: () => Promise; + readonly catch?: (error: unknown) => unknown; + } + ): Effect; + export function succeed(value: A): Effect; +} + +export namespace Layer { + export interface Layer { + readonly _ROut?: ROut; + readonly _E?: E; + readonly _RIn?: RIn; + } + + export function succeed(tag: Context.Tag, value: Value): Layer; +} + +export namespace Context { + export interface Tag extends Effect.Effect {} + + export function GenericTag(id: string): Tag; +} diff --git a/typescript/fixtures/effect-requirements/node_modules/effect/package.json b/typescript/fixtures/effect-requirements/node_modules/effect/package.json new file mode 100644 index 0000000..b324cf9 --- /dev/null +++ b/typescript/fixtures/effect-requirements/node_modules/effect/package.json @@ -0,0 +1,5 @@ +{ + "name": "effect", + "version": "3.19.16", + "types": "index.d.ts" +} diff --git a/typescript/fixtures/effect-requirements/src/effect-core.ts b/typescript/fixtures/effect-requirements/src/effect-core.ts new file mode 100644 index 0000000..92e7bc4 --- /dev/null +++ b/typescript/fixtures/effect-requirements/src/effect-core.ts @@ -0,0 +1,60 @@ +import { Context, Effect, Layer } from "effect"; +import { SqlClient } from "@effect/sql"; +import { PgClient } from "@effect/sql-pg"; + +/** + * @archlint.module core + * @archlint.domain effect.requirements + */ + +interface LocalService { + readonly value: string; +} + +const LocalServiceTag = Context.GenericTag("LocalService"); + +export function decide(value: string): string { + return value.trim(); +} + +export function loadDirect(): Effect.Effect { + return {} as Effect.Effect; +} + +export const loadViaBinding: Effect.Effect = + {} as Effect.Effect; + +export const layerNeedsSql: Layer.Layer = + {} as Layer.Layer; + +export const pgRequirement: Effect.Effect = + {} as Effect.Effect; + +export const localOnly: Effect.Effect = + {} as Effect.Effect; + +export const localLayer = Layer.succeed(LocalServiceTag, { value: "ok" }); + +export function runSqlRequirement(): Promise { + return Effect.runPromise(loadViaBinding); +} + +export function runPureEffect(): Promise { + return Effect.runPromise(Effect.succeed(undefined)); +} + +function platformFetch(): void { + fetch("https://example.com"); +} + +export const fetchProgram = Effect.sync(platformFetch); + +export const tryPromiseProgram = Effect.tryPromise({ + try: async () => { + platformFetch(); + }, +}); + +export function runFetchProgram(): Promise { + return Effect.runPromise(fetchProgram); +} diff --git a/typescript/fixtures/effect-requirements/tests/effect-core.test.mts b/typescript/fixtures/effect-requirements/tests/effect-core.test.mts new file mode 100644 index 0000000..bd2f355 --- /dev/null +++ b/typescript/fixtures/effect-requirements/tests/effect-core.test.mts @@ -0,0 +1,12 @@ +import fc from "fast-check"; +import { decide } from "../src/effect-core"; + +/** + * @archlint.module test + * @archlint.domain effect.requirements + */ +fc.assert( + fc.property(fc.string(), (value) => { + return decide(value).length <= value.length; + }), +); diff --git a/typescript/fixtures/effect-requirements/tsconfig.json b/typescript/fixtures/effect-requirements/tsconfig.json new file mode 100644 index 0000000..0edb017 --- /dev/null +++ b/typescript/fixtures/effect-requirements/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true + }, + "include": ["src/**/*.ts", "tests/**/*.mts"] +} diff --git a/typescript/fixtures/effectful-expansion/src/effectful-core.ts b/typescript/fixtures/effectful-expansion/src/effectful-core.ts new file mode 100644 index 0000000..cd9d7a1 --- /dev/null +++ b/typescript/fixtures/effectful-expansion/src/effectful-core.ts @@ -0,0 +1,13 @@ +// @archlint.module core +// @archlint.domain demo.effectful-expansion + +import { Command } from "commander"; + +function createCommand(): Command { + return new Command(); +} + +export function decideWithEffect(value: number): boolean { + createCommand(); + return value >= 0; +} diff --git a/typescript/fixtures/effectful-expansion/tests/effectful-core.test.mts b/typescript/fixtures/effectful-expansion/tests/effectful-core.test.mts new file mode 100644 index 0000000..6702272 --- /dev/null +++ b/typescript/fixtures/effectful-expansion/tests/effectful-core.test.mts @@ -0,0 +1,15 @@ +// @archlint.module test +// @archlint.domain demo.effectful-expansion + +import { test } from "node:test"; +import assert from "node:assert/strict"; +import * as fc from "fast-check"; +import { decideWithEffect } from "../src/effectful-core.js"; + +test("decision property", () => { + fc.assert( + fc.property(fc.integer(), (value) => { + assert.equal(typeof decideWithEffect(value), "boolean"); + }), + ); +}); diff --git a/typescript/fixtures/effectful-expansion/tsconfig.json b/typescript/fixtures/effectful-expansion/tsconfig.json new file mode 100644 index 0000000..aac9d0f --- /dev/null +++ b/typescript/fixtures/effectful-expansion/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "skipLibCheck": true, + "types": ["local-env"] + }, + "include": ["src/**/*", "tests/**/*"] +} diff --git a/typescript/fixtures/local-relative-expansion/src/core.ts b/typescript/fixtures/local-relative-expansion/src/core.ts new file mode 100644 index 0000000..92d44a8 --- /dev/null +++ b/typescript/fixtures/local-relative-expansion/src/core.ts @@ -0,0 +1,9 @@ +// @archlint.module core +// @archlint.domain demo.local-relative-expansion + +import { effectfulHelper } from "../support/effects.js"; + +export function decideViaExternalHelper(value: number): boolean { + effectfulHelper(); + return value >= 0; +} diff --git a/typescript/fixtures/local-relative-expansion/support/effects.ts b/typescript/fixtures/local-relative-expansion/support/effects.ts new file mode 100644 index 0000000..4d5da0b --- /dev/null +++ b/typescript/fixtures/local-relative-expansion/support/effects.ts @@ -0,0 +1,3 @@ +export function effectfulHelper(): void { + fetch("https://example.com"); +} diff --git a/typescript/fixtures/local-relative-expansion/tsconfig.json b/typescript/fixtures/local-relative-expansion/tsconfig.json new file mode 100644 index 0000000..2cca41b --- /dev/null +++ b/typescript/fixtures/local-relative-expansion/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src/**/*.ts", "support/**/*.ts"] +} diff --git a/typescript/fixtures/openai-dependency/node_modules/openai/index.d.ts b/typescript/fixtures/openai-dependency/node_modules/openai/index.d.ts new file mode 100644 index 0000000..b1681ac --- /dev/null +++ b/typescript/fixtures/openai-dependency/node_modules/openai/index.d.ts @@ -0,0 +1,16 @@ +declare class OpenAI { + constructor(options?: { readonly apiKey?: string }); + + readonly chat: { + readonly completions: { + create(body: unknown): Promise<{ id: string }>; + }; + }; + + readonly embeddings: { + create(body: unknown): Promise<{ data: readonly { embedding: readonly number[] }[] }>; + }; +} + +export { OpenAI }; +export default OpenAI; diff --git a/typescript/fixtures/openai-dependency/node_modules/openai/package.json b/typescript/fixtures/openai-dependency/node_modules/openai/package.json new file mode 100644 index 0000000..5e9ffcc --- /dev/null +++ b/typescript/fixtures/openai-dependency/node_modules/openai/package.json @@ -0,0 +1,5 @@ +{ + "name": "openai", + "version": "6.34.0", + "types": "index.d.ts" +} diff --git a/typescript/fixtures/openai-dependency/src/openai-core.ts b/typescript/fixtures/openai-dependency/src/openai-core.ts new file mode 100644 index 0000000..b2081e1 --- /dev/null +++ b/typescript/fixtures/openai-dependency/src/openai-core.ts @@ -0,0 +1,36 @@ +import OpenAI from "openai"; + +/** + * @archlint.module core + * @archlint.domain openai.effects + */ +export async function runInference(params: { readonly client: OpenAI }): Promise { + const response = await params.client.chat.completions.create({ + messages: [], + model: "gpt-test", + }); + return response.id; +} + +export async function embedText(params: { readonly client: OpenAI; readonly text: string }): Promise { + const response = await params.client.embeddings.create({ + input: params.text, + model: "text-embedding-test", + }); + return response.data.length; +} + +export function configureClient(): OpenAI { + return new OpenAI({ apiKey: "test" }); +} + +export async function localShape(params: { + readonly client: { + readonly chat: { readonly completions: { readonly create: () => Promise<{ id: string }> } }; + readonly embeddings: { readonly create: () => Promise<{ data: readonly unknown[] }> }; + }; +}): Promise { + const completion = await params.client.chat.completions.create(); + const embedding = await params.client.embeddings.create(); + return completion.id.length + embedding.data.length; +} diff --git a/typescript/fixtures/openai-dependency/tests/openai-core.test.mts b/typescript/fixtures/openai-dependency/tests/openai-core.test.mts new file mode 100644 index 0000000..b7b5579 --- /dev/null +++ b/typescript/fixtures/openai-dependency/tests/openai-core.test.mts @@ -0,0 +1,16 @@ +import fc from "fast-check"; +import { configureClient, embedText, localShape, runInference } from "../src/openai-core"; + +/** + * @archlint.module test + * @archlint.domain openai.effects + */ +fc.assert( + fc.property(fc.string(), (value) => { + void runInference; + void embedText; + void configureClient; + void localShape; + return value.length >= 0; + }), +); diff --git a/typescript/fixtures/openai-dependency/tsconfig.json b/typescript/fixtures/openai-dependency/tsconfig.json new file mode 100644 index 0000000..0edb017 --- /dev/null +++ b/typescript/fixtures/openai-dependency/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true + }, + "include": ["src/**/*.ts", "tests/**/*.mts"] +} diff --git a/typescript/fixtures/sentry-dependency/node_modules/@sentry/node/index.d.ts b/typescript/fixtures/sentry-dependency/node_modules/@sentry/node/index.d.ts new file mode 100644 index 0000000..9cc5f05 --- /dev/null +++ b/typescript/fixtures/sentry-dependency/node_modules/@sentry/node/index.d.ts @@ -0,0 +1,12 @@ +export interface Client { + readonly close: () => Promise; +} + +export interface NodeOptions { + readonly dsn?: string; +} + +export declare function init(options?: NodeOptions): void; +export declare function captureException(exception: unknown): string; +export declare function flush(timeout?: number): Promise; +export declare function getClient(): Client | undefined; diff --git a/typescript/fixtures/sentry-dependency/node_modules/@sentry/node/package.json b/typescript/fixtures/sentry-dependency/node_modules/@sentry/node/package.json new file mode 100644 index 0000000..019daf7 --- /dev/null +++ b/typescript/fixtures/sentry-dependency/node_modules/@sentry/node/package.json @@ -0,0 +1,5 @@ +{ + "name": "@sentry/node", + "version": "10.29.0", + "types": "index.d.ts" +} diff --git a/typescript/fixtures/sentry-dependency/src/sentry-core.ts b/typescript/fixtures/sentry-dependency/src/sentry-core.ts new file mode 100644 index 0000000..7ac90e9 --- /dev/null +++ b/typescript/fixtures/sentry-dependency/src/sentry-core.ts @@ -0,0 +1,27 @@ +import * as Sentry from "@sentry/node"; + +/** + * @archlint.module core + * @archlint.domain sentry.effects + */ +export async function reportFailure(error: unknown): Promise { + Sentry.init({ dsn: "https://example.invalid/1" }); + Sentry.captureException(error); + const client = Sentry.getClient(); + void client; + return Sentry.flush(250); +} + +export async function localShape(params: { + readonly Sentry: { + readonly init: (options?: unknown) => void; + readonly captureException: (error: unknown) => string; + readonly flush: (timeout?: number) => Promise; + readonly getClient: () => unknown; + }; +}): Promise { + params.Sentry.init({}); + params.Sentry.captureException(new Error("local")); + params.Sentry.getClient(); + return params.Sentry.flush(1); +} diff --git a/typescript/fixtures/sentry-dependency/tests/sentry-core.test.mts b/typescript/fixtures/sentry-dependency/tests/sentry-core.test.mts new file mode 100644 index 0000000..21cec3e --- /dev/null +++ b/typescript/fixtures/sentry-dependency/tests/sentry-core.test.mts @@ -0,0 +1,14 @@ +import fc from "fast-check"; +import { localShape, reportFailure } from "../src/sentry-core"; + +/** + * @archlint.module test + * @archlint.domain sentry.effects + */ +fc.assert( + fc.property(fc.string(), (value) => { + void reportFailure; + void localShape; + return value.length >= 0; + }), +); diff --git a/typescript/fixtures/sentry-dependency/tsconfig.json b/typescript/fixtures/sentry-dependency/tsconfig.json new file mode 100644 index 0000000..0edb017 --- /dev/null +++ b/typescript/fixtures/sentry-dependency/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true + }, + "include": ["src/**/*.ts", "tests/**/*.mts"] +} diff --git a/typescript/fixtures/trigger-dependency/node_modules/@trigger.dev/core/dist/esm/v3/index.d.ts b/typescript/fixtures/trigger-dependency/node_modules/@trigger.dev/core/dist/esm/v3/index.d.ts new file mode 100644 index 0000000..c3a3fb1 --- /dev/null +++ b/typescript/fixtures/trigger-dependency/node_modules/@trigger.dev/core/dist/esm/v3/index.d.ts @@ -0,0 +1 @@ +export * from "./logger-api.js"; diff --git a/typescript/fixtures/trigger-dependency/node_modules/@trigger.dev/core/dist/esm/v3/logger-api.d.ts b/typescript/fixtures/trigger-dependency/node_modules/@trigger.dev/core/dist/esm/v3/logger-api.d.ts new file mode 100644 index 0000000..e8609a4 --- /dev/null +++ b/typescript/fixtures/trigger-dependency/node_modules/@trigger.dev/core/dist/esm/v3/logger-api.d.ts @@ -0,0 +1,3 @@ +import { LoggerAPI } from "./logger/index.js"; + +export declare const logger: LoggerAPI; diff --git a/typescript/fixtures/trigger-dependency/node_modules/@trigger.dev/core/dist/esm/v3/logger/index.d.ts b/typescript/fixtures/trigger-dependency/node_modules/@trigger.dev/core/dist/esm/v3/logger/index.d.ts new file mode 100644 index 0000000..ffa346b --- /dev/null +++ b/typescript/fixtures/trigger-dependency/node_modules/@trigger.dev/core/dist/esm/v3/logger/index.d.ts @@ -0,0 +1,4 @@ +export declare class LoggerAPI { + warn(message: string, metadata?: Record): void; + info(message: string, metadata?: Record): void; +} diff --git a/typescript/fixtures/trigger-dependency/node_modules/@trigger.dev/core/package.json b/typescript/fixtures/trigger-dependency/node_modules/@trigger.dev/core/package.json new file mode 100644 index 0000000..57fc8f2 --- /dev/null +++ b/typescript/fixtures/trigger-dependency/node_modules/@trigger.dev/core/package.json @@ -0,0 +1,10 @@ +{ + "name": "@trigger.dev/core", + "version": "4.4.6", + "type": "module", + "exports": { + "./v3": { + "types": "./dist/esm/v3/index.d.ts" + } + } +} diff --git a/typescript/fixtures/trigger-dependency/node_modules/@trigger.dev/sdk/dist/esm/v3/index.d.ts b/typescript/fixtures/trigger-dependency/node_modules/@trigger.dev/sdk/dist/esm/v3/index.d.ts new file mode 100644 index 0000000..840b55b --- /dev/null +++ b/typescript/fixtures/trigger-dependency/node_modules/@trigger.dev/sdk/dist/esm/v3/index.d.ts @@ -0,0 +1,2 @@ +export * from "./metadata.js"; +export { logger } from "@trigger.dev/core/v3"; diff --git a/typescript/fixtures/trigger-dependency/node_modules/@trigger.dev/sdk/dist/esm/v3/metadata.d.ts b/typescript/fixtures/trigger-dependency/node_modules/@trigger.dev/sdk/dist/esm/v3/metadata.d.ts new file mode 100644 index 0000000..75d48af --- /dev/null +++ b/typescript/fixtures/trigger-dependency/node_modules/@trigger.dev/sdk/dist/esm/v3/metadata.d.ts @@ -0,0 +1,5 @@ +export declare const metadata: { + append(key: string, value: unknown): void; + set(key: string, value: unknown): void; + flush(): Promise; +}; diff --git a/typescript/fixtures/trigger-dependency/node_modules/@trigger.dev/sdk/package.json b/typescript/fixtures/trigger-dependency/node_modules/@trigger.dev/sdk/package.json new file mode 100644 index 0000000..c084db8 --- /dev/null +++ b/typescript/fixtures/trigger-dependency/node_modules/@trigger.dev/sdk/package.json @@ -0,0 +1,10 @@ +{ + "name": "@trigger.dev/sdk", + "version": "4.4.6", + "type": "module", + "exports": { + "./v3": { + "types": "./dist/esm/v3/index.d.ts" + } + } +} diff --git a/typescript/fixtures/trigger-dependency/src/trigger-core.ts b/typescript/fixtures/trigger-dependency/src/trigger-core.ts new file mode 100644 index 0000000..39a1d15 --- /dev/null +++ b/typescript/fixtures/trigger-dependency/src/trigger-core.ts @@ -0,0 +1,18 @@ +import { logger, metadata } from "@trigger.dev/sdk/v3"; + +/** + * @archlint.module core + * @archlint.domain trigger.effects + */ +export function emitTriggerEvent(): void { + metadata.append("events", { type: "started" }); + logger.warn("started", { source: "test" }); +} + +export function localShape(params: { + readonly metadata: { readonly append: (key: string, value: unknown) => void }; + readonly logger: { readonly warn: (message: string) => void }; +}): void { + params.metadata.append("events", {}); + params.logger.warn("local"); +} diff --git a/typescript/fixtures/trigger-dependency/tsconfig.json b/typescript/fixtures/trigger-dependency/tsconfig.json new file mode 100644 index 0000000..bf4fec0 --- /dev/null +++ b/typescript/fixtures/trigger-dependency/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src/**/*.ts"] +} diff --git a/typescript/fixtures/workspace-package-expansion/package.json b/typescript/fixtures/workspace-package-expansion/package.json new file mode 100644 index 0000000..aad9ea8 --- /dev/null +++ b/typescript/fixtures/workspace-package-expansion/package.json @@ -0,0 +1,4 @@ +{ + "private": true, + "workspaces": ["packages/*"] +} diff --git a/typescript/fixtures/workspace-package-expansion/packages/consumer/package.json b/typescript/fixtures/workspace-package-expansion/packages/consumer/package.json new file mode 100644 index 0000000..61d0076 --- /dev/null +++ b/typescript/fixtures/workspace-package-expansion/packages/consumer/package.json @@ -0,0 +1,5 @@ +{ + "name": "@demo/consumer", + "private": true, + "type": "module" +} diff --git a/typescript/fixtures/workspace-package-expansion/packages/consumer/src/core.ts b/typescript/fixtures/workspace-package-expansion/packages/consumer/src/core.ts new file mode 100644 index 0000000..207d06d --- /dev/null +++ b/typescript/fixtures/workspace-package-expansion/packages/consumer/src/core.ts @@ -0,0 +1,9 @@ +// @archlint.module core +// @archlint.domain demo.workspace-package-expansion + +import { effectfulHelper } from "@demo/provider"; + +export function decideViaWorkspacePackage(value: number): boolean { + effectfulHelper(); + return value >= 0; +} diff --git a/typescript/fixtures/workspace-package-expansion/packages/consumer/tsconfig.json b/typescript/fixtures/workspace-package-expansion/packages/consumer/tsconfig.json new file mode 100644 index 0000000..c39ea00 --- /dev/null +++ b/typescript/fixtures/workspace-package-expansion/packages/consumer/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "baseUrl": ".", + "paths": { + "@demo/provider": ["../provider/src/index.ts"], + "@demo/provider/*": ["../provider/src/*"] + } + }, + "include": ["src/**/*.ts"] +} diff --git a/typescript/fixtures/workspace-package-expansion/packages/provider/package.json b/typescript/fixtures/workspace-package-expansion/packages/provider/package.json new file mode 100644 index 0000000..ba6b6c7 --- /dev/null +++ b/typescript/fixtures/workspace-package-expansion/packages/provider/package.json @@ -0,0 +1,5 @@ +{ + "name": "@demo/provider", + "private": true, + "type": "module" +} diff --git a/typescript/fixtures/workspace-package-expansion/packages/provider/src/index.ts b/typescript/fixtures/workspace-package-expansion/packages/provider/src/index.ts new file mode 100644 index 0000000..4d5da0b --- /dev/null +++ b/typescript/fixtures/workspace-package-expansion/packages/provider/src/index.ts @@ -0,0 +1,3 @@ +export function effectfulHelper(): void { + fetch("https://example.com"); +} diff --git a/typescript/src/main.ts b/typescript/src/main.ts index bbb31f6..93be920 100644 --- a/typescript/src/main.ts +++ b/typescript/src/main.ts @@ -1,5 +1,6 @@ -import { readdirSync, readFileSync } from "node:fs"; +import { existsSync, realpathSync, readdirSync, readFileSync } from "node:fs"; import path from "node:path"; +import { fileURLToPath } from "node:url"; import ts from "typescript"; type Metadata = { @@ -30,6 +31,18 @@ type SharedStateFact = { references: string[]; }; +type EffectfulReferenceFact = { + kind: string; + category: string; + reference: string; + origin: string; + enclosingIdentifier: string; + packageName: string; + summaryId: string; + receiverType: string; + evidence: string[]; +}; + type InterfaceLogicEvidence = { functionBodies: string[]; constructorBodies: string[]; @@ -53,6 +66,7 @@ type SourceFact = { qualifiedReferences: string[]; effectfulImports: string[]; effectfulIdentifiers: string[]; + effectfulReferences: EffectfulReferenceFact[]; sharedState: SharedStateFact[]; propertyChecks: PropertyCheckFact[]; interfaceLogicEvidence: InterfaceLogicEvidence; @@ -68,6 +82,7 @@ type Facts = { decisionReferences: Set; qualifiedReferences: Set; effectfulIdentifiers: Set; + effectfulReferences: Map; sharedState: Map>; propertyChecks: PropertyCheckFact[]; functionReferences: Map>; @@ -86,57 +101,293 @@ type ProgramContext = { program: ts.Program; checker: ts.TypeChecker; ownedSourceFiles: Set; + dependencySummaries: DependencySummary[]; + workspacePackageRoots: Set; +}; + +type LocalCallGraph = { + functionReferences: Map>; + effectfulFunctions: Map; +}; + +type DependencySummaryDocument = { + dependencies: DependencySummary[]; +}; + +type DependencySummary = { + packageName: string; + versionRange: string; + rules: DependencyEffectRule[]; +}; + +type DependencyEffectRule = { + id: string; + effectCategory: string; + receiverTypeNames: string[]; + memberPaths: string[]; + evidence: string[]; +}; + +type EffectRequirementClassification = { + category: string; + reference: string; + packageName: string; + summaryId: string; + receiverType: string; + evidence: string; }; const sourceExtensions = new Set([".ts", ".tsx", ".mts", ".cts"]); const ignoredDirectories = new Set(["node_modules", "dist", "build", "coverage", ".git"]); const effectfulImports = new Set([ + "bun", "child_process", "commander", "fs", + "node:cluster", "node:child_process", + "node:dgram", + "node:dns", + "node:dns/promises", "node:fs", "node:fs/promises", "node:http", "node:https", + "node:inspector", "node:net", "node:os", "node:path", "node:process", "node:readline", + "node:readline/promises", + "node:sqlite", "node:stream", + "node:timers", + "node:timers/promises", + "node:tls", + "node:tty", "node:url", "node:worker_threads", "process", ]); +const effectfulBindingImports = new Set([ + "bun", + "child_process", + "fs", + "node:cluster", + "node:child_process", + "node:dgram", + "node:dns", + "node:dns/promises", + "node:fs", + "node:fs/promises", + "node:http", + "node:https", + "node:inspector", + "node:net", + "node:process", + "node:readline", + "node:readline/promises", + "node:sqlite", + "node:timers", + "node:timers/promises", + "node:tls", + "node:tty", + "node:worker_threads", + "process", +]); + const effectfulIdentifiers = new Set([ + "Bun", "Command", + "BroadcastChannel", + "Deno", + "EventSource", "console", + "caches", + "clearImmediate", + "clearInterval", + "clearTimeout", + "crypto", + "document", "fetch", "File", + "indexedDB", + "localStorage", + "navigator", "process", + "queueMicrotask", "readFile", "readFileSync", + "sessionStorage", + "setImmediate", + "setInterval", + "setTimeout", + "SharedWorker", + "WebSocket", + "window", + "Worker", "writeFile", "writeFileSync", + "XMLHttpRequest", +]); + +const effectfulImportCategories = new Map([ + ["bun", "process"], + ["child_process", "process"], + ["commander", "process"], + ["fs", "filesystem"], + ["node:cluster", "process"], + ["node:child_process", "process"], + ["node:dgram", "network"], + ["node:dns", "network"], + ["node:dns/promises", "network"], + ["node:fs", "filesystem"], + ["node:fs/promises", "filesystem"], + ["node:http", "network"], + ["node:https", "network"], + ["node:inspector", "process"], + ["node:net", "network"], + ["node:process", "process"], + ["node:readline", "process"], + ["node:readline/promises", "process"], + ["node:sqlite", "filesystem"], + ["node:timers", "timer"], + ["node:timers/promises", "timer"], + ["node:tls", "network"], + ["node:tty", "process"], + ["node:worker_threads", "process"], + ["process", "process"], ]); -const testImports = new Set(["node:test", "fast-check"]); +const effectfulIdentifierCategories = new Map([ + ["Bun", "process"], + ["Command", "process"], + ["BroadcastChannel", "browser"], + ["Deno", "process"], + ["EventSource", "network"], + ["console", "console"], + ["caches", "storage"], + ["clearImmediate", "timer"], + ["clearInterval", "timer"], + ["clearTimeout", "timer"], + ["crypto", "unknown"], + ["document", "browser"], + ["fetch", "network"], + ["File", "filesystem"], + ["indexedDB", "storage"], + ["localStorage", "storage"], + ["navigator", "browser"], + ["process", "process"], + ["queueMicrotask", "timer"], + ["readFile", "filesystem"], + ["readFileSync", "filesystem"], + ["sessionStorage", "storage"], + ["setImmediate", "timer"], + ["setInterval", "timer"], + ["setTimeout", "timer"], + ["SharedWorker", "browser"], + ["WebSocket", "network"], + ["window", "browser"], + ["Worker", "browser"], + ["writeFile", "filesystem"], + ["writeFileSync", "filesystem"], + ["XMLHttpRequest", "network"], +]); + +const testImports = new Set(["bun:test", "node:test", "fast-check"]); const propertyNames = new Set(["property", "asyncProperty"]); const operationGeneratorNames = new Set(["array", "uniqueArray"]); +const effectRequirementClassifications = new Map([ + [ + "@effect/sql:SqlClient", + { + category: "database", + reference: "SqlClient.SqlClient", + packageName: "@effect/sql", + summaryId: "effect-requirement-sqlclient", + receiverType: "SqlClient", + evidence: "Effect requirement channel includes @effect/sql SqlClient.SqlClient", + }, + ], + [ + "@effect/sql-pg:PgClient", + { + category: "database", + reference: "PgClient.PgClient", + packageName: "@effect/sql-pg", + summaryId: "effect-requirement-pgclient", + receiverType: "PgClient", + evidence: "Effect requirement channel includes @effect/sql-pg PgClient.PgClient", + }, + ], + [ + "effect:Clock", + { + category: "timer", + reference: "Clock.Clock", + packageName: "effect", + summaryId: "effect-requirement-clock", + receiverType: "Clock", + evidence: "Effect requirement channel includes effect Clock.Clock", + }, + ], + [ + "effect:Random", + { + category: "unknown", + reference: "Random.Random", + packageName: "effect", + summaryId: "effect-requirement-random", + receiverType: "Random", + evidence: "Effect requirement channel includes effect Random.Random", + }, + ], + [ + "effect:Scope", + { + category: "unknown", + reference: "Scope.Scope", + packageName: "effect", + summaryId: "effect-requirement-scope", + receiverType: "Scope", + evidence: "Effect requirement channel includes effect Scope.Scope", + }, + ], +]); + +const effectRunnerMemberPaths = new Set([ + "runCallback", + "runFork", + "runPromise", + "runPromiseExit", + "runRequestBlock", + "runSync", + "runSyncExit", +]); + +const effectConstructorMemberPaths = new Set([ + "async", + "promise", + "suspend", + "sync", + "tryPromise", +]); + function main(): void { try { const args = parseArgs(process.argv.slice(2)); const repoRoot = path.resolve(args.get("--repo-root") ?? "."); const root = path.resolve(repoRoot, args.get("--typescript-root") ?? "."); const files = sourceFiles(root); - const programContext = buildProgram(files, root); + const programContext = buildProgram(files, root, repoRoot, loadDependencySummaries()); const parsedFiles = files.map((filePath) => parseSourceFacts(filePath, programContext)); + const localCallGraph = collectLocalCallGraph(programContext); const globalFunctionReferences = uniqueGlobalFunctionReferences(parsedFiles); - const facts = parsedFiles.map((file) => sourceFact(file, globalFunctionReferences, programContext)); + const facts = parsedFiles.map((file) => sourceFact(file, globalFunctionReferences, localCallGraph, programContext)); process.stdout.write(`${JSON.stringify({ files: facts })}\n`); } catch (error) { const message = error instanceof Error ? error.message : String(error); @@ -181,7 +432,19 @@ function sourceFiles(root: string): string[] { return files.sort(); } -function buildProgram(filePaths: string[], root: string): ProgramContext { +function loadDependencySummaries(): DependencySummary[] { + const adapterRoot = path.dirname(fileURLToPath(import.meta.url)); + const summaryPath = path.resolve(adapterRoot, "../dependency-summaries/npm.json"); + const document = JSON.parse(readFileSync(summaryPath, "utf8")) as DependencySummaryDocument; + return document.dependencies; +} + +function buildProgram( + filePaths: string[], + root: string, + repoRoot: string, + dependencySummaries: DependencySummary[], +): ProgramContext { const configPath = ts.findConfigFile(root, ts.sys.fileExists); const parsedConfig = configPath === undefined ? defaultCompilerConfig(filePaths) : compilerConfigFromTsconfig(configPath); const rootNames = sorted(new Set([...parsedConfig.fileNames, ...filePaths])).map((filePath) => path.resolve(filePath)); @@ -197,6 +460,8 @@ function buildProgram(filePaths: string[], root: string): ProgramContext { program, checker: program.getTypeChecker(), ownedSourceFiles: new Set(filePaths.map(normalizePath)), + dependencySummaries, + workspacePackageRoots: discoverWorkspacePackageRoots(repoRoot), }; } @@ -264,6 +529,84 @@ function resolveLocalModuleSpecifier(baseDirectory: string, specifier: string): return candidates.find((candidate) => ts.sys.fileExists(candidate)); } +function discoverWorkspacePackageRoots(repoRoot: string): Set { + const packageJsonPath = path.join(repoRoot, "package.json"); + if (!existsSync(packageJsonPath)) { + return new Set(); + } + const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { + workspaces?: unknown; + }; + const workspacePatterns = workspacePatternsFromPackageJson(packageJson.workspaces); + if (workspacePatterns.length === 0) { + return new Set(); + } + const roots = new Set(); + for (const pattern of workspacePatterns) { + for (const directory of expandWorkspacePattern(repoRoot, pattern)) { + const childPackageJson = path.join(directory, "package.json"); + if (existsSync(childPackageJson)) { + roots.add(realNormalizePath(directory)); + } + } + } + return roots; +} + +function workspacePatternsFromPackageJson(workspaces: unknown): string[] { + if (Array.isArray(workspaces)) { + return workspaces.filter((value): value is string => typeof value === "string"); + } + if (workspaces !== null && typeof workspaces === "object" && "packages" in workspaces) { + const packages = (workspaces as { packages?: unknown }).packages; + if (Array.isArray(packages)) { + return packages.filter((value): value is string => typeof value === "string"); + } + } + return []; +} + +function expandWorkspacePattern(repoRoot: string, pattern: string): string[] { + const segments = pattern.split(/[\\/]+/u).filter((segment) => segment !== ""); + const results: string[] = []; + const visit = (directory: string, index: number): void => { + if (index >= segments.length) { + results.push(directory); + return; + } + const segment = segments[index]; + if (segment === undefined) { + return; + } + if (segment === "*") { + for (const entry of safeReadDirectories(directory)) { + visit(path.join(directory, entry), index + 1); + } + return; + } + if (segment === "**") { + visit(directory, index + 1); + for (const entry of safeReadDirectories(directory)) { + visit(path.join(directory, entry), index); + } + return; + } + visit(path.join(directory, segment), index + 1); + }; + visit(repoRoot, 0); + return results; +} + +function safeReadDirectories(directory: string): string[] { + if (!existsSync(directory)) { + return []; + } + return readdirSync(directory, { withFileTypes: true }) + .filter((entry) => entry.isDirectory() && !ignoredDirectories.has(entry.name)) + .map((entry) => entry.name) + .sort(); +} + function formatDiagnostics(diagnostics: ts.Diagnostic[]): string { const host: ts.FormatDiagnosticsHost = { getCanonicalFileName: (fileName) => fileName, @@ -280,7 +623,7 @@ function parseSourceFacts(filePath: string, programContext: ProgramContext): Par throw new Error(`program did not include ${filePath}`); } const facts = emptyFacts(); - collectTopLevel(facts, sourceFile); + collectTopLevel(facts, sourceFile, programContext); return { filePath, source, sourceFile, facts }; } @@ -302,14 +645,25 @@ function moduleNameFromPath(filePath: string): string { return baseName === "" ? "" : `${baseName[0]?.toUpperCase() ?? ""}${baseName.slice(1)}`; } -function sourceFact(file: ParsedFile, globalFunctionReferences: Map>, programContext: ProgramContext): SourceFact { +function sourceFact( + file: ParsedFile, + globalFunctionReferences: Map>, + localCallGraph: LocalCallGraph, + programContext: ProgramContext, +): SourceFact { const { filePath, source, sourceFile, facts } = file; for (const [name, references] of globalFunctionReferences) { if (!facts.functionReferences.has(name)) { facts.functionReferences.set(name, references); } } - collectNodeFacts(facts, sourceFile); + for (const [name, references] of localCallGraph.functionReferences) { + if (!facts.functionReferences.has(name)) { + facts.functionReferences.set(name, references); + } + } + collectNodeFacts(facts, sourceFile, programContext); + expandEffectfulIdentifiers(facts, localCallGraph.effectfulFunctions); collectQualifiedReferences(facts, sourceFile, programContext); const metadata = parseMetadata(source); if (metadata.moduleType !== "stateTest") { @@ -331,7 +685,8 @@ function sourceFact(file: ParsedFile, globalFunctionReferences: Map>(); + const effectfulFunctions = new Map(); + for (const sourceFile of localProgramSourceFiles(programContext)) { + const facts = emptyFacts(); + collectTopLevel(facts, sourceFile, programContext); + collectNodeFacts(facts, sourceFile, programContext); + for (const [name, references] of facts.functionReferences) { + functionReferences.set(qualifiedLocalReference(sourceFile, name), references); + } + expandEffectfulIdentifiers(facts, new Map()); + for (const identifier of facts.effectfulIdentifiers) { + const qualified = qualifiedLocalReference(sourceFile, identifier); + effectfulFunctions.set(qualified, effectfulReferenceFactsForIdentifier(facts, identifier)); + } + } + return { functionReferences, effectfulFunctions }; +} + +function localProgramSourceFiles(programContext: ProgramContext): ts.SourceFile[] { + return programContext.program.getSourceFiles().filter((sourceFile) => + !sourceFile.isDeclarationFile + && !normalizePath(sourceFile.fileName).includes(`${path.sep}node_modules${path.sep}`) + && sourceExtensions.has(path.extname(sourceFile.fileName)) + ); +} + +function qualifiedLocalReference(sourceFile: ts.SourceFile, name: string): string { + return `${moduleNameFromPath(sourceFile.fileName)}.${name}`; +} + +function collectTopLevel(facts: Facts, sourceFile: ts.SourceFile, programContext: ProgramContext): void { for (const statement of sourceFile.statements) { if (ts.isImportDeclaration(statement)) { const specifier = statement.moduleSpecifier; if (ts.isStringLiteral(specifier)) { facts.imports.add(specifier.text); + if (effectfulImports.has(specifier.text)) { + addEffectfulReference(facts, { + kind: "import", + category: effectfulImportCategories.get(specifier.text) ?? "unknown", + reference: specifier.text, + origin: "standard-library", + enclosingIdentifier: "", + packageName: "", + summaryId: "", + receiverType: "", + evidence: [`effectful import root: ${specifier.text}`], + }); + } + if (!statement.importClause?.isTypeOnly && effectfulBindingImports.has(specifier.text)) { + for (const name of importBindingNames(statement.importClause)) { + facts.effectfulIdentifiers.add(name); + } + } } continue; } @@ -491,11 +896,11 @@ function collectTopLevel(facts: Facts, sourceFile: ts.SourceFile): void { continue; } if (ts.isFunctionDeclaration(statement) && statement.name !== undefined) { - recordFunctionDeclaration(facts, statement); + recordFunctionDeclaration(facts, statement, sourceFile, programContext); continue; } if (ts.isVariableStatement(statement)) { - recordVariableStatement(facts, statement, true); + recordVariableStatement(facts, statement, true, sourceFile, programContext); continue; } if (ts.isClassDeclaration(statement) && statement.name !== undefined) { @@ -508,7 +913,7 @@ function collectTopLevel(facts: Facts, sourceFile: ts.SourceFile): void { const name = propertyName(member.name); if (name !== "" && member.body !== undefined) { addUnique(facts.interfaceLogicEvidence.functionBodies, name); - facts.functionReferences.set(name, apiReferencesInNode(member.body)); + facts.functionReferences.set(name, apiReferencesInNode(member.body, sourceFile, programContext)); } } } @@ -520,7 +925,36 @@ function collectTopLevel(facts: Facts, sourceFile: ts.SourceFile): void { } } -function recordFunctionDeclaration(facts: Facts, declaration: ts.FunctionDeclaration): void { +function importBindingNames(importClause: ts.ImportClause | undefined): string[] { + if (importClause === undefined) { + return []; + } + const names: string[] = []; + if (importClause.name !== undefined) { + names.push(importClause.name.text); + } + const namedBindings = importClause.namedBindings; + if (namedBindings === undefined) { + return names; + } + if (ts.isNamespaceImport(namedBindings)) { + names.push(namedBindings.name.text); + return names; + } + for (const element of namedBindings.elements) { + if (!element.isTypeOnly) { + names.push(element.name.text); + } + } + return names; +} + +function recordFunctionDeclaration( + facts: Facts, + declaration: ts.FunctionDeclaration, + sourceFile: ts.SourceFile, + programContext: ProgramContext, +): void { if (declaration.name === undefined) { return; } @@ -534,13 +968,19 @@ function recordFunctionDeclaration(facts: Facts, declaration: ts.FunctionDeclara } if (declaration.body !== undefined) { addUnique(facts.interfaceLogicEvidence.functionBodies, name); - facts.functionReferences.set(name, apiReferencesInNode(declaration.body)); + facts.functionReferences.set(name, apiReferencesInNode(declaration.body, sourceFile, programContext)); facts.functionBodies.set(name, declaration); } recordReturnTypeProducts(facts, declaration.type); } -function recordVariableStatement(facts: Facts, statement: ts.VariableStatement, topLevel: boolean): void { +function recordVariableStatement( + facts: Facts, + statement: ts.VariableStatement, + topLevel: boolean, + sourceFile: ts.SourceFile, + programContext: ProgramContext, +): void { const exported = isExported(statement); const mutable = (statement.declarationList.flags & ts.NodeFlags.Const) === 0; for (const declaration of statement.declarationList.declarations) { @@ -561,11 +1001,11 @@ function recordVariableStatement(facts: Facts, statement: ts.VariableStatement, } } addUnique(facts.interfaceLogicEvidence.functionBodies, name); - facts.functionReferences.set(name, apiReferencesInNode(declaration.initializer)); + facts.functionReferences.set(name, apiReferencesInNode(declaration.initializer, sourceFile, programContext)); facts.functionBodies.set(name, declaration.initializer); } else if (declaration.initializer !== undefined && !isLiteralLike(declaration.initializer)) { addUnique(facts.interfaceLogicEvidence.derivedValueBodies, name); - facts.functionReferences.set(name, apiReferencesInNode(declaration.initializer)); + facts.functionReferences.set(name, apiReferencesInNode(declaration.initializer, sourceFile, programContext)); } } } @@ -578,16 +1018,42 @@ function recordNamedDeclaration(facts: Facts, name: string, exported: boolean): } } -function collectNodeFacts(facts: Facts, sourceFile: ts.SourceFile): void { +function collectNodeFacts(facts: Facts, sourceFile: ts.SourceFile, programContext: ProgramContext): void { const visit = (node: ts.Node): void => { + if (ts.isFunctionDeclaration(node) && node.name !== undefined) { + recordEffectRequirementsForFunction(facts, node, programContext); + } + if (ts.isMethodDeclaration(node) && node.name !== undefined) { + recordEffectRequirementsForFunction(facts, node, programContext); + } + if (ts.isVariableDeclaration(node)) { + recordEffectRequirementsForVariable(facts, node, programContext); + } if (ts.isIdentifier(node)) { facts.identifiers.add(node.text); if (effectfulIdentifiers.has(node.text)) { facts.effectfulIdentifiers.add(node.text); + addEffectfulReference(facts, { + kind: "identifier", + category: effectfulIdentifierCategories.get(node.text) ?? "unknown", + reference: node.text, + origin: "global", + enclosingIdentifier: enclosingIdentifier(node), + packageName: "", + summaryId: "", + receiverType: "", + evidence: [`effectful global identifier: ${node.text}`], + }); } } if (ts.isCallExpression(node)) { recordCallableReference(facts.apiReferences, node.expression); + recordLocalCallableReference(facts.apiReferences, node.expression, sourceFile, programContext); + recordDependencyEffectfulReference(facts, node, node.expression, "call", programContext); + recordEffectRequirementsForExpression(facts, node, programContext, "call"); + recordEffectRequirementsForRunnerArgument(facts, node, programContext); + recordEffectRunnerArgumentReferences(facts, node, sourceFile, programContext); + recordEffectConstructorCallbackReferences(facts, node, sourceFile, programContext); const propertyCheck = propertyCheckForCall(facts, node); if (propertyCheck !== undefined) { for (const reference of propertyCheck.references) { @@ -603,6 +1069,8 @@ function collectNodeFacts(facts: Facts, sourceFile: ts.SourceFile): void { } if (ts.isNewExpression(node)) { recordCallableReference(facts.apiReferences, node.expression); + recordLocalCallableReference(facts.apiReferences, node.expression, sourceFile, programContext); + recordDependencyEffectfulReference(facts, node, node.expression, "new", programContext); } if (ts.isTypeReferenceNode(node)) { recordEntityReference(facts.apiReferences, node.typeName); @@ -623,6 +1091,57 @@ function collectNodeFacts(facts: Facts, sourceFile: ts.SourceFile): void { visit(sourceFile); } +function expandEffectfulIdentifiers(facts: Facts, localEffectfulFunctions: Map): void { + let changed = true; + while (changed) { + changed = false; + for (const [name, references] of facts.functionReferences) { + if (!facts.identifiers.has(name) || facts.effectfulIdentifiers.has(name)) { + continue; + } + const expandedReferences = expandedApiReferences(facts, references, new Set()); + const reachedLocalEffects = reachedLocalEffectfulReferences(expandedReferences, localEffectfulFunctions); + if (hasIntersection(expandedReferences, facts.effectfulIdentifiers) || reachedLocalEffects.length > 0) { + facts.effectfulIdentifiers.add(name); + for (const reached of reachedLocalEffects) { + addEffectfulReference(facts, { + kind: "call", + category: reached.effect.category, + reference: reached.reference, + origin: "local-call-expansion", + enclosingIdentifier: name, + packageName: reached.effect.packageName, + summaryId: reached.effect.summaryId, + receiverType: reached.effect.receiverType, + evidence: sorted([ + `local call expansion reached ${reached.reference}`, + ...reached.effect.evidence, + ]), + }); + } + changed = true; + } + } + } +} + +function reachedLocalEffectfulReferences( + expandedReferences: Set, + localEffectfulFunctions: Map, +): { reference: string; effect: EffectfulReferenceFact }[] { + const reached: { reference: string; effect: EffectfulReferenceFact }[] = []; + for (const reference of expandedReferences) { + const effects = localEffectfulFunctions.get(reference); + if (effects === undefined) { + continue; + } + for (const effect of effects) { + reached.push({ reference, effect }); + } + } + return reached; +} + function propertyCheckForCall(facts: Facts, call: ts.CallExpression): PropertyCheckFact | undefined { if (!isFastCheckPropertyCall(call.expression) || call.arguments.length === 0) { return undefined; @@ -771,14 +1290,24 @@ function generatedInputApiUses(facts: Facts, body: ts.Node, name: string): Set { +function apiReferencesInNode( + node: ts.Node, + sourceFile?: ts.SourceFile, + programContext?: ProgramContext, +): Set { const references = new Set(); const visit = (current: ts.Node): void => { if (ts.isCallExpression(current)) { recordCallableReference(references, current.expression); + if (sourceFile !== undefined && programContext !== undefined) { + recordLocalCallableReference(references, current.expression, sourceFile, programContext); + } } if (ts.isNewExpression(current)) { recordCallableReference(references, current.expression); + if (sourceFile !== undefined && programContext !== undefined) { + recordLocalCallableReference(references, current.expression, sourceFile, programContext); + } } if (ts.isTypeReferenceNode(current)) { recordEntityReference(references, current.typeName); @@ -832,6 +1361,90 @@ function recordCallableReference(references: Set, expression: ts.Express } } +function recordLocalCallableReference( + references: Set, + expression: ts.Expression, + sourceFile: ts.SourceFile, + programContext: ProgramContext, +): void { + const reference = localCallableReference(expression, sourceFile, programContext); + if (reference !== undefined) { + references.add(reference); + } +} + +function localCallableReference( + expression: ts.Expression, + sourceFile: ts.SourceFile, + programContext: ProgramContext, +): string | undefined { + const symbolNode = ts.isPropertyAccessExpression(expression) ? expression.name : expression; + const symbol = programContext.checker.getSymbolAtLocation(symbolNode); + if (symbol === undefined) { + return undefined; + } + const resolved = (symbol.flags & ts.SymbolFlags.Alias) !== 0 ? programContext.checker.getAliasedSymbol(symbol) : symbol; + const declaration = resolved.valueDeclaration ?? resolved.declarations?.[0]; + if (declaration === undefined) { + return undefined; + } + const ownerFile = declaration.getSourceFile(); + if (normalizePath(ownerFile.fileName) === normalizePath(sourceFile.fileName)) { + return undefined; + } + if ( + !relativeImportTargetFiles(sourceFile).has(normalizePath(ownerFile.fileName)) + && !isWorkspaceSourceFile(ownerFile, programContext) + ) { + return undefined; + } + return qualifiedLocalReference(ownerFile, resolved.getName()); +} + +function isWorkspaceSourceFile(sourceFile: ts.SourceFile, programContext: ProgramContext): boolean { + return workspacePackageRootForFile(sourceFile.fileName, programContext) !== undefined; +} + +function workspacePackageRootForFile(filePath: string, programContext: ProgramContext): string | undefined { + let current = realNormalizePath(filePath); + if (!existsSync(current) || path.extname(current) !== "") { + current = path.dirname(current); + } + while (current !== path.dirname(current)) { + if (programContext.workspacePackageRoots.has(current)) { + return current; + } + current = path.dirname(current); + } + return undefined; +} + +const relativeImportTargetFileCache = new Map>(); + +function relativeImportTargetFiles(sourceFile: ts.SourceFile): Set { + const sourcePath = normalizePath(sourceFile.fileName); + const cached = relativeImportTargetFileCache.get(sourcePath); + if (cached !== undefined) { + return cached; + } + const targets = new Set(); + for (const statement of sourceFile.statements) { + if (!ts.isImportDeclaration(statement) && !ts.isExportDeclaration(statement)) { + continue; + } + const specifier = statement.moduleSpecifier; + if (specifier === undefined || !ts.isStringLiteral(specifier) || !specifier.text.startsWith(".")) { + continue; + } + const resolved = resolveLocalModuleSpecifier(path.dirname(sourceFile.fileName), specifier.text); + if (resolved !== undefined) { + targets.add(normalizePath(resolved)); + } + } + relativeImportTargetFileCache.set(sourcePath, targets); + return targets; +} + function recordEntityReference(references: Set, node: ts.Node): void { if (ts.isIdentifier(node)) { references.add(node.text); @@ -844,6 +1457,617 @@ function recordEntityReference(references: Set, node: ts.Node): void { } } +function recordDependencyEffectfulReference( + facts: Facts, + node: ts.Node, + expression: ts.Expression, + kind: string, + programContext: ProgramContext, +): void { + const chain = propertyAccessPrefixes(expression); + if (chain.length < 2) { + return; + } + const fullSegments = chain[chain.length - 1]?.segments ?? []; + for (let index = 0; index < chain.length - 1; index += 1) { + const prefix = chain[index]; + if (prefix === undefined) { + continue; + } + const memberPath = fullSegments.slice(prefix.segments.length).join("."); + if (memberPath === "") { + continue; + } + const typeInfo = dependencyTypeInfo(prefix.node, programContext); + if (typeInfo === undefined) { + continue; + } + for (const summary of programContext.dependencySummaries) { + if (summary.packageName !== typeInfo.packageName) { + continue; + } + for (const rule of summary.rules) { + if (!rule.memberPaths.includes(memberPath) || !receiverTypeMatches(typeInfo.typeNames, rule.receiverTypeNames)) { + continue; + } + const enclosing = enclosingIdentifier(node); + addEffectfulReference(facts, { + kind, + category: rule.effectCategory, + reference: memberPath, + origin: "dependency-summary", + enclosingIdentifier: enclosing, + packageName: summary.packageName, + summaryId: rule.id, + receiverType: sorted(typeInfo.typeNames)[0] ?? "", + evidence: rule.evidence, + }); + if (enclosing !== "") { + facts.effectfulIdentifiers.add(enclosing); + } + } + } + } +} + +function recordEffectRequirementsForFunction( + facts: Facts, + declaration: ts.FunctionDeclaration | ts.MethodDeclaration, + programContext: ProgramContext, +): void { + const signature = programContext.checker.getSignatureFromDeclaration(declaration); + if (signature === undefined) { + return; + } + recordEffectRequirementsFromType( + facts, + declaration, + programContext.checker.getReturnTypeOfSignature(signature), + programContext, + "identifier", + "function return type", + ); +} + +function recordEffectRequirementsForVariable( + facts: Facts, + declaration: ts.VariableDeclaration, + programContext: ProgramContext, +): void { + recordEffectRequirementsFromType( + facts, + declaration, + programContext.checker.getTypeAtLocation(declaration.name), + programContext, + "identifier", + "variable type", + ); + if (declaration.initializer !== undefined) { + recordEffectRequirementsFromType( + facts, + declaration, + programContext.checker.getTypeAtLocation(declaration.initializer), + programContext, + "identifier", + "variable initializer type", + ); + } +} + +function recordEffectRequirementsForExpression( + facts: Facts, + expression: ts.Expression, + programContext: ProgramContext, + kind: string, +): void { + recordEffectRequirementsFromType( + facts, + expression, + programContext.checker.getTypeAtLocation(expression), + programContext, + kind, + "expression type", + ); +} + +function recordEffectRequirementsForRunnerArgument( + facts: Facts, + call: ts.CallExpression, + programContext: ProgramContext, +): void { + if (!isEffectRunnerCall(call.expression, programContext)) { + return; + } + const executed = call.arguments[0]; + if (executed === undefined) { + return; + } + recordEffectRequirementsFromType( + facts, + call, + programContext.checker.getTypeAtLocation(executed), + programContext, + "call", + "executed deferred computation type", + ); +} + +function recordEffectRunnerArgumentReferences( + facts: Facts, + call: ts.CallExpression, + sourceFile: ts.SourceFile, + programContext: ProgramContext, +): void { + if (!isEffectRunnerCall(call.expression, programContext)) { + return; + } + const executed = call.arguments[0]; + if (executed === undefined) { + return; + } + addReferencesToEnclosingIdentifier( + facts, + call, + expressionAsApiReferences(executed, sourceFile, programContext), + ); +} + +function isEffectRunnerCall(expression: ts.Expression, programContext: ProgramContext): boolean { + return isEffectNamespaceMemberCall(expression, effectRunnerMemberPaths, programContext); +} + +function recordEffectConstructorCallbackReferences( + facts: Facts, + call: ts.CallExpression, + sourceFile: ts.SourceFile, + programContext: ProgramContext, +): void { + if (!isEffectConstructorCall(call.expression, programContext)) { + return; + } + const callbackReferences = unionAll( + effectConstructorCallbackExpressions(call).map((expression) => + expressionAsApiReferences(expression, sourceFile, programContext), + ), + ); + addReferencesToEnclosingIdentifier(facts, call, callbackReferences); +} + +function isEffectConstructorCall(expression: ts.Expression, programContext: ProgramContext): boolean { + return isEffectNamespaceMemberCall(expression, effectConstructorMemberPaths, programContext); +} + +function isEffectNamespaceMemberCall( + expression: ts.Expression, + memberNames: Set, + programContext: ProgramContext, +): boolean { + if (!ts.isPropertyAccessExpression(expression) || !memberNames.has(expression.name.text)) { + return false; + } + const typeInfo = dependencyTypeInfo(expression.expression, programContext); + return typeInfo?.packageName === "effect" && typeInfo.typeNames.has("Effect"); +} + +function effectConstructorCallbackExpressions(call: ts.CallExpression): ts.Expression[] { + const memberName = ts.isPropertyAccessExpression(call.expression) ? call.expression.name.text : ""; + const first = call.arguments[0]; + if (first === undefined) { + return []; + } + if (memberName === "tryPromise" && ts.isObjectLiteralExpression(first)) { + return first.properties.flatMap((property) => { + if (!ts.isPropertyAssignment(property) || propertyName(property.name) !== "try") { + return []; + } + return [property.initializer]; + }); + } + return [first]; +} + +function expressionAsApiReferences( + expression: ts.Expression, + sourceFile: ts.SourceFile, + programContext: ProgramContext, +): Set { + if (isFunctionLikeExpression(expression)) { + return apiReferencesInNode(expression.body ?? expression, sourceFile, programContext); + } + const references = new Set(); + recordCallableReference(references, expression); + recordLocalCallableReference(references, expression, sourceFile, programContext); + return references; +} + +function addReferencesToEnclosingIdentifier(facts: Facts, node: ts.Node, references: Set): void { + if (references.size === 0) { + return; + } + const enclosing = enclosingIdentifier(node); + if (enclosing === "") { + return; + } + const existing = facts.functionReferences.get(enclosing) ?? new Set(); + for (const reference of references) { + existing.add(reference); + } + facts.functionReferences.set(enclosing, existing); +} + +function unionAll(sets: Set[]): Set { + const values = new Set(); + for (const set of sets) { + for (const value of set) { + values.add(value); + } + } + return values; +} + +function recordEffectRequirementsFromType( + facts: Facts, + node: ts.Node, + type: ts.Type, + programContext: ProgramContext, + kind: string, + evidenceContext: string, +): void { + const enclosing = enclosingIdentifier(node); + if (enclosing === "") { + return; + } + for (const classification of classifiedEffectRequirements(type, programContext.checker, new Set())) { + addEffectfulReference(facts, { + kind, + category: classification.category, + reference: classification.reference, + origin: "dependency-summary", + enclosingIdentifier: enclosing, + packageName: classification.packageName, + summaryId: classification.summaryId, + receiverType: classification.receiverType, + evidence: [`${evidenceContext}: ${classification.evidence}`], + }); + facts.effectfulIdentifiers.add(enclosing); + } +} + +function classifiedEffectRequirements( + type: ts.Type, + checker: ts.TypeChecker, + visited: Set, +): EffectRequirementClassification[] { + const id = typeId(type); + if (id !== undefined) { + if (visited.has(id)) { + return []; + } + visited.add(id); + } + const requirements = effectRequirementTypes(type, checker); + const classifications = requirements.flatMap((requirement) => + classifiedRequirementTypes(requirement, checker, visited), + ); + return uniqueClassifications(classifications); +} + +function effectRequirementTypes(type: ts.Type, checker: ts.TypeChecker): ts.Type[] { + const requirements: ts.Type[] = []; + collectEffectRequirementTypes(type, checker, requirements, new Set()); + return requirements; +} + +function collectEffectRequirementTypes( + type: ts.Type, + checker: ts.TypeChecker, + requirements: ts.Type[], + visited: Set, +): void { + const id = typeId(type); + if (id !== undefined) { + if (visited.has(id)) { + return; + } + visited.add(id); + } + if (type.isUnionOrIntersection()) { + for (const subtype of type.types) { + collectEffectRequirementTypes(subtype, checker, requirements, visited); + } + } + const reference = typeReferenceInfo(type, checker); + if (reference === undefined) { + return; + } + if (reference.packageName === "effect" && reference.typeName === "Effect") { + const requirement = reference.typeArguments[2]; + if (requirement !== undefined && !isEmptyRequirementType(requirement)) { + requirements.push(requirement); + } + } + if (reference.packageName === "effect" && reference.typeName === "Layer") { + const requirement = reference.typeArguments[2]; + if (requirement !== undefined && !isEmptyRequirementType(requirement)) { + requirements.push(requirement); + } + } +} + +function classifiedRequirementTypes( + type: ts.Type, + checker: ts.TypeChecker, + visited: Set, +): EffectRequirementClassification[] { + const id = typeId(type); + if (id !== undefined) { + if (visited.has(id)) { + return []; + } + visited.add(id); + } + if (isEmptyRequirementType(type)) { + return []; + } + if (type.isUnionOrIntersection()) { + return uniqueClassifications(type.types.flatMap((subtype) => classifiedRequirementTypes(subtype, checker, visited))); + } + const classifications: EffectRequirementClassification[] = []; + for (const symbol of symbolsForType(type, checker)) { + const packageName = packageNameForSymbol(symbol); + if (packageName === undefined) { + continue; + } + const key = `${packageName}:${symbol.getName()}`; + const classification = effectRequirementClassifications.get(key); + if (classification !== undefined) { + classifications.push(classification); + } + } + return uniqueClassifications(classifications); +} + +function uniqueClassifications(classifications: EffectRequirementClassification[]): EffectRequirementClassification[] { + const byKey = new Map(); + for (const classification of classifications) { + byKey.set(`${classification.packageName}:${classification.summaryId}:${classification.reference}`, classification); + } + return [...byKey.values()]; +} + +function typeReferenceInfo( + type: ts.Type, + checker: ts.TypeChecker, +): { packageName: string; typeName: string; typeArguments: readonly ts.Type[] } | undefined { + for (const candidate of [type, checker.getApparentType(type)]) { + const symbol = candidate.aliasSymbol ?? candidate.symbol; + if (symbol === undefined) { + continue; + } + const packageName = packageNameForSymbol(symbol); + if (packageName === undefined) { + continue; + } + const typeArguments = typeReferenceArguments(candidate, checker); + return { + packageName, + typeName: symbol.getName(), + typeArguments, + }; + } + return undefined; +} + +function typeReferenceArguments(type: ts.Type, checker: ts.TypeChecker): readonly ts.Type[] { + if ((type.flags & ts.TypeFlags.Object) === 0) { + return []; + } + const objectType = type as ts.ObjectType; + if ((objectType.objectFlags & ts.ObjectFlags.Reference) === 0) { + return []; + } + return checker.getTypeArguments(objectType as ts.TypeReference); +} + +function symbolsForType(type: ts.Type, checker: ts.TypeChecker): ts.Symbol[] { + return [ + type.aliasSymbol, + type.symbol, + checker.getApparentType(type).aliasSymbol, + checker.getApparentType(type).symbol, + ].filter(isDefined); +} + +function packageNameForSymbol(symbol: ts.Symbol): string | undefined { + const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0]; + if (declaration === undefined) { + return undefined; + } + return packageNameForFile(declaration.getSourceFile().fileName); +} + +function isEmptyRequirementType(type: ts.Type): boolean { + return (type.flags & (ts.TypeFlags.Never | ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0; +} + +function typeId(type: ts.Type): number | undefined { + return (type as { id?: number }).id; +} + +function propertyAccessPrefixes(expression: ts.Expression): { node: ts.Expression; segments: string[] }[] { + if (ts.isIdentifier(expression)) { + return [{ node: expression, segments: [expression.text] }]; + } + if (ts.isPropertyAccessExpression(expression)) { + const prefixes = propertyAccessPrefixes(expression.expression); + const previous = prefixes[prefixes.length - 1]; + if (previous === undefined) { + return []; + } + return [...prefixes, { node: expression, segments: [...previous.segments, expression.name.text] }]; + } + return []; +} + +function dependencyTypeInfo( + expression: ts.Expression, + programContext: ProgramContext, +): { packageName: string; typeNames: Set } | undefined { + const type = programContext.checker.getTypeAtLocation(expression); + const declarations = declarationsForType(type, programContext.checker); + const packageNames = new Set(declarations.map((declaration) => packageNameForFile(declaration.getSourceFile().fileName)).filter(isDefined)); + for (const packageName of packageNamesForExpressionSymbol(expression, programContext)) { + packageNames.add(packageName); + } + if (packageNames.size === 0) { + return undefined; + } + const typeNames = typeNamesForType(type, programContext.checker); + const symbolName = expressionSymbolName(expression, programContext); + if (symbolName !== undefined) { + typeNames.add(symbolName); + } + for (const packageName of packageNames) { + return { packageName, typeNames }; + } + return undefined; +} + +function packageNamesForExpressionSymbol(expression: ts.Expression, programContext: ProgramContext): Set { + const packageNames = new Set(); + const symbol = expressionSymbol(expression, programContext); + if (symbol === undefined) { + return packageNames; + } + for (const declaration of symbol.declarations ?? []) { + const packageName = packageNameForFile(declaration.getSourceFile().fileName); + if (packageName !== undefined) { + packageNames.add(packageName); + } + } + return packageNames; +} + +function expressionSymbolName(expression: ts.Expression, programContext: ProgramContext): string | undefined { + return expressionSymbol(expression, programContext)?.getName(); +} + +function expressionSymbol(expression: ts.Expression, programContext: ProgramContext): ts.Symbol | undefined { + const symbol = programContext.checker.getSymbolAtLocation(expression); + if (symbol === undefined) { + return undefined; + } + return (symbol.flags & ts.SymbolFlags.Alias) !== 0 ? programContext.checker.getAliasedSymbol(symbol) : symbol; +} + +function declarationsForType(type: ts.Type, checker: ts.TypeChecker): ts.Declaration[] { + const symbols = [ + type.symbol, + type.aliasSymbol, + checker.getApparentType(type).symbol, + checker.getApparentType(type).aliasSymbol, + ].filter(isDefined); + return symbols.flatMap((symbol) => symbol.declarations ?? []); +} + +function typeNamesForType(type: ts.Type, checker: ts.TypeChecker): Set { + const names = new Set(); + for (const symbol of [ + type.symbol, + type.aliasSymbol, + checker.getApparentType(type).symbol, + checker.getApparentType(type).aliasSymbol, + ]) { + if (symbol !== undefined) { + names.add(symbol.getName()); + } + } + names.add(stripTypeArguments(checker.typeToString(type))); + names.add(stripTypeArguments(checker.typeToString(checker.getApparentType(type)))); + return names; +} + +function stripTypeArguments(value: string): string { + return value.replace(/<.*$/u, ""); +} + +const packageNameByDirectory = new Map(); + +function packageNameForFile(filePath: string): string | undefined { + let current = path.dirname(filePath); + while (current !== path.dirname(current)) { + if (packageNameByDirectory.has(current)) { + return packageNameByDirectory.get(current); + } + const packageJsonPath = path.join(current, "package.json"); + if (existsSync(packageJsonPath)) { + const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { name?: unknown }; + const packageName = typeof packageJson.name === "string" ? packageJson.name : undefined; + if (packageName !== undefined) { + packageNameByDirectory.set(current, packageName); + return packageName; + } + } + current = path.dirname(current); + } + return undefined; +} + +function enclosingIdentifier(node: ts.Node): string { + let current: ts.Node | undefined = node; + while (current !== undefined) { + if (ts.isFunctionDeclaration(current) && current.name !== undefined) { + return current.name.text; + } + if (ts.isMethodDeclaration(current) && current.name !== undefined) { + return propertyName(current.name); + } + if (ts.isVariableDeclaration(current) && ts.isIdentifier(current.name) && isBoundaryVariableDeclaration(current)) { + return current.name.text; + } + current = current.parent; + } + return ""; +} + +function isBoundaryVariableDeclaration(node: ts.VariableDeclaration): boolean { + if (node.initializer !== undefined && isFunctionLikeExpression(node.initializer)) { + return true; + } + return ts.isVariableDeclarationList(node.parent) + && ts.isVariableStatement(node.parent.parent) + && ts.isSourceFile(node.parent.parent.parent); +} + +function addEffectfulReference(facts: Facts, fact: EffectfulReferenceFact): void { + facts.effectfulReferences.set(effectfulReferenceKey(fact), { + ...fact, + evidence: sorted(fact.evidence), + }); +} + +function effectfulReferenceKey(fact: EffectfulReferenceFact): string { + return [ + fact.kind, + fact.category, + fact.reference, + fact.origin, + fact.enclosingIdentifier, + fact.packageName, + fact.summaryId, + fact.receiverType, + ...fact.evidence, + ].join("\u0000"); +} + +function effectfulReferenceFacts(facts: Map): EffectfulReferenceFact[] { + return [...facts.values()].sort((left, right) => effectfulReferenceKey(left).localeCompare(effectfulReferenceKey(right))); +} + +function effectfulReferenceFactsForIdentifier(facts: Facts, identifier: string): EffectfulReferenceFact[] { + return effectfulReferenceFacts(facts.effectfulReferences).filter((fact) => fact.enclosingIdentifier === identifier); +} + function parseMetadata(source: string): Metadata { const metadata: Metadata = { moduleType: "", domain: "", exemptReason: "" }; const seen = new Set(); @@ -1030,6 +2254,23 @@ function hasIntersection(left: Set, right: Set): boolean { return false; } +function hasAnyString(left: Set, right: string[]): boolean { + for (const value of right) { + if (left.has(value)) { + return true; + } + } + return false; +} + +function receiverTypeMatches(typeNames: Set, receiverTypeNames: string[]): boolean { + return receiverTypeNames.length === 0 || hasAnyString(typeNames, receiverTypeNames); +} + +function isDefined(value: T | undefined): value is T { + return value !== undefined; +} + function addUnique(values: string[], value: string): void { if (value !== "" && !values.includes(value)) { values.push(value); @@ -1044,4 +2285,12 @@ function normalizePath(filePath: string): string { return path.resolve(filePath); } +function realNormalizePath(filePath: string): string { + try { + return path.resolve(realpathSync(filePath)); + } catch { + return path.resolve(filePath); + } +} + main(); diff --git a/typescript/test.sh b/typescript/test.sh index d245fb2..695397a 100755 --- a/typescript/test.sh +++ b/typescript/test.sh @@ -33,7 +33,12 @@ assert check["generatedInputs"] == [{"name": "value", "uses": ["value"]}], check handler = [item for item in document["files"] if item["path"].endswith("handler.ts")][0] assert "commander" in handler["effectfulImports"], handler assert "Command" in handler["effectfulIdentifiers"], handler +assert "Bun" in handler["effectfulIdentifiers"], handler +assert "dnsLookup" in handler["effectfulIdentifiers"], handler +assert "fetch" in handler["effectfulIdentifiers"], handler +assert "setTimeout" in handler["effectfulIdentifiers"], handler assert "createCommand" in handler["effectfulIdentifiers"], handler +assert "usePlatformEffects" in handler["effectfulIdentifiers"], handler assert "run" in handler["effectfulIdentifiers"], handler # moduleName is the capitalized file basename and qualifiedReferences use the # same prefix for semantic cross-file references. @@ -60,3 +65,261 @@ copy_fixture "$ROOT/typescript/fixtures/effectful-expansion" "$TMPDIR" expect_violation "effectful identifiers may only appear in shell, state, interface, test, stateTest, or exempt modules" \ uv run --project "$ROOT" python "$ROOT/evaluate.py" \ --repo-root "$TMPDIR" --adapter typescript --typescript-root . + +copy_fixture "$ROOT/typescript/fixtures/sentry-dependency" "$TMPDIR" + +npm --prefix "$ROOT/typescript" run --silent archlint -- --repo-root "$TMPDIR" --typescript-root . > "$TMPDIR/sentry-facts.json" +assert_facts "$TMPDIR/sentry-facts.json" <<'PY' +import json +import sys + +document = json.load(open(sys.argv[1], encoding="utf-8")) +core = [item for item in document["files"] if item["path"].endswith("sentry-core.ts")][0] +effects = core["effectfulReferences"] +sentry_effects = [ + effect for effect in effects + if effect["origin"] == "dependency-summary" + and effect["packageName"] == "@sentry/node" +] +by_reference = {effect["reference"]: effect for effect in sentry_effects} +assert sorted(by_reference) == ["captureException", "flush", "init"], effects +assert by_reference["init"]["category"] == "network", by_reference["init"] +assert by_reference["init"]["summaryId"] == "sentry-node-init-setup-effect", by_reference["init"] +assert by_reference["captureException"]["category"] == "network", by_reference["captureException"] +assert by_reference["captureException"]["summaryId"] == "sentry-node-capture-exception-network", by_reference["captureException"] +assert by_reference["flush"]["category"] == "network", by_reference["flush"] +assert by_reference["flush"]["summaryId"] == "sentry-node-flush-network", by_reference["flush"] +assert "getClient" not in by_reference, effects +assert "reportFailure" in core["effectfulIdentifiers"], core +local_shape_effects = [ + effect for effect in effects + if effect["enclosingIdentifier"] == "localShape" + and effect["origin"] == "dependency-summary" +] +assert local_shape_effects == [], effects +PY + +expect_violation "effectful identifiers may only appear in shell, state, interface, test, stateTest, or exempt modules" \ + uv run --project "$ROOT" python "$ROOT/evaluate.py" \ + --repo-root "$TMPDIR" --adapter typescript --typescript-root . + +copy_fixture "$ROOT/typescript/fixtures/openai-dependency" "$TMPDIR" + +npm --prefix "$ROOT/typescript" run --silent archlint -- --repo-root "$TMPDIR" --typescript-root . > "$TMPDIR/openai-facts.json" +assert_facts "$TMPDIR/openai-facts.json" <<'PY' +import json +import sys + +document = json.load(open(sys.argv[1], encoding="utf-8")) +core = [item for item in document["files"] if item["path"].endswith("openai-core.ts")][0] +effects = core["effectfulReferences"] +openai_effects = [ + effect for effect in effects + if effect["origin"] == "dependency-summary" + and effect["packageName"] == "openai" +] +by_identifier = { + effect["enclosingIdentifier"]: effect + for effect in openai_effects +} +assert by_identifier["runInference"]["reference"] == "chat.completions.create", effects +assert by_identifier["runInference"]["category"] == "network", effects +assert by_identifier["runInference"]["summaryId"] == "openai-chat-completions-create-network", effects +assert by_identifier["runInference"]["receiverType"] == "OpenAI", effects +assert by_identifier["embedText"]["reference"] == "embeddings.create", effects +assert by_identifier["embedText"]["category"] == "network", effects +assert by_identifier["embedText"]["summaryId"] == "openai-embeddings-create-network", effects +assert "runInference" in core["effectfulIdentifiers"], core +assert "embedText" in core["effectfulIdentifiers"], core +assert "configureClient" not in core["effectfulIdentifiers"], core +local_shape_effects = [ + effect for effect in effects + if effect["enclosingIdentifier"] == "localShape" + and effect["origin"] == "dependency-summary" +] +assert local_shape_effects == [], effects +PY + +expect_violation "effectful identifiers may only appear in shell, state, interface, test, stateTest, or exempt modules" \ + uv run --project "$ROOT" python "$ROOT/evaluate.py" \ + --repo-root "$TMPDIR" --adapter typescript --typescript-root . + +copy_fixture "$ROOT/typescript/fixtures/local-relative-expansion" "$TMPDIR" + +npm --prefix "$ROOT/typescript" run --silent archlint -- --repo-root "$TMPDIR" --typescript-root src > "$TMPDIR/local-relative-facts.json" +assert_facts "$TMPDIR/local-relative-facts.json" <<'PY' +import json +import sys + +document = json.load(open(sys.argv[1], encoding="utf-8")) +core = [ + item for item in document["files"] + if item["metadata"]["domain"] == "demo.local-relative-expansion" +][0] +assert "decideViaExternalHelper" in core["effectfulIdentifiers"], core +effects = [ + effect for effect in core["effectfulReferences"] + if effect["origin"] == "local-call-expansion" +] +assert len(effects) == 1, effects +effect = effects[0] +assert effect["reference"] == "Effects.effectfulHelper", effect +assert effect["category"] == "network", effect +assert effect["enclosingIdentifier"] == "decideViaExternalHelper", effect +assert any("local call expansion reached Effects.effectfulHelper" in item for item in effect["evidence"]), effect +PY + +expect_violation "effectful identifiers may only appear in shell, state, interface, test, stateTest, or exempt modules" \ + uv run --project "$ROOT" python "$ROOT/evaluate.py" \ + --repo-root "$TMPDIR" --adapter typescript --typescript-root src + +copy_fixture "$ROOT/typescript/fixtures/workspace-package-expansion" "$TMPDIR" + +npm --prefix "$ROOT/typescript" run --silent archlint -- --repo-root "$TMPDIR" --typescript-root packages/consumer > "$TMPDIR/workspace-package-facts.json" +assert_facts "$TMPDIR/workspace-package-facts.json" <<'PY' +import json +import sys + +document = json.load(open(sys.argv[1], encoding="utf-8")) +core = [ + item for item in document["files"] + if item["metadata"]["domain"] == "demo.workspace-package-expansion" +][0] +assert "decideViaWorkspacePackage" in core["effectfulIdentifiers"], core +effects = [ + effect for effect in core["effectfulReferences"] + if effect["origin"] == "local-call-expansion" +] +assert len(effects) == 1, effects +effect = effects[0] +assert effect["reference"] == "Index.effectfulHelper", effect +assert effect["category"] == "network", effect +assert effect["enclosingIdentifier"] == "decideViaWorkspacePackage", effect +assert any("local call expansion reached Index.effectfulHelper" in item for item in effect["evidence"]), effect +PY + +expect_violation "effectful identifiers may only appear in shell, state, interface, test, stateTest, or exempt modules" \ + uv run --project "$ROOT" python "$ROOT/evaluate.py" \ + --repo-root "$TMPDIR" --adapter typescript --typescript-root packages/consumer + +copy_fixture "$ROOT/typescript/fixtures/trigger-dependency" "$TMPDIR" + +npm --prefix "$ROOT/typescript" run --silent archlint -- --repo-root "$TMPDIR" --typescript-root . > "$TMPDIR/trigger-facts.json" +assert_facts "$TMPDIR/trigger-facts.json" <<'PY' +import json +import sys + +document = json.load(open(sys.argv[1], encoding="utf-8")) +core = [item for item in document["files"] if item["path"].endswith("trigger-core.ts")][0] +effects = core["effectfulReferences"] +metadata_effects = [ + effect for effect in effects + if effect["origin"] == "dependency-summary" + and effect["packageName"] == "@trigger.dev/sdk" + and effect["reference"] == "append" +] +assert len(metadata_effects) == 1, effects +assert metadata_effects[0]["category"] == "storage", metadata_effects[0] +assert metadata_effects[0]["summaryId"] == "trigger-sdk-metadata-append-runtime-metadata", metadata_effects[0] +logger_effects = [ + effect for effect in effects + if effect["origin"] == "dependency-summary" + and effect["packageName"] == "@trigger.dev/core" + and effect["reference"] == "warn" +] +assert len(logger_effects) == 1, effects +assert logger_effects[0]["category"] == "console", logger_effects[0] +assert logger_effects[0]["summaryId"] == "trigger-core-logger-runtime-log", logger_effects[0] +local_shape_effects = [ + effect for effect in effects + if effect["enclosingIdentifier"] == "localShape" + and effect["origin"] == "dependency-summary" +] +assert local_shape_effects == [], effects +assert "emitTriggerEvent" in core["effectfulIdentifiers"], core +assert "localShape" not in core["effectfulIdentifiers"], core +PY + +expect_violation "effectful identifiers may only appear in shell, state, interface, test, stateTest, or exempt modules" \ + uv run --project "$ROOT" python "$ROOT/evaluate.py" \ + --repo-root "$TMPDIR" --adapter typescript --typescript-root . + +copy_fixture "$ROOT/typescript/fixtures/effect-requirements" "$TMPDIR" + +npm --prefix "$ROOT/typescript" run --silent archlint -- --repo-root "$TMPDIR" --typescript-root . > "$TMPDIR/effect-facts.json" +assert_facts "$TMPDIR/effect-facts.json" <<'PY' +import json +import sys + +document = json.load(open(sys.argv[1], encoding="utf-8")) +core = [item for item in document["files"] if item["path"].endswith("effect-core.ts")][0] +effects = core["effectfulReferences"] +by_identifier = { + effect["enclosingIdentifier"]: effect + for effect in effects + if effect["origin"] == "dependency-summary" +} +assert by_identifier["loadDirect"]["reference"] == "SqlClient.SqlClient", effects +assert by_identifier["loadDirect"]["category"] == "database", effects +assert by_identifier["loadDirect"]["summaryId"] == "effect-requirement-sqlclient", effects +assert by_identifier["loadViaBinding"]["reference"] == "SqlClient.SqlClient", effects +assert by_identifier["layerNeedsSql"]["reference"] == "SqlClient.SqlClient", effects +assert by_identifier["pgRequirement"]["reference"] == "PgClient.PgClient", effects +assert by_identifier["pgRequirement"]["packageName"] == "@effect/sql-pg", effects +assert by_identifier["runSqlRequirement"]["reference"] == "SqlClient.SqlClient", effects +assert by_identifier["runSqlRequirement"]["category"] == "database", effects +assert any("executed deferred computation type" in item for item in by_identifier["runSqlRequirement"]["evidence"]), effects +assert "loadDirect" in core["effectfulIdentifiers"], core +assert "loadViaBinding" in core["effectfulIdentifiers"], core +assert "layerNeedsSql" in core["effectfulIdentifiers"], core +assert "pgRequirement" in core["effectfulIdentifiers"], core +assert "runSqlRequirement" in core["effectfulIdentifiers"], core +assert "platformFetch" in core["effectfulIdentifiers"], core +assert "fetchProgram" in core["effectfulIdentifiers"], core +assert "tryPromiseProgram" in core["effectfulIdentifiers"], core +assert "runFetchProgram" in core["effectfulIdentifiers"], core +assert "localOnly" not in core["effectfulIdentifiers"], core +assert "localLayer" not in core["effectfulIdentifiers"], core +assert "runPureEffect" not in core["effectfulIdentifiers"], core +PY + +expect_violation "effectful identifiers may only appear in shell, state, interface, test, stateTest, or exempt modules" \ + uv run --project "$ROOT" python "$ROOT/evaluate.py" \ + --repo-root "$TMPDIR" --adapter typescript --typescript-root . + +copy_fixture "$ROOT/typescript/fixtures/anthropic-dependency" "$TMPDIR" + +npm --prefix "$ROOT/typescript" run --silent archlint -- --repo-root "$TMPDIR" --typescript-root . > "$TMPDIR/anthropic-facts.json" +assert_facts "$TMPDIR/anthropic-facts.json" <<'PY' +import json +import sys + +document = json.load(open(sys.argv[1], encoding="utf-8")) +core = [item for item in document["files"] if item["path"].endswith("anthropic-core.ts")][0] +effects = core["effectfulReferences"] +anthropic_effects = [ + effect for effect in effects + if effect["origin"] == "dependency-summary" + and effect["packageName"] == "@anthropic-ai/sdk" + and effect["reference"] == "messages.create" +] +assert len(anthropic_effects) == 1, effects +effect = anthropic_effects[0] +assert effect["kind"] == "call", effect +assert effect["category"] == "network", effect +assert effect["summaryId"] == "anthropic-sdk-messages-create-network", effect +assert effect["receiverType"] == "Anthropic", effect +assert effect["enclosingIdentifier"] == "createMessage", effect +assert "createMessage" in core["effectfulIdentifiers"], core +assert "runModelTurn" in core["effectfulIdentifiers"], core +local_shape_effects = [ + effect for effect in effects + if effect["enclosingIdentifier"] == "localShape" + and effect["origin"] == "dependency-summary" +] +assert local_shape_effects == [], effects +PY + +expect_violation "effectful identifiers may only appear in shell, state, interface, test, stateTest, or exempt modules" \ + uv run --project "$ROOT" python "$ROOT/evaluate.py" \ + --repo-root "$TMPDIR" --adapter typescript --typescript-root .